lphillips / SecretHitlerDiscord

A discord bot for the famous board game Secret Hitler
Other
1 stars 0 forks source link

Game refactor for testability #51

Closed lphillips closed 3 years ago

lphillips commented 3 years ago

This is very much a work-in-progress of a refactor of the game logic to improve testability. The aim of this refactor is to:

I've focused on the code around voting so far, since that is one of the main inputs that drives state changes currently. This means updating the Game.vote(player_id, vote) method and on_reaction_add(reaction, user) function, with some changes radiating out from there.

In order to centralize state transitions and have a common flow that is executed when state changes, I've introduced Game.__transition_to_state(state) to isolate assignments to Game.state and also add methods that can tell an observer when the game has entered a new state. The idea here is that the app module sends inputs to the Game via methods like vote, execute_player, nominate, etc, and that can trigger state changes. When the state has changed, the app is notified via a method like on_legislative_presidential, so it can created the appropriate embed and send it to Discord.

I've run into one hiccup here though. Messaging with Discord is asynchronous, so I can't call from the synchronous state management code in Game to the app module to update the UI. I'm very new to async programming in Python, but I'm currently thinking of setting up some async dispatch to the embed creation in response to the game callback. Something like this:

# In app.py
def on_legislative_presidential(game):
   # dispatch_to_async() is a stand-in for whatever asyncio method I can call to get it to run
   # an async method and not wait for the result. Also need to send the game through to
   # async method, however that works
    asyncio.dispatch_to_async(send_embed_for_legislative_presidential)

async def send_embed_for_legislative_presidential(game):
    # use discord API to create and send an embed
    game.finish_legislative_presidential() # this call triggers the game to move to the next state
bsmccain commented 3 years ago

I think asyncio.create_task() is what you want to make the async call. That's what I did in the emit() method on DiscordChannelHandler. Still reviewing, wanted to make this comment before I forgot.

lphillips commented 3 years ago

asyncio.create_task() seems to do the trick. I set up the code for creating a game and reacting to the game_start state change and everything happened as expected.

lphillips commented 3 years ago

I think this is in a state where it is ready for further review and possible merging. Here's a summary of the current state:

And a caveat:

lphillips commented 3 years ago

I like the direction this is going, and approve the current implementation. I think if I were doing this from scratch, I'd probably create state objects that had methods for handling voting, transitions, etc. I'm not certain it's worth the effort in this case, but the large if/else trees seem unwieldy. But they're also implemented and functioning currently, so just rearranging is probably fine.

Yeah, I considered pulling in a state machine library and building an entirely new game engine off of that, but ultimately decided I could achieve good results by moving some of this code around and reducing duplication. A lot of the deeply nested if-statements are gone now, so it is easier to read and reason about.

lphillips commented 3 years ago

I addressed the smaller items from your comments. Here's what I plan to do in my next PR: