Building A State Machine In Go
While working on the backend for a web-based playing card game called Judgement, I required a clean way to manage the flow of the game. The obvious solution to that was a state machine, here’s how I built it with Go.
States
In my game, I have the following states:
- Bidding: Players place their bids
- Playing: Players play their moves
- Resolution: Points are assigned to each player
- Game Over: The game ends and the winner is chosen
The game cycles through Bidding, Playing, and Resolution for 10-14 rounds where players accumulate points at the end of each round.
Events
In order to actually facilitate the transiton of each state, we introduce the concept of Events:
- bidding_done
- playing_continue
- playing_done
- round_resolved
Implementation
StateMachine Struct
First define types for State and Event to make the code more readable
type State stringtype Event stringThen create the state machine
type StateMachine struct { state State transitions map[State]map[Event]State}This transitions defines which events are valid for each state and what state they lead to.
Initializing The State Machine
func NewStateMachine(initial State) *StateMachine { return &StateMachine{ state: initial, transitions: make(map[State]map[Event]State), }}Adding Transitions
func (sm *StateMachine) AddTransition(from State, event Event, to State) { if sm.transitions[from] == nil { sm.transitions[from] = make(map[Event]State) } sm.transitions[from][event] = to}Triggering Events
func (sm *StateMachine) Trigger(event Event) error { next, ok := sm.transitions[sm.state][event] if !ok { return fmt.Errorf("invalid transition: %s + %s", sm.state, event) } sm.state = next return nil}Example Usage
First the game-specific states and events are defined
const ( // States StateBid State = "bidding" StatePlay State = "playing" StateResolution State = "resolution" StateGameOver State = "gameover")
const ( // Events BiddingDone Event = "bidding_done" PlayingContinue Event = "playing_continue" PlayingDone Event = "playing_done" RoundResolved Event = "round_resolved")Then the state machine is instantiated and the transitions are added.
sm := NewStateMachine(StateBid)
// Define valid state transitionssm.AddTransition(StateBid, BiddingDone, StatePlay)sm.AddTransition(StatePlay, PlayingDone, StateResolution)sm.AddTransition(StateResolution, PlayingContinue, StateBid)sm.AddTransition(StateResolution, PlayingDone, StateGameOver)Remember, you don’t have to linearly transition. Notice how StateResolution can transition back to StateBid or StateGameOver depending on the Event.
Finally in our game logic, whenever an event occurs, we can transition by using the event as an input.
For example, if we call Trigger(BiddingDone) it should automatically transition to
if err := sm.Trigger(BiddingDone); err != nil { fmt.Println(err)}← Back to blog