Complete Roguelike Tutorial, using Python+libtcod, extras

From RogueBasin
Revision as of 21:30, 21 December 2015 by Vtrbender (talk | contribs) (added missing statement for example to work)
Jump to navigation Jump to search

This is part of a series of tutorials; the main page can be found here.


Extras

A neat Python shortcut for Notepad++

Although Notepad++ is light and has many nice features, it can be tricky to set up a shortcut to quickly run your game and see any errors or debug output. After reading the docs on Run commands, you might try to open a console (cmd) that doesn't close after the program runs so you can see debug output (/k), and using the path to the currently open file:


cmd /k "$(FULL_CURRENT_PATH)"


You then may puzzle over the following error (or similar), which doesn't seem connected to Notepad++ at all:


WindowsError: [Error 126] The specified module could not be found


The problem is this: Notepad++ and Python are smart enough to run the file, but not smart enough to initialize the "current directory" to the file's directory. So when trying to load libtcod, Python looks for it in all the usual places except where you put libtcod. (Computers can be so thick sometimes!) The fix is to previously "change directory" there, which can be done with a batch file.

Here's my setup. I want to launch the file in a console, so I can see any debug output, and I wanna keep it around in case of errors (to see the traceback) but close it automatically if the program runs fine -- accumulating lots of console windows when there are no errors is annoying. (To explicitly "pause" the console even when there are no errors call the built-in function raw_input from your program.) I simply created a windows batch file debug_py.bat with this:


@echo off

cd %1
%2

if not errorlevel 1 goto quit
echo.
echo.
pause
:quit


Assuming parameter %1 is the file's directory, and %2 is the file name, this just changes to that dir and runs it, pausing the console if there's an error. The notepad++ shortcut that executes it with the correct parameters is this:


"C:\whatever\debug_py.bat" "$(CURRENT_DIRECTORY)" $(FILE_NAME)


Just change "C:\whatever\debug_py.bat" to the full path of the bat file you created. To create that shortcut in Notepad++ go to menu Run->Run..., paste that line into the textbox, then choose Save. No more problems, and the console closes automatically if there are no errors.


Old-school wall and floor tiles

Not a big fan of the new-school look of the map tiles in the tutorial? Perhaps you want your game to look more like one of the Major Roguelikes, with ' . ' characters for floor and ' # ' for wall tiles. No problem, just a couple of tweaks.

First, the most obvious one: in the render_all() function, replace the console_set_back calls, which only change the background color of a tile, with console_put_char_ex, which changes everything (here's the relevant page of the manual.). So a line like this:


libtcod.console_set_back(con, x, y, color_dark_wall, libtcod.BKGND_SET)


Changes to something like this:


libtcod.console_put_char_ex(con, x, y, '#', libtcod.white, libtcod.dark_blue)


Which will change it to a white ' # ' character on a dark blue background. This is only an example; I'm sure you'd like to choose other colors!

If you change the floor characters too, it doesn't quite behave as expected, since the player erases the floor characters it steps on. To fix this you need to change the Object 's clear method, to this:


    def clear(self):
        #erase the character that represents this object
        if libtcod.map_is_in_fov(fov_map, self.x, self.y):
            libtcod.console_put_char_ex(con, self.x, self.y, '.', libtcod.white, libtcod.dark_blue)


Assuming, of course, those are the colors and character you wanted for a lit ground tile!


Real-time combat

Ok, so you're making a real-time game. You got through the section on combat, but the system described there is more or less turn-based. Don't despair! The only thing missing is a speed system.

Each object will have a wait value, which tells the number of frames it has to wait until it can take another action (move or attack). It's decreased by 1 every frame, and when it's 0, the object can move or hit again! Doing it will increase the wait value, so it has to wait again.

We'll start by defining a few speed constants, which are the values that wait is increased to when taking certain actions.


#number of frames to wait after moving/attacking
PLAYER_SPEED = 2
DEFAULT_SPEED = 8
DEFAULT_ATTACK_SPEED = 20


As you can see, we're making it a bit easier on the player, so he can maneuver around the monsters fast, but both attack at the same rate. These numbers are entirely tweakable of course!

The Object 's __init__ method must accept a move speed. Just add speed=DEFAULT_SPEED as a parameter; if unspecified, the default speed is used. The initialization code stores it, and sets wait to 0:


        self.speed = speed
        self.wait = 0


Whenever the object moves, it has to wait. At the end of the move method:


self.wait = self.speed


The Fighter class stores the attack speed, so it's very similar. Its __init__ method accepts the parameter attack_speed=DEFAULT_ATTACK_SPEED, and stores it with self.attack_speed = attack_speed. At the end of the attack method, the object has to wait until it can attack or move again:


        self.owner.wait = self.attack_speed


Ok, but how do we enforce these wait periods? For the player, before testing for the movement keys (in handle_keys), add the wait logic right after the if game_state == 'playing' ... line:


        if player.wait > 0:  #don't take a turn yet if still waiting
            player.wait -= 1
            return


So the movement/attack keys aren't used if the player has to wait. Next, the same behavior for the monsters! Replace the object.ai.take_turn() line with the block:


                if object.wait > 0:  #don't take a turn yet if still waiting
                    object.wait -= 1
                else:
                    object.ai.take_turn()


Also, there's a condition that only lets the monsters move/attack after the player moved or attacked. This is appropriate for a turn-based game, but for real-time, it has to be removed. A few lines above the code you just modified, replace the line if game_state == 'playing' and player_action != 'didnt-take-turn': with just if game_state == 'playing':.

And don't forget to switch to the non-blocking function in handle_keys():


    key = libtcod.console_check_for_keypress()


There it is, a speed system for a real-time game! Don't forget to add speed=PLAYER_SPEED when creating the player, or any other speeds you want to modify. I left all the others as default.


Creating a Binary

Source is great, but let's be honest, forcing your players to recreate your development environment isn't very polite. Let's do them the courtesy of packaging it all up in a nice and tidy executable.

Windows+py2exe

Py2exe is a set of tools for creating stand-alone Windows programs from python scripts. Perfect, this is exactly what we want! Download py2exe (version 0.6.9) for your installed version of Python and install it. The project homepage has a nice tutorial which can help get you started, or you can use my script I've provided below.

If you are using my script, set the target_file to the name of the script that serves as the main point of entry for your game. Then just run the script! You should see a deal of console text about byte-compiling and copying, and if all goes well there should be two new directories. The dist folder is the one we care about, so go check it out.

Inside the dist folder there should be a shiny new executable file and a few other files. In my test I had: main.exe, SDL.dll, lib-tcod.dll, and w9xpopen.exe. Don't delete these, you need them! Run the executable to verify everything works fine and you are ready to package and distribute your game!

from distutils.core import setup
import py2exe
import os
import sys

sys.argv.append('py2exe')

# The filename of the script you use to start your program.
target_file = 'main.py'

# The root directory containing your assets, libraries, etc.
assets_dir = '.\\'

# Filetypes not to be included in the above.
excluded_file_types = ['py','pyc','project','pydevproject']

def get_data_files(base_dir, target_dir, list=[]):
    """
    " * get_data_files
    " *    base_dir:    The full path to the current working directory.
    " *    target_dir:  The directory of assets to include.
    " *    list:        Current list of assets. Used for recursion.
    " *
    " *    returns:     A list of relative and full path pairs. This is 
    " *                 specified by distutils.
    """
    for file in os.listdir(base_dir + target_dir):
        
        full_path = base_dir + target_dir + file
        if os.path.isdir(full_path):
            get_data_files(base_dir, target_dir + file + '\\', list)
        elif os.path.isfile(full_path):
            if (len(file.split('.')) == 2 and file.split('.')[1] not in excluded_file_types):
                list.append((target_dir, [full_path]))
            
    return list

# The directory of assets to include.
my_files = get_data_files(sys.path[0] + '\\', assets_dir)

# Build a dictionary of the options we want.
opts = { 'py2exe': {
                    'ascii':'True',
                    'excludes':['_ssl','_hashlib'],
                    'includes' : ['anydbm', 'dbhash'],
                    'bundle_files':'1',
                    'compressed':'True'}}

# Run the setup utility.
setup(console=[target_file],
      data_files=my_files,
      zipfile=None,
      options=opts)

Linux+cx_Freeze

Cx_Freeze is a set of tools for creating Linux executables.


Mac+py2app

Py2app is a set of tools for creating Mac OSX applications.