Files
jonas 2764849943 feat: add Chapter 2 gold miner FSM example in Go
Port the finite state machine from "Programming Game AI by Example"
chapter 2: BaseGameEntity, a generic State[T]/StateMachine[T] pair
(Go generics standing in for the book's C++ templates), and a Miner
that cycles through digging, banking, sleeping, and drinking states.
VisitBankAndDepositGold, GoHomeAndSleepTilRested, and QuenchThirst
are original implementations based on the book's written behavior
description, since their C++ source wasn't available. Includes a
runnable demo in main.go.
2026-09-03 11:36:05 +02:00

128 lines
4.3 KiB
Go

package westworld
// StateMachine is the Go version of the C++ class template:
//
// template <class entity_type>
// class StateMachine { ... };
//
// Same trick as State[T]: [T any] is a blank slot filled in with
// whichever entity type owns this machine, e.g. StateMachine[Miner].
// Instead of an agent managing its own currentState field and
// ChangeState method by hand (like Miner did before), the agent now
// just owns one of these and hands it all the work.
type StateMachine[T any] struct {
// owner is the agent this state machine belongs to and acts on.
// Go version of "entity_type* m_pOwner".
owner *T
// currentState is whatever mode the agent is in right now.
currentState State[T]
// previousState is a record of the last state the agent was in,
// so RevertToPreviousState can jump back to it.
previousState State[T]
// globalState's Execute runs on every single Update, no matter
// what currentState is - for behavior that should always be
// checked (the book will show a real use for this later).
globalState State[T]
}
// NewStateMachine is the Go stand-in for the C++ constructor
// "StateMachine(entity_type* owner)". Go doesn't run constructor
// initializer lists automatically, so this function does that job
// by hand: remember the owner, leave every state slot empty. An
// empty interface value in Go is nil - that's the direct equivalent
// of the book's NULL.
func NewStateMachine[T any](owner *T) *StateMachine[T] {
return &StateMachine[T]{owner: owner}
}
// SetCurrentState, SetGlobalState and SetPreviousState are for
// initializing the FSM directly - for example, giving an agent its
// very first state without running it through the Enter/Exit dance
// that ChangeState does.
func (sm *StateMachine[T]) SetCurrentState(s State[T]) {
sm.currentState = s
}
func (sm *StateMachine[T]) SetGlobalState(s State[T]) {
sm.globalState = s
}
func (sm *StateMachine[T]) SetPreviousState(s State[T]) {
sm.previousState = s
}
// Update runs every tick. The C++ version marked this method const
// (a promise that it won't change the StateMachine's own fields) -
// Go has no equivalent keyword, and doesn't need one; nothing here
// requires special marking. If a global state is set, its Execute
// always runs; then, separately, whatever the current state is also
// runs. Both can fire in the same tick - that's the point of having
// two slots instead of one.
func (sm *StateMachine[T]) Update() {
// if a global state exists, call its execute method
if sm.globalState != nil {
sm.globalState.Execute(sm.owner)
}
// same for the current state
if sm.currentState != nil {
sm.currentState.Execute(sm.owner)
}
}
// ChangeState switches the agent into a new state properly: it
// remembers where it came from, tells the old state it's being
// left, swaps over, then tells the new state it's being entered.
func (sm *StateMachine[T]) ChangeState(newState State[T]) {
// The C++ code used assert() to make sure the new state isn't a
// null pointer. Go's equivalent is panic() - nil is Go's version
// of a null pointer/null value.
if newState == nil {
panic("StateMachine.ChangeState: trying to change to a nil state")
}
// keep a record of the previous state
sm.previousState = sm.currentState
// call the exit method of the existing state
sm.currentState.Exit(sm.owner)
// change state to the new state
sm.currentState = newState
// call the entry method of the new state
sm.currentState.Enter(sm.owner)
}
// RevertToPreviousState switches back to whatever state the agent
// was in right before its current one.
func (sm *StateMachine[T]) RevertToPreviousState() {
sm.ChangeState(sm.previousState)
}
// CurrentState, GlobalState and PreviousState are plain read-only
// accessors for the three state slots.
func (sm *StateMachine[T]) CurrentState() State[T] {
return sm.currentState
}
func (sm *StateMachine[T]) GlobalState() State[T] {
return sm.globalState
}
func (sm *StateMachine[T]) PreviousState() State[T] {
return sm.previousState
}
// IsInState is supposed to report whether the agent's current state
// is the same type as the state passed in. The book only declared
// this method here - its body comes later - so this is a
// placeholder for now.
func (sm *StateMachine[T]) IsInState(st State[T]) bool {
// TODO: fill in once the book shows this method's body.
return false
}