Homunculus Devlog 2018-05-12

By Max Woerner Chase

In this post:


Looking over the code that I've yet to collapse down with abstractions, I realized that I didn't like the basic state machine setup. My current plan is to try replacing the if statements with functions and lookup tables.

Although, the way this is done now, the state machine is the overall run method, since the transition logic is just all over the place.

I'll try implementing this stuff as a visitor.

Here's how game_states.py looks after putting in a visitor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# To test: just import.

from enum import Enum, auto


class Visitor:

    def __init__(self, default=None):
        self.state_methods = {}
        self.default = default or self._default

    def _default(self, state, *args, **kwargs):
        raise NotImplementedError(f'Not implemented for {state}')

    def bind(self, state):
        def inner_bind(function):
            self.state_methods[state] = function
            return function
        return inner_bind

    def visit(self, state, *args, **kwargs):
        return self.state_methods.get(state, self.default)(
            state, *args, **kwargs)


class GameStates(Enum):
    PLAYERS_TURN = auto()
    ENEMY_TURN = auto()
    PLAYER_DEAD = auto()
    SHOW_INVENTORY = auto()
    DROP_INVENTORY = auto()
    TARGETING = auto()

The Visitor class is the new thing. Because I'm not writing Java, it's a single class that gets instantiated with different behaviors, and handles the behaviors in a mapping on the instance. It should probably get moved to its own module, because it's not really coupled to the idea of using states specifically. In fact, one module that got rewritten uses it in several contexts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# To test: is thankfully self-contained.

from .game_states import GameStates, Visitor


@Visitor
def state_visitor(state, user_input):
    return {}


@Visitor
def input_visitor(user_input, state):
    return state_visitor.visit(state, user_input)


@input_visitor.bind(None)
def no_input(user_input, state):
    return {}


@state_visitor.bind(GameStates.PLAYERS_TURN)
def handle_player_turn_keys(_, user_input):
    key_char = user_input.char
    if user_input.key == 'UP':
        return {'move': (0, -1)}
    elif user_input.key == 'DOWN':
        return {'move': (0, 1)}
    elif user_input.key == 'LEFT':
        return {'move': (-1, 0)}
    elif user_input.key == 'RIGHT':
        return {'move': (1, 0)}
    elif key_char == 'g':
        return {'pickup': True}
    elif key_char == 'i':
        return {'show_inventory': True}
    elif key_char == 'd':
        return {'drop_inventory': True}

    if user_input.key == 'ENTER' and user_input.alt:
        return {'fullscreen': True}
    elif user_input.key == 'ESCAPE':
        return {'exit': True}

    return {}


@state_visitor.bind(GameStates.TARGETING)
def handle_targeting_keys(_, user_input):
    if user_input.key == 'ESCAPE':
        return {'exit': True}

    return {}


@state_visitor.bind(GameStates.PLAYER_DEAD)
def handle_player_dead_keys(_, user_input):
    key_char = user_input.char

    if key_char == 'i':
        return {'show_inventory': True}

    if user_input.key == 'ENTER' and user_input.alt:
        return {'fullscreen': True}
    elif user_input.key == 'ESCAPE':
        return {'exit': True}

    return {}


@state_visitor.bind(GameStates.DROP_INVENTORY)
@state_visitor.bind(GameStates.SHOW_INVENTORY)
def handle_inventory_keys(_, user_input):
    if not user_input.char:
        return {}

    index = ord(user_input.char) - ord('a')

    if index >= 0:
        return {'inventory_index': index}

    if user_input.key == 'ENTER' and user_input.alt:
        return {'fullscreen': True}
    elif user_input.key == 'ESCAPE':
        return {'exit': True}

    return {}


def handle_mouse(mouse_event):
    if mouse_event:
        if mouse_event.button == 'LEFT':
            return {'left_click': mouse_event.cell}
        elif mouse_event.button == 'RIGHT':
            return {'right_click': mouse_event.cell}

    return {}

I'm not done rewriting this. My goal with this module is to turn the whole thing into, like, a static data-structure that just gets consumed by a simple function. Just get rid of as many if blocks as possible. I don't like changing code after I've copied it into a post, so I'll probably do this after I publish, but I ought to factor out all that dict stuff into a common function, so I can find some other way to represent things later, without needing a painful search-and-replace, and also start looking around for magic string stuff to turn into constants.

I just realized something interesting: the different parts of the "result" dictionaries don't actually need to be bundled together, if they're being yielded, so they can be converted into multiple statements. And that frees me up to convert them into some other data type.

I don't know when I'm going to be fully satisfied with this, but I'm starting to think that when I am, I should try to put together my own tutorial, since the code is going to look so different when all is said and done.


Next week, I demux the engine events, and convert them to custom classes.