-
Notifications
You must be signed in to change notification settings - Fork 19
Game Engine
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
User
sends a message to the backend via their websocket. It is handled inhandlers.py
and 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_loop
intablebeat.py
. This passes the message down to thecontrollers.py
level. - The
PokerController
'sdispatch
has involves a number of steps:- The message is coerced into an
Action
(anEnum
defined inconstants.py
), and passed on toplayer_dispatch
. -
player_dispatch
validates that the action was available to thePlayer
, and then calls a method of the same name with whatever arguments were passed in. AllAction
methods are pure functions which produce a series ofEvent
tuples, which include a(subj, event, kwargs)
. IfAction
tuples define the external interface of the poker engine,Event
tuples define the interface of the underlying models. - The
Action
and any accompanying**kwargs
are written to theHandHistoryLog
. - The
Event
list produced by theAction
method is passed intointernal_dispatch
, which has its own sub-steps:- Each
subj
is 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_TO
event on thePlayer
model invokes itson_raise_to(amt)
function. - Each
Event
is also passed to a list ofsubscriber
classes, 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 createsTransfer
objects to represent the movement of chips, and theLogSubscriber
, which createsHandHistoryEvent
objects in the database to record allEvents
for later use in aReplayer
. For a complete list, see the__init__
function.
- Each
- Once
internal_dispatch
completes,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_dispatch
a number of times itself. - Once all of these changes have been applied, the
controller
callscommit()
, 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 thesubscriber
s and theaccessor
, and passes that state to allPlayer
anduser
(observer) sockets in a message oftype
'UPDATE_GAMESTATE'
.
- The message is coerced into an
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 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 Message
s, 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 Action
s, 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 Model
s, and additionally, things that can trigger side-effects in subscribers
, all of which use SIDE_EFFECT_SUBJ
.
In the game engine, models are never written to directly, but instead mutated through "atomic" Event
s. Subscribers
can manage some model
s directly, such as Transfer
s, HandHistoryEvent
s, or Badge
s, 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()
.
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.
All the the Animation
s 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 AnimationEvent
s to not map one-to-one with Event
s, there is a somewhat complicated reduction process that has to take place inside of animations.py
.
Each 'UPDATE_GAMESATE'
message gets a group of Animation
s, 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.
The DBLog
, defined in handhistory.py
, is used by a LogSubscriber
to log all Event
s 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 Subscriber
s 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.
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.