Bidden 7DRL code review

While things are still fresh in my mind I want to review the somewhat bizarre code design I came up with during the 7DRL, if only to mark a way point to look back on as things change.

Here’s the code (Leo makes exporting this outline easy):

+ Code
	- TODO
	+ @thin bidden.py
		- << docstring >>
		- << import >>
		- << constants >>
		+ class Game
			- __init__
			- load
			- save
			- create_gid
			- register_gid
		+ class Event
			- __init__
			- broadcast
			- register_component
		+ class Control
			- __init__
			- action
			- switch_view
			- quit
		+ Controllers
			+ class Menu
				- __init__
				+ new_game
					- << make player >>
					- << make binding >>
					+ << load from .bid >>
						- << test >>
					- << put things in stage >>
			+ class Intro
				- __init__
			+ class Play
				- __init__
				- move
				- use_boon
				- wait
				- help
			+ class Coda
				- __init__
		+ class Component
			- __init__
			- __lt__
			- __gt__
		+ Components
			+ class Hunter
				- __init__
			+ class Container
				- __init__
			+ class Model
				- __init__
				- on_death
			+ class XY
				- __init__
				- on_move
			+ class Stage
				- __init__
				- create
			+ class Collision
				- __init__
				- on_move
			+ class Bump
				- __init__
				- on_move
			+ class Title
				- __init__
				- create
				- make
			+ class Hunted
				- __init__
				- on_death
			+ class Wounds
				- __init__
			+ Boons
				+ class Boon
					- __init__
				+ class Strike
					- __init__
					- on_boon
			+ AIs
				+ class AI
					- __init__
				+ class Zombie
					- __init__
					- decide
		+ class Prototype
			- __init__
		+ class UI
			- init
			+ class Pane
				- __init__
				- fade_in
			+ class Animation
				- __init__
			- input
			- view
			- menu_draw
			- intro_draw
			- play_draw
			- coda_draw
			- quit_draw
			- play_help_draw
		+ class Keyboard
			- __init__
			- check
		- << run game >>

Let’s look at the concise version:

+ @thin bidden.py
	- << docstring >>
	- << import >>
	- << constants >>
	+ class Game
	+ class Event
	+ class Control
	+ Controllers
	+ class Component
	+ Components
	+ class Prototype
	+ class UI
	+ class Keyboard
	- << run game >>

Going over things from the main loop:

    while not libtcod.console_is_window_closed() and not game.control == 'quit':
        keys.check()

        if game.control == 'play':
            for item in game.gids:
                thing = game.gids[item]
                for k in thing.__dict__:
                    c = thing.__dict__[k]
                    if isinstance(c, AI):
                        c.decide()
        ui.view()

The keys instance of Keyboard waits for a key press. If it gets an ‘x’ or the control key it waits for another key press. This is my somewhat hacked way to get key + direction key combinations. It checks if the key is in the configuration for the game, and sends a message (such as ‘up’ or ‘enter’) to the method UI.input.

That method passes along the input to the current game.control, including it in a dict and adding the enactor game ID (in the case of the player, 0). There’s a control basically for each state of the game (main menu, in-play, etcetera). I tried to make all arguments passed around in the form of a dictionary, using the same key names for like types of data.

Each control inherits an action method from the Control parent. This method checks the input message against an actions dictionary, and if the message is a key, it runs the corresponding value as a method on the control instance (for example, I had the ‘enter’ key mapped to a method to advance the state of the game arbitrarily, so I could restart the game without exiting/restarting the Python file). Individual control instances may map the same message to different methods of course.

This method gets the data in the dict described above. A good example is the move method (in the ‘play’ control instance, mapped to the ‘up/down/right/left’ message which is mapped to the arrow keys in the keys instance):

def move(self, data):
    self.directions = {
                        'up' : {'x':0, 'y':-1},
                        'down' : {'x':0, 'y':1},
                        'right' : {'x':1, 'y':0},
                        'left' : {'x':-1, 'y':0},
                        }

    data['event'] = 'on_move'
    data['outcome'] = 'continue'
    data.update(self.directions[data['input']])


    event.broadcast(data)

So this method basically adds data to the dictionary, and then sends it to the instance of Event.

Event , in the context of the 7DRL, probably was a big mistake…going into the week I had vague thoughts of trying out a message passing/broadcasting scheme for the first time, and this was the result. I spent a lot of time figuring this out.

The Event.broadcast method takes the data and sends it along to anything registered to listen for the event included, in this case ‘on_move’. So, when the player presses the up key, it’s not directly moving the player-character, but telling the game to announce that the player-character has the intention of moving…I know, I know.

To digress for a moment, each thing in the game is an instance of Prototype, which basically is a collection of Components. A component is a narrowly-defined piece of functionality, that registers itself with the event instance upon creation. For example, if the component has a method called on_move, the event instance will tell the component when an on_move event occurs.

There is a component called XY that holds the in-game position of a thing, and its on_move looks like this:

def on_move(self, data):
    if (data['enactor'] == self.gid) and (data['outcome'] == 'continue'):
        self.x += data['x']
        self.y += data['y'] 
    return data

When the player presses the arrow key and the event instance broadcasts ‘on_move’, the XY component of the player-character will take that data — if the ‘outcome’ is ‘continue’ — modify its x and y position, then pass the data along back to the broadcaster.

The reason for ‘outcome’, and passing the data back to the broadcaster, is that other components handle things like collision with walls and bumping into other things. They’ll have an on_move method, and it receives the data broadcast. For example, here is collision:

def on_move(self, data):
    enactor = game.gids[data['enactor']]
    dx = data['x']
    dy = data['y']
    xx = enactor.XY.x + dx
    yy = enactor.XY.y + dy 

    stage = game.gids[game.stage].Stage 

    if not stage.cells[yy][xx]['kind'] == 'floor':
        data['outcome'] = 'failure'
        return data

    return data

Now obviously the question came up — in which order is the event broadcast? In other words, if the XY of the player happened to get the event before collision, the player would move before the collision check even happened.

To make this work I gave each component a priority from 1 to 1000, with a default of 1000. The collision component’s priority is 100, and the XY priority is 1000. Then before event broadcasts, it sorts the list of components registered for the event. This is easy to do with Python by writing a method like so on the Component parent.

def __lt__(self, other):
    if self.priority < other.priority:
        return True
    else:
        return False

If the class defines __lt__, then calling sort() on a list of instances of that class will sort it appropriately.

That’s all there is to the event broadcasting, basically a chain of event filters that modify both the event data and game world data as the event propagates through the listening components. I suppose I’ll find out what problems this causes down the road!

NPC actions are basically the same, the only difference being in how they’re initiated. Going back to the game loop:

        if game.control == 'play':
            for item in game.gids:
                thing = game.gids[item]
                for k in thing.__dict__:
                    c = thing.__dict__[k]
                    if isinstance(c, AI):
                        c.decide()

This runs through each component, and if it’s an AI, it runs the decide method. This is all I have for decide at the moment (in the Zombie component):

def decide(self):
    choice = random.choice(self.actions)

    data = {'enactor' : self.gid, 'input' : choice}
    control = game.controllers[game.control]
    control.action(data)

Where its self.actions is just this list — [‘up’, ‘down’, ‘right’, ‘left’, ‘wait’].

This just gave me an idea for attaching AI components to the player…anyway.

After the player does their thing and the AI runs, the UI instance runs its view method, which looks like this:

def view(self):
    libtcod.console_clear(0)

    for item in self.views:
            func = getattr(self, game.control + item)
            func()

    libtcod.console_flush()

So, clear the console, and go through its list of views and run the function — for example, there’s one called ‘play_draw’, where ‘play’ is the game.control and the item is ‘_draw’.

Other conceivable ones might be ‘_log’ to capture the game state, or perhaps ‘_write’ to turn this into IF ;D. In any case, yet another example of severe over-engineering…

These methods are fairly straightforward…I did one weird thing where certain events could create ‘animations’, components that last for just one game loop and get added to the list of game objects to be drawn. That’s something I need to redo (I want to have multi-frame animations possible between each player turn). My current idea here is to make drawing a series of frames, where normally there’s just one frame as I have now in play_draw, but with the potential for adding frames to make the animations.

So that’s it. If I expand this I can see the component system getting really massive…on the other hand I don’t know that any game wouldn’t have a similar situation once you add a lot of stuff. I can derive children from component classes in some cases (for example with AI) so that might keep things more organized. I like the separation of input/views/logic. Right now the world state is woven into the game logic quite a bit, but I’m of mixed feelings of trying to make a hard separation between the two. In this application it may not be worth it.

Any comments or criticism is particularly welcome at this point, so feel free.

Advertisements

1 comment so far

  1. Hemebond on

    Components? Events? Sounds like a great setup to me. I’ve found this series of posts very interesting to read.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: