Game Engine - Monadical-SAS/oddslingers.poker GitHub Wiki
Dispatch cycle -- overview
At each level of message processing below the handlers, all state change is handled by a dispatch function. The flow of information is uni-directional and heirarchical--that is, a system can never cause state changes at the level above it. Information starts as an Action, and gets broken down into Events before finally reaching the Models. After all the effects of an Action have been committed to the database, an UPDATE_GAMESTATE message is broadcast back out to any observers or Players. Below is a more detailed account of that process.
- A
Usersends a message to the backend via their websocket. It is handled inhandlers.pyand added to the heartbeat queue. The message consists of a user (which later maps to aPlayer, atype(which later maps toAction), and some**kwargs. - The message is eventually dequeued by a poker game's process via the
tablebeat_loopintablebeat.py. This passes the message down to thecontrollers.pylevel. - The
PokerController'sdispatchhas involves a number of steps:- The message is coerced into an
Action(anEnumdefined inconstants.py), and passed on toplayer_dispatch. player_dispatchvalidates that the action was available to thePlayer, and then calls a method of the same name with whatever arguments were passed in. AllActionmethods are pure functions which produce a series ofEventtuples, which include a(subj, event, kwargs). IfActiontuples define the external interface of the poker engine,Eventtuples define the interface of the underlying models.- The
Actionand any accompanying**kwargsare written to theHandHistoryLog. - The
Eventlist produced by theActionmethod is passed intointernal_dispatch, which has its own sub-steps:- Each
subjis either a database models (defined inmodels.py), or theSIDE_EFFECT_SUBJ(which means it triggers something in asubscriber--more on that later). The model objects define the way in which they will be modified by a given event with a member method of the same name, except starting with the stringon_. For example, theRAISE_TOevent on thePlayermodel invokes itson_raise_to(amt)function. - Each
Eventis also passed to a list ofsubscriberclasses, which are pure listeners. Among these is theAnimationSubscriber, which defines the way a set of backend changes will end up being displayed in the front-end. Others include theBankerSubscriber, which createsTransferobjects to represent the movement of chips, and theLogSubscriber, which createsHandHistoryEventobjects in the database to record allEventsfor later use in aReplayer. For a complete list, see the__init__function.
- Each
- Once
internal_dispatchcompletes,step()is called, which makes any new changes to the game (e.g. starting a new hand, dealing new cards, etc). Step may also callinternal_dispatcha number of times itself. - Once all of these changes have been applied, the
controllercallscommit(), which attempts a database transaction in all poker models and any side effects caused bysubscribers. If it fails, all change attempts are dropped. - Finally,
commit()callsbroadcast_to_sockets()as defined inmegaphone.py, which accumulates updates from all thesubscribers and theaccessor, and passes that state to allPlayeranduser(observer) sockets in a message oftype'UPDATE_GAMESTATE'.
- The message is coerced into an
Controller and Accessor
The Controller contains any function that deals directly with Event or Action objects. Also, any function that isn't directly called by step() should be pure-functional. That is, setup_hand() makes several state-mutating calls to internal_dispatch, but each of the functions it calls (e.g. sit_in_pending_and_move_blinds(), post_and_deal()) merely return lists of Event-tuples.
The Accessor is the interface between the Controller and the Models. Any function that aggregates or transforms models belongs in the Accessor. Examples include active_players(), which returns the players seated at a table who are actively involved in play. All of the Accessor's functions are read-only.
Messages, Actions and Events
Messages are payloads that come in from the front-end, or go back out. They always include a sender, type, and kwargs. They are currently inconsistently named in the codebase.
Actions are effectively a subset of Messages, and refer specifically to the Controller API for a Player. Currently, the full (Player, Action, kwargs) tuple is often denoted action as a parameter, even though the Action type is specifically the enum that maps to a controller function.
Events take the same form as Actions, except the first field is called subj in most places, and isn't necessarily a Player--it can be a Table or SIDE_EFFECT_SUBJ. The Event enum defines the API of the underlying Models, and additionally, things that can trigger side-effects in subscribers, all of which use SIDE_EFFECT_SUBJ.
Models
In the game engine, models are never written to directly, but instead mutated through "atomic" Events. Subscribers can manage some models directly, such as Transfers, HandHistoryEvents, or Badges, in which case the list of objects that have changed are accumulated and committed to the database in the same transaction as any game-engine events when the Controller calls commit().
Subscribers
All subscribers have a dispatch() function, which is called every time an Event passes through the Controller's internal_dispatch. Additionally, they all have a commit(), which is called by the controller inside of a Transacaction. Finally, each has an updates_for_broadcast() function which takes a player and returns any information that should be added to the 'UPDATE_GAMESTATE' Message at the end of a dispatch cycle.
Animations
All the the Animations that are played in the front-end are specified in the back-end, by the AnimationSubscriber. They are defined by the AnimationEvent enum in constants.py. Because AnimationEvents to not map one-to-one with Events, there is a somewhat complicated reduction process that has to take place inside of animations.py.
Each 'UPDATE_GAMESATE' message gets a group of Animations, which begin with a 'SNAPTO' and also end with a 'SNAPTO', which update the frontend state completely, and provide a fallback in case of frontend errors or packet loss.
Logging
The DBLog, defined in handhistory.py, is used by a LogSubscriber to log all Events that pass through a Controller's internal dispatch. Each Event-tuple is logged to database using the HandHistoryEvent model.
However, the logging system is unlike other Subscribers in that it also receives every Action as well, in a call to write_action() inside of player_dispatch() on the Controller. These are stored with the HandHistoryAction model.
Both HandHistoryEvent and HandHistoryAction point to a parent, HandHistory, one of which exists for each individual hand played at a table. A new HandHistory object is created each time the NEW_HAND Event is dispatched, and a serialized snapshot of the PokerTable and Player objects are added in JSON format to the database.
This system makes it possible to recreate the state at any point in the history of any table, using the Replayer classes in replayer.py. These are especially useful for debugging.
Bug Reports
The Controller has a report_bug() method which calls its own dump_for_test(). This creates a JSON-serialized set of HandHistory objects, which can be loaded into a Replayer for debugging. Un-comment the CheckWhatTest at the bottom of test_hands.py and run ./manage.py test poker.tests.test_hands.CheckWhatTest to introspect state at a broken point in the history.