Homunculus Devlog 2018-05-19
In this post:
- I talk about, and then somewhat rewrite, the input-handling code.
- I then talk about how I'm not totally sure about my plan for rewriting things, then have an idea for clarifying the meaning of the various bits of code.
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.