Rogue Detective Part 2 – Curses!

Last time, we covered the rather ridiculous choices that led up to my decision to write a roguelike from scratch. Now, we’ll cover the first steps in the creation of Rogue Detective, and indeed, most games I write.

After the standard step of creating a new repository for source control comes the creation of the first source file. This is usually the most daunting part of development: Opening up your editor and seeing a completely blank file that needs to be populated. Step one: Save this blank file as ‘main.py’. Great, we’re half-way to finishing the game, but there’s no time for a break quite yet.

I already knew we’d be using the curses library, so a good first step would be to import it. Additionally, I wrote a barebones do-nothing main function and uses a curses utility function to call it. At this point, here’s the totality of what I had:

import curses

def main(stdscr):
    pass

if __name__ == '__main__':
    """Handles all the nasty stuff to do with curses set-up + tear-down"""
    curses.wrapper(main)

As part of the wrapper function, the main is called and passed a variable called stdscr. This is a curses Screen object. This is our canvas upon which we’ll draw our player character, NPCs; the world, basically. This will be passed to the object that controls the world, so it can also control the canvas. Calling the function via the wrapper serves a couple of purposes:

  1. The wrapper handles all curses set-up and tear-down, as mentioned in the docstring. It will initialise curses and set up various desirable cursor attributes such as disabling echo and enabling instant keyboard inputs. After the passed in function has ended, it shuts down curses gracefully.
  2. In case of an unhandled exception in the called function, the wrapper will catch it, shut down curses correctly, and then re-throw the exception. This prevents such a crash from destroying your terminal settings.

I went in to this particular stretch of work with the goal of having my character (an @ symbol) move around the screen controlled by the arrow keys. With the program successfully bootstrapped and in a ‘working state’, it was time to accomplish that goal.

I wanted to keep the main function as lightweight as possible, leaving it as only the entry point to the application and nothing more. I accomplished this by writing a Game class which the main would instantiate and then call a function of. In this case, the function is called mainLoop, and as you might expect, it is responsible for controlling the game loop itself. Before we get in to that, let’s examine the members of the Game class.

class Game:
    """The game logic itself. The loop and input handling are here."""
    def __init__(self, screen):
        """Create the screen, player, assets."""
        self.screen = screen
        self.running = True
        self.player = Player()

So all we have right now are variables for storing the aforementioned Screen instance, another for storing the state of the game (running, not running), and one containing a currently mysterious Player object. How about that mainLoop I mentioned?

    def mainLoop(self):
        """Run the game while a flag is set."""
        while (self.running):
            self.logic()
            self.draw()
            self.handleInput()

As long as the game is running, we run all the logic for the game, draw the resulting position, then wait for player input.

    def logic(self):
        pass

No game logic exists at this point, so logic is an empty function.

    def draw(self):
        """ Draw it all, filling in the blanks as necessary """
        self.screen.erase()
        player = self.player
        self.screen.addstr(player.y, player.x,
                           player.character, curses.color_pair(1))
        self.screen.move(player.y, player.x)
        self.screen.refresh()

draw actually does something useful. It clears the previous screen, grabs the Player object, draws it in the right place, and finally moves the cursor to the player’s position. That last step is one of the many niceties of a console-based roguelike: if we don’t put it directly on the player character, there are two problems:

  1. If we don’t uniquely colour the player character, the player (user, not game object) won’t be able to identify his character if multiple objects share the same symbol.
  2. If we don’t put the cursor on the player character, it has to go somewhere else. And where else would we even want to put it? It needs to go somewhere. Because of problem one, it makes sense to place it on the player character when possible.
    def handleInput(self):
        """ Wait for the player to press a key, then handle
            input appropriately."""
        character = self.screen.getch()
        if character == ord('q'):
            self.running = False
        elif character == ord('h'):
            self.player.move(Direction.LEFT)
        elif character == ord('j'):
            self.player.move(Direction.DOWN)
        elif character == ord('k'):
            self.player.move(Direction.UP)
        elif character == ord('l'):
            self.player.move(Direction.RIGHT)

Given the turn-based nature of the game, there’s no need to do anything particularly clever like pump event queues or have time-out functions. We explicitly wait for the player to input a character via a Screen function, check the result against the list of characters we’re expecting, and perform the associated action if we get a match. In this case, we call one of the Player functions to move the character around the screen. You’ll also notice we pass in an enum. In Python, enums are usually just static class variables grouped in a class that becomes the enum’s name:

class Direction:
    """An enum for the possible movement directions"""
    UP = 1
    DOWN = 2
    LEFT = 3
    RIGHT = 4

In this case, nothing clever is going on: An enum of four elements in declared for the four directions of movement. Let’s cover the Player object now:

class Player(object):
    """The player object, containing data such as HP etc."""
    def __init__(self):
        """Initialise the player object"""
        self.x = 20
        self.y = 30
        self.character = '@'

At the point in development, the Player contains only information strictly required to render it: Its position on the screen (y, x) and the character we should use to represent it. This is used by the draw function in the above Game class. It has a single member function:

    def move(self, direction):
        """Move the player one unit in the specified direction"""
        if direction == Direction.UP:
            self.y -= 1
        elif direction == Direction.DOWN:
            self.y += 1
        elif direction == Direction.LEFT:
            self.x -= 1
        elif direction == Direction.RIGHT:
            self.x += 1

This gets called by the handleInput function, and moves the player in the given direction. I should probably mention that this code will let you move the player character out-of-bounds of the Screen object. This throws an exception and crashes the program. Additionally, you can’t move to the bottom-right most part of the screen, for a much more subtle reason: Curses will draw the @ symbol in the very last available space, but then attempt to move the cursor along to a location that doesn’t exist in the screen space. Both of these problems will be fixed later on.

At this point, we can revisit the main function and see how we incorporate this new Game object. There’s no surprises here:

def main(stdscr):
    """Initialises the Game object and basically gets out of the way"""
    stdscr.bkgd(' ', curses.color_pair(0))
    game = Game(stdscr)
    game.mainLoop()

We set the ‘background’ to be made up of blank characters. If we ever erase the screen, it is actually filled with the background character, as set by the bkgd function. We then instantiate and start the game.

That’s it! The actual code looks a little different for the first commit (there’s a superfluous class floating around, and some of the code has been changed here for clarity). How does it look? Well, take a look:

FirstCommit

In the future, we won’t cover whole commits. Indeed, we won’t cover each commit, but rather the functional milestones of the project. Hopefully you’ve gained a little bit on insight in to how a Python curses project starts life. Next time, we’ll look at the implementation of walls and other standard obstacles you tend to see in roguelikes.

Rogue Detective Part 1 – A Defective Rogue

Rogue Detective started off as an attempt at the annual 7 Day Roguelike competition, wherein one aspires to create a full roguelike in a mere seven days. Entrants are allowed to use previous work, provided the final game is a new roguelike unto itself.

This year, the competition fell in one of the two weeks of my holiday from work. Being one of the many proud staycationers so prevalent in England, this meant I would have the full week to develop a game! I heard about the competition one day in advance of its start, which gave me a little bit of time to start mulling over a few ideas I had.

At the same time, I started considering at my options regarding development. I already knew what my editor would be: Emacs, of course! I figured if I was going to pull this off in a reasonable time frame, I’d need to use a language I was comfortable developing in. That meant a choice between C(++) and Python. Given how I’d recently¬†worked on a Python game, the language was still fresh in my mind. Indeed, for speedy development I am in general more comfortable in Python.

Editor sorted, language sorted.. Now I needed to consider what libraries might exist out there to help me. I vaguely remembered NetHack using the curses library for terminal stuff, so the first thing I did was check for Python bindings. Sure enough, a Python library for curses existed. At this point, I hadn’t even considered a graphical roguelike. I knew I had no chance to produce something pretty, so I would stick to ascii. I started work on getting an @ symbol wandering the void. This is not the most thorough approach to library selection, but yeah! Limited time means limited research.

I hadn’t quite realised what I was undertaking: A complete roguelike from scratch, save for terminal character manipulation. Given only a week, surely the typical entrant would have to take the whole time to finish something even remotely playable, eschewing work and a social life?

Meanwhile, my friend Ariff was considering entering the same competition. The first thing he did was check for the already existing roguelike libraries that could help. Of course! Why wouldn’t something so obviously useful exist?

For Python, there already exists the libtcod library. Rather than build everything from scratch, libtcod provides users with useful functionality such as built-in dungeon generation, pathfinding, line of sight algorithms.. And to top it off, the whole thing is portable: Rather than build off a terminal-based library, it uses the SDL library. So while programs now have to run in a non-terminal, there’s very little messing about with getting games working on multiple platforms AND with full colour!

So it’s a no-brainer, right? Using this library, I’d be able to get to work on the meat of my game idea immediately! Maybe this wouldn’t be such an impossible task after all.

Of course, I’d already gotten an @ symbol walking around.. And it would be a shame to waste ‘all that work’.. Long story short, I made the questionable decision to continue with my own ‘from scratch’ engine. This was probably due to a mix of hubris and ignorance. Sufficed to say, I didn’t finish in seven days. It turns out that unless you’ve implemented things like path-finding and field of view before, they can be more than a little time consuming!

So the dream of a roguelike in seven days was dead on arrival. But this wouldn’t kill the dream of a roguelike in totality. Over the past month I’ve continued to work on my little game, and over the next few posts I’d like to go through some of the basics of roguelike development using my repository’s history to guide us. It will definitely not be a guide on the proper usage of source control (one branch for life!), but I think maybe it’ll be useful for me to solidify the process and maybe useful for people reading if they want some insights on how a newbie might write a roguelike.