For the past few months I've been messing around with implementing my own version of a well-known board game. After much experimentation I've arrived on a few key points that describe my system:
The game state is centralized and serializable. The server can be interrupted at any time, but simply by pulling the state from storage, a player could resume their game exactly where it stopped
Game rules are simply a function of the state
(state, ...args) => newStateClients interact and modify the state through actions, described by a rule which modifies the state accordingly (see point above), and a values function
(state, player) => args[], which, given the current state and the player, returns the valid values (if any) for that particular type of action. This is used to inform the client of their valid actions and also as a validator for incoming actionsThe player chooses one of the possible actions, sends it to the server in the form
{type, args}, which validates the action against the possible values for that particular action type and executes the corresponding rule, returning the new state back to the playerSome actions might lead to a state where a player is required to perform other actions as part of it, disallowing all other actions which could otherwise be made. This sequence has to be able to be interrupted and resumed at any time, like in a case of server outage - this could be implemented as separate actions which set some kind of "flag" that the subsequent actions could look for in their values function, but would make it kind of verbose, because everything had to be declared explicitly:
actionA1 = (state, ...args) => { //some code here... return { ...state, waitFor: actionA2 } } actionA2 = { handler: (state, ...args) => { //actionA2 code here... //some more code here... }, values: (state, player) => { if(state.waitFor == actionA2) //return values for actionA2 return [] } }I'd be much happier with some way to declare this sequence of actions implicitly and have the game engine take care of it, like:
actionA1 = (state, ...args) => { //some code here... /* suspend execution until a valid actionA2 is performed, but somehow this can be resumed just from reloading the serialized state, meaning that on a server restart the code above would not be executed */ await actionA2 //some more code here... } actionA2 = { handler: (state, player) => { //actionA2 code here... }, values: (state, ...args) => { /* no need to explicitly check any conditions as this action can only be performed as part of actionA1 */ //return values for actionA2 } }
I haven't managed to find any well-known pattern which could give me some insight on how to best go about implementing this. I'm not trying to shoehorn something here for the sake of it, just trying to get a sense of a general guideline and other similar cases in real-life that I could learn from.
In some sense, this works much like a state machine, with some kind of RPC system to mediate between client/server that triggers the state changes.
I'm trying to go for a functional approach here, so no mutations of objects, and all effects depend solely on the game state. Also, I'm developing this in JavaScript, but I don't think that matters much.