Homunculus Devlog 2018-05-19

Tags:
By Max Woerner Chase

In this post:


I switched most of the dict return values to a custom type that emulates enough of the dict interface to get by. These new "Events" each contain a single type of value, which makes them easily amenable to changing to some other form of layout later. I haven't touched the input processing code, because I'm not exactly sure how it should look.

Anyway, possible directions to take this include moving the specification of "how an event behaves" out of the Game class. I'm not exactly sure what the events of The Distant Future Of Several Weeks From Now should look like. Perhaps the events are module-level constants or returned from a factory function, and the actual structure is just a pretty-printing wrapper around a generator function.

Probably, I want to next go for turning them into constants and factories, and I can worry about what's in them later.

Thinking about the input handling, I think what's confusing me is that there's this weird inter-weaving of knowing-about-state, wherein the GameState controls both which events can be generated, and what those events mean? And so sometimes ideas with different semantics are expressed using the same variable, because that's more compact in the main method? But I want to offload the processing into the Event itself potentially, so I'm willing to take the hit for refactoring purposes.

Here's an interesting idea that will break my save game (no great loss), but might really clear up the structure of things: instead of a game object that includes all state including the state enum, instead make each enum state its own class, where its instance variables represent "all of the required state", and state transitions are handled by returning the new state object. This would be kind of a big change, and I'm not sure how to get to it gradually, which is really important, but it seems like a worthy goal, so far as getting rid of high-level if-statements.

In any case, step one here is to use Events in the context of input handling, then add in more variables to deal with the fact that sometimes a message can mean different things. Formalizing that idea will let me get rid of some state checks in the take_player_turn method, which will let me move towards not varying behavior based on the state, which should make it easier to clean things up. Yeah. Separate out the names, rewrite to deal with the separated names, rewrite the blocks into their own generators, and see where I want to go from there.

It seems to me that I should be able to have "all details of behavior" collected into distinct classes, which all rely on other modules for stuff like "interpreting input events". So, such a GameState class would have all the details of how to render and deal with state, and would basically be a unified "mode".

I've got a few hours between getting home later, and publishing this post. Let's see what I can do.

Ah, sudden inspiration first: I don't want to put everything in the GameState, just the data that is specific to certain GameStates. So, stuff like the targeting item or the previous state goes in there, but the map doesn't. This might shake out a few different ways in practice, but it's a good thing to have in mind.

Here's what I got so far. I have different names for identical events, in the assumption that I'll later diverge them so I can tease the logic apart.

  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
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# To test: is thankfully self-contained.

from . import events
from .game_states import GameStates, Visitor

EVENT_UP = events.Event('move', (0, -1))
EVENT_DOWN = events.Event('move', (0, 1))
EVENT_LEFT = events.Event('move', (-1, 0))
EVENT_RIGHT = events.Event('move', (1, 0))
EVENT_PICKUP = events.Event('pickup', True)
EVENT_SHOW_INVENTORY = events.Event('show_inventory', True)
EVENT_DROP_INVENTORY = events.Event('drop_inventory', True)

EVENT_SAVE_AND_QUIT = events.Event('exit', True)

EVENT_CANCEL_TARGETING = events.Event('exit', True)

EVENT_SHOW_DEAD_INVENTORY = events.Event('show_inventory', True)
EVENT_QUIT = events.Event('exit', True)

EVENT_EXIT_INVENTORY = events.Event('exit', True)

EVENT_FULLSCREEN = events.Event('fullscreen', True)


def inventory_index(index):
    return events.Event('inventory_index', index)


def left_click(cell):
    return events.Event('left_click', cell)


def right_click(cell):
    return events.Event('right_click', cell)


@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 EVENT_UP
    elif user_input.key == 'DOWN':
        return EVENT_DOWN
    elif user_input.key == 'LEFT':
        return EVENT_LEFT
    elif user_input.key == 'RIGHT':
        return EVENT_RIGHT
    elif key_char == 'g':
        return EVENT_PICKUP
    elif key_char == 'i':
        return EVENT_SHOW_INVENTORY
    elif key_char == 'd':
        return EVENT_DROP_INVENTORY

    if user_input.key == 'ENTER' and user_input.alt:
        return EVENT_FULLSCREEN
    elif user_input.key == 'ESCAPE':
        return EVENT_SAVE_AND_QUIT

    return {}


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

    return {}


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

    if key_char == 'i':
        return EVENT_SHOW_DEAD_INVENTORY

    if user_input.key == 'ENTER' and user_input.alt:
        return EVENT_FULLSCREEN
    elif user_input.key == 'ESCAPE':
        return EVENT_QUIT

    return {}


# This stuff maybe wants to be split up somehow?
@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 EVENT_FULLSCREEN
    elif user_input.key == 'ESCAPE':
        return EVENT_EXIT_INVENTORY

    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 {}

The big thing I'm trying to work out right now is, how do I want to handle the fact that I've got two distinct sources of input here? I was kind of considering using itertools.zip_longest, or something. But switching the key interpretation to generators seems like the wrong move. It probably ought to be done with data structures of some kind, like I was saying earlier.

All sorts of things I want to be doing with this...

One thing I should note is that I do have the nagging sense that I'm missing something, and potentially setting myself up to over-extend an idea or something. Take the idea of the "item_dropped" event, for example. The event is emitted from within the Inventory component, and intepreted by a generator method on the Game class. If I move the logic into the "Event" instances, then that means that the Inventory component must somehow be able to access the logic, but the logic centers around changes to the Game and GameState, so the logic can't reside within the Inventory module. (On the other hand, the current implementation of Inventory knows about the Map and Entity systems to a degree I'm not quite comfortable with.) So, if the semantics of "item_dropped" focus on stuff in the Game object, but the Inventory cannot (should not, will not) see the Game object, does that mean that there needs to be a module that just contains functions that look like, but are not, methods of the game object? That doesn't seem right. Ideally, and I'm coming back to this so I think there might be something to it, there wouldn't be too much behavior embedded in the Events; instead, they'd key into some kind of data structure that converts input into a method call. Maybe this would make more sense if the Inventory did some translation. If, instead of drop_item emitting "item_dropped", it emitted something like "place: entity, coordinates", and everything that ends the player's turn perhaps emits some kind of "turn end" event. Basically, instead of implementing drop_item as "do some bookkeeping and emit the item_dropped event", facyor it into "determine where the item will be placed, remove it from the list, and tell the game to place it at the coordinates".


Next week, I'm not quite sure. Maybe I'll re-do the save format so it's properly contained within a versioned object. After that, maybe it's a good time to convert the GameStates into classes that contain state-specific data and keep it out of the Game object.