Open johnameyer opened 5 months ago
Likely this can hook in with #91 wherein we would like to clearly establish which events are acceptable in response.
We should consider fixing up the "standard states" as a part of this
Potential euchre example:
/ TODO if we are changing handling to be a different mode, do we need to expose waiting / players controllers?
const stateMachine = sequence([
{ // TODO how does this outer level look?
id: 'start-game',
run: controllers => { /* start game - state should already be clean so no-op */ },
},
loop({
id: 'round',
beforeEach: controllers => { /* clean up state */ },
afterAll: controllers => { /* update scores and send messages */ },
condition: controllers => /* no player has yet won */,
run: sequence([
handle({
id: 'order-up', // these can probably be left off for the handler arg if we handle collisions
mode: 'round-robin',
// TODO should we have different functions for different modes or will discriminated union work fine?
handler: 'orderUp',
startingPosition: controllers => controllers.deck.dealer,
beforeAll: controllers => { /* message about flipped card */ },
// TODO ponder about semantics between outer beforeEach and inner beforeAll
afterEach: controllers => { /* message (incl. going along) */ },
breakingIf: controllers => controllers.euchre.bidder !== undefined,
afterBreak: handle({ // TODO better terminology
mode: 'single',
handler: 'dealerDiscard',
position: controllers => controllers.deck.dealer,
after: controllers => { /* take the discard and give new card */ }, // plays into #116
}),
otherwise: handle({ // TODO better terminology
mode: 'round-robin',
handler: 'nameTrump',
startingPosition: controllers => controllers.deck.dealer,
breakingIf: controllers => controllers.euchre.bidder !== undefined,
afterBreak: controllers => { /* message (incl. going along) */ }
// TODO handle misdeal / no trump selected - likely need to wrap in a loop
}),
}),
loop({
id: 'trick',
beforeEach: controllers => { /* send messages, reset states */ },
afterEach: controllers => { /* message, update leader, update count */ },
condition: controllers => /* players have cards */,
run: handle({
mode: 'round-robin',
handler: 'turn',
startingPosition: controllers => controllers.trick.leader,
condition: controllers => isPlayingThisRound(controllers.turn.get(), controllers)
after: controllers => { /* send message (setting state currently handled by event-handler merge */ }
})
}),
]),
}),
{
id: 'end-game',
run: () => { /* send game over message */ },
},
]);
Some other questions:
As a part of the build / test, we should consider generating a state diagram (mermaid?).
We should try to have the transitions named as a result
Currently our state machines exist as a flat structure
To enable further composition and re-use, we must enable nesting of logic. For example, every startXyz, waitXyz, and handleXyz could all exist as a sub-machine (e.g. WaitStateTransitions), and loops might be their own sub-machine as well (though a loop is just a specialized general state). These constructs would be parameterized such that the specific behavior could be customized (or children states enabled). For the WaitStateTransitions, it would take in before and after handlers, and the event to call on which handlers. Likely the declaration and selection of the next state would remain in effect with the parent.
An open question is if the state controller should still be available for direct access in other controllers or in these handler functions.
Naturally, strong typing and proper serialization (meaning avoiding running handlers multiple times) is ideal. Likely the state variable would be represented by an array of states being pushed on.
Long term, we might want to consider supporting mermaid as an output for validation of flow.