Event System
The Open Mafia Engine is primarily Event-based, see Event-driven Architecture on Wikipedia. The Event/Action system is what drives all state change in the game.
Events, Actions and Subscribers
An Event typically represents some change in the game's state.
An Action is essentially a delayed function call.
Each Action object references its source (the entity that created the action),
some parameters, priority and whether it's cancelled.
A Subscriber
object subscribes to particular types of events through event handlers.
An EventHandler
(usually a method wrapped in @handler
or @handles)
takes particular types of Events and returns zero or more Actions (the response).
Event and Action Logic
Handling Events
Let's go through the event handling logic.
- The
gamebegins toprocess_event(e)for some evente. - The
gamebroadcasts to all event handlers fore. - Each handler responds to the event, returning
Noneor a list ofActions. - Each
Actionis added to thegame.action_queue - Depending on the phase, the
ActionQueueis proccessed either immediately or at the end of the phase.
Essentially, each EventHandler a delayed Action in response to the Event.
Action Queues
An ActionQueue
is just that - a queue of delayed actions.
Actions are sorted by their priority (higher priority goes first), then by the
order they were recieved. This means the action order should be deterministic.
The Game object contains the main queue, but more can be created temporarily
as part of the branching structure.
Each ActionQueue also holds the history of executed actions, for reference
(this may be needed by some other object).
Processing the ActionQueue
ActionQueue.process_all(game) runs through all enqueued actions one by one.
For each action:
- An
EPreAction(pre-actionEvent) is created. - This
EPreActionis broadcast to all relevantSubscribers, who createActions in response. - A new, secondary
ActionQueueis formed using these actions, and that queue is run through. - If the current
Actionwas cancelled, - Otherwise,
action.doit(game)is run (i. e. the action occurs). - An
EPostAction(post-actionEvent) is created and broadcast. - The responses to the
EPostActionform their ownActionQueuethat is also run through. - The history of all sub-queues is added to the current history, along with the action itself.
In reality, the above happens for all actions of the same priority, as a batch. It's much easier to understand it as single actions, though:
- You: "I'm about to do ACTION, any objections?"
- Sub A: "No, wait, I need to do PRE-RESPONSE first."
- (PRE-RESPONSE occurs)
- (action occurs, assuming PRE-RESPONSE didn't cancel it)
- You: "OK, guys, I did ACTION."
- Sub B: "You did? Okay, let me POST-PRESPONSE."
- (POST-RESPONSE occurs)
Why make it this complicated?
This sort of event framework is common in many user interface applications, though usually it's done in a top-down manner, with callbacks depending on changes in the state.
In the Open Mafia Engine, you can think of EventHandlers as being the "callbacks",
and the Actions as their effects.
However, in order to allow "countering" or otherwise modifying actions (for example, roleblocking and jailing, protection, passive abilities, and much more), these actions themselves need to create their own events, and be able to be intercepted.
This flexibility is very similar to how action resolution works the game Magic: The Gathering, where there is an action stack caused by activated and triggered abilities. In fact, MTG had a large influence on the development of the Open Mafia Engine.
Creating a simple action is fairly simple, since all this response logic is part
of the Engine itself. More involved actions can require multiple types (e. g. an
Action, Ability and some sort of watcher), but another framework would not be
this flexible. Events can make debugging interactions fairly difficult, though.