diff --git a/chapter-2-state-driven-agent-design/main.go b/chapter-2-state-driven-agent-design/cmd/westworld/main.go similarity index 100% rename from chapter-2-state-driven-agent-design/main.go rename to chapter-2-state-driven-agent-design/cmd/westworld/main.go diff --git a/chapter-2-state-driven-agent-design/cmd/westworldwithwoman/main.go b/chapter-2-state-driven-agent-design/cmd/westworldwithwoman/main.go new file mode 100644 index 0000000..1c1d188 --- /dev/null +++ b/chapter-2-state-driven-agent-design/cmd/westworldwithwoman/main.go @@ -0,0 +1,24 @@ +// Command westworldwithwoman runs the extended demo from the book: +// Miner Bob and his wife Elsa both tick forward together, so you can +// watch their two state machines run side by side, same as the +// book's sample console output. +package main + +import ( + "fmt" + + "git.jjstudio.me/jonas/Programming-Game-AI-by-Example-Go/chapter-2-state-driven-agent-design/westworldwithwoman" +) + +func main() { + bob := westworldwithwoman.NewMiner(1) + elsa := westworldwithwoman.NewElsa(2) + + const numUpdateSteps = 30 + for range numUpdateSteps { + bob.Update() + elsa.Update() + } + + fmt.Println() +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go new file mode 100644 index 0000000..5877069 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go @@ -0,0 +1,49 @@ +package westworldwithwoman + +// Elsa is the miner's wife, introduced in this section of the book. +// She's built the exact same way Miner is: embed *BaseGameEntity for +// her ID, own a *StateMachine[Elsa] and delegate all state +// bookkeeping to it. This is the payoff of making State[T] and +// StateMachine[T] generic earlier - Elsa reuses both unchanged, +// just with the blank slot filled in as Elsa instead of Miner. +type Elsa struct { + *BaseGameEntity + + stateMachine *StateMachine[Elsa] +} + +// NewElsa sets up Elsa's state machine: DoHousework is her ordinary, +// everyday state, and WifesGlobalState is what checks - every single +// tick, no matter what she's currently doing - whether nature is +// calling. That's exactly why it has to be a global state rather +// than logic inside DoHousework: the book says "Elsa may feel the +// call of nature during any state and at any time," and a global +// state's Execute runs regardless of the current state. +func NewElsa(id int) *Elsa { + e := &Elsa{ + BaseGameEntity: NewBaseGameEntity(id), + } + + e.stateMachine = NewStateMachine(e) + e.stateMachine.SetCurrentState(DoHousework()) + e.stateMachine.SetGlobalState(WifesGlobalState()) + + return e +} + +// FSM returns Elsa's state machine. +func (e *Elsa) FSM() *StateMachine[Elsa] { + return e.stateMachine +} + +// Update satisfies the Entity interface (alongside the ID() method +// borrowed from BaseGameEntity) and just delegates to the state +// machine, same shape as Miner.Update. +func (e *Elsa) Update() { + e.stateMachine.Update() +} + +// ChangeState forwards to the state machine, same as Miner.ChangeState. +func (e *Elsa) ChangeState(newState State[Elsa]) { + e.stateMachine.ChangeState(newState) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/entity.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/entity.go new file mode 100644 index 0000000..2741fe1 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/entity.go @@ -0,0 +1,69 @@ +package westworldwithwoman + +// Entity is what every game object in the world must be able to do. +// The C++ code used a pure virtual function (Update()=0) to force +// every subclass to write its own Update. Go has no subclasses, so +// instead we say: "anything with an Update() and an ID() method +// counts as an Entity." A type only satisfies this interface once +// it has its own Update method - just like the C++ version couldn't +// be used until Update was implemented. +type Entity interface { + Update() + ID() int +} + +// nextValidID plays the role of the C++ "static int m_iNextValidID". +// In C++, "static" on a class member means "one shared copy for the +// whole class, not one per object." Go doesn't have that keyword for +// structs, so we just use a normal package-level variable - it's +// shared by every BaseGameEntity for the same reason: there's only +// one of it. +var nextValidID int + +// BaseGameEntity is the Go version of the C++ class BaseGameEntity. +// Go doesn't have "classes" or "inheritance" - instead, other types +// (like a Miner or a Wife later in the book) will embed this struct +// inside themselves to borrow its fields and methods, and then add +// their own Update method on top. +type BaseGameEntity struct { + id int // was "m_ID" - the entity's own unique number +} + +// NewBaseGameEntity replaces the C++ constructor: +// +// BaseGameEntity(int id) { SetID(id); } +// +// Go structs don't run any code automatically when you create them, +// so we write a plain function that builds one correctly. Any code +// that wants a BaseGameEntity should call this instead of building +// the struct by hand. +func NewBaseGameEntity(id int) *BaseGameEntity { + e := &BaseGameEntity{} + e.setID(id) + return e +} + +// setID is the Go version of the private C++ method SetID. It does +// exactly what the comment in the book says: check the requested ID +// isn't lower than the next free one, store it, then move the +// "next free ID" counter past it. +// +// The C++ code used assert() for the check, which only runs in +// debug builds and does nothing in a release build. Go has no +// assert, so we use panic - it always runs, and it stops the +// program immediately if this ever happens, which is exactly what +// a broken invariant like this deserves. +func (e *BaseGameEntity) setID(val int) { + if val < nextValidID { + panic("BaseGameEntity.setID: invalid ID") + } + e.id = val + nextValidID = e.id + 1 +} + +// ID returns the entity's unique number. This is the Go version of +// the C++ "int ID() const { return m_ID; }" - a small read-only +// accessor for the private field. +func (e *BaseGameEntity) ID() int { + return e.id +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/miner.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/miner.go new file mode 100644 index 0000000..a3fca36 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/miner.go @@ -0,0 +1,248 @@ +package westworldwithwoman + +// State is the Go version of the C++ class template: +// +// template +// class State +// { +// public: +// virtual void Enter(entity_type*)=0; +// virtual void Execute(entity_type*)=0; +// virtual void Exit(entity_type*)=0; +// virtual ~State(){} +// }; +// +// The book turned State into a template so it isn't stuck being a +// checklist for Miner only - any entity type can use it. Go's +// version of a template is a generic type: the "[T any]" part is a +// blank slot (called a type parameter) that gets filled in with a +// real type wherever State is used. T stands in for the book's +// entity_type. "any" just means "T can be filled in with literally +// any type" - no restrictions. +// +// C++ used "=0" (pure virtual) to say "every real state MUST provide +// these three methods, this class itself is just a checklist." Go's +// interface type does exactly that job: nothing implements State +// automatically - a type only counts as a State[T] once it has all +// three methods with these exact signatures for that particular T. +// +// There's no Go equivalent needed for "virtual ~State(){}" (the +// destructor) - Go has a garbage collector that cleans up memory on +// its own, so there's nothing to write by hand for that. +type State[T any] interface { + // Enter runs once, the moment an entity switches into this state. + Enter(entity *T) + + // Execute runs every single update step while the entity is in + // this state - this is where the actual behavior/logic lives. + Execute(entity *T) + + // Exit runs once, the moment the entity leaves this state. + Exit(entity *T) +} + +// locationType is a temporary stand-in for the C++ enum +// "location_type" (things like shack, goldmine, bank, saloon). The +// book hasn't shown us that enum yet, so this is a plain number type +// for now until you paste the real definition. +type locationType int + +// shack, goldmine, bank and saloon are the four locations the miner +// visits, matching the four states described in the book (we still +// haven't seen the book's actual enum definition, so these are our +// own stand-in values - their exact numbers don't matter, we only +// ever compare locations for equality). +const ( + shack locationType = iota + goldmine + bank + saloon +) + +// Miner is the Go version of: +// +// class Miner : public BaseGameEntity +// +// In C++, "Miner : public BaseGameEntity" means a Miner IS a +// BaseGameEntity - it inherits all of its fields and methods for +// free. Go has no inheritance. Instead we EMBED a *BaseGameEntity +// inside Miner: we stick the smaller struct inside the bigger one, +// and Go automatically lets Miner "borrow" BaseGameEntity's methods +// as if they were its own. That's why Miner gets an ID() method +// without us writing one ourselves - it's borrowed. +type Miner struct { + *BaseGameEntity + + // stateMachine is the Go version of + // "StateMachine* m_pStateMachine". All the bookkeeping + // that used to live directly on Miner (currentState, and the + // exit/enter dance in ChangeState) now belongs to this instead - + // Miner just delegates to it. + stateMachine *StateMachine[Miner] + + // location is where the miner currently is. + location locationType + + // goldCarried: how many gold nuggets the miner has in his pockets. + goldCarried int + + // moneyInBank: how much money the miner has deposited at the bank. + moneyInBank int + + // thirst: the higher this number, the thirstier the miner is. + thirst int + + // fatigue: the higher this number, the more tired the miner is. + fatigue int +} + +// NewMiner is the Go stand-in for the C++ constructor "Miner(int id)". +// C++'s member initializer list (the "m_Location(shack), ..." part +// before the constructor's { }) is just how C++ sets a struct's +// starting field values before the body runs - in Go we set the same +// starting values with an ordinary struct literal instead. +func NewMiner(id int) *Miner { + m := &Miner{ + BaseGameEntity: NewBaseGameEntity(id), + location: shack, + goldCarried: 0, + moneyInBank: 0, + thirst: 0, + fatigue: 0, + } + + // set up state machine + m.stateMachine = NewStateMachine(m) + m.stateMachine.SetCurrentState(GoHomeAndSleepTilRested()) + m.stateMachine.SetGlobalState(MinerGlobalState()) + + return m +} + +// FSM returns the miner's state machine. This is the Go version of +// "GetFSM()const" - Go convention drops the "Get" prefix that +// C++/Java getters use, so it's just FSM(). +func (m *Miner) FSM() *StateMachine[Miner] { + return m.stateMachine +} + +// Location returns where the miner currently is. This is the getter +// the book's Enter/Execute/Exit methods call to check on a miner +// before deciding what to do. +func (m *Miner) Location() locationType { + return m.location +} + +// ChangeLocation moves the miner to a new location. This is the +// setter half of the pair - states call this to actually send a +// miner somewhere. +func (m *Miner) ChangeLocation(loc locationType) { + m.location = loc +} + +// AddToGoldCarried adds amount to how much gold the miner is +// currently carrying in his pockets (separate from what's already +// safely deposited at the bank). +func (m *Miner) AddToGoldCarried(amount int) { + m.goldCarried += amount +} + +// IncreaseFatigue bumps the miner's tiredness up by one notch. +func (m *Miner) IncreaseFatigue() { + m.fatigue++ +} + +// maxNuggets is a placeholder for a constant the book hasn't shown +// us the exact value of yet (something like "const int MaxNuggets"). +// It's how many gold nuggets a miner's pockets can hold before his +// pockets count as "full." Replace this once you paste the real +// value from the book. +const maxNuggets = 3 + +// PocketsFull reports whether the miner is carrying as much gold as +// his pockets can hold. +func (m *Miner) PocketsFull() bool { + return m.goldCarried >= maxNuggets +} + +// thirstThreshold is a placeholder for a constant the book hasn't +// shown us the exact value of yet (it's called something like +// "ComfortLevel" there). It's the point past which the miner's +// thirst counts as "too thirsty." Replace this once you paste the +// real value from the book. +const thirstThreshold = 5 + +// Thirsty reports whether the miner's thirst has crossed the +// threshold where he needs a drink. +func (m *Miner) Thirsty() bool { + return m.thirst > thirstThreshold +} + +// BuyAndDrinkAWhiskey simulates buying and drinking a whiskey: +// thirst drops straight back to zero. We're inventing this method +// ourselves (the book hasn't shown the QuenchThirst state's real +// Execute body to us), based purely on the chapter's written +// description: "he visits the saloon in order to buy a whiskey. When +// his thirst is quenched, ...". Named differently from the +// QuenchThirst state itself (below) to avoid confusing the two. +func (m *Miner) BuyAndDrinkAWhiskey() { + m.thirst = 0 +} + +// TransferGoldToBank moves everything the miner is carrying into his +// bank balance, and empties his pockets back out. +func (m *Miner) TransferGoldToBank() { + m.moneyInBank += m.goldCarried + m.goldCarried = 0 +} + +// comfortableWealth is our own placeholder for how much money in the +// bank counts as "wealthy enough" to head home and rest, per the +// book's description of VisitBankAndDepositGold. The book hasn't +// given us a real number for this, so this is a guess - swap it out +// if you find the book's actual value. +const comfortableWealth = 6 + +// RichEnough reports whether the miner has banked enough money to +// feel comfortable stopping work for a while. +func (m *Miner) RichEnough() bool { + return m.moneyInBank >= comfortableWealth +} + +// DecreaseFatigue lowers the miner's tiredness by one notch, without +// letting it go below zero. +func (m *Miner) DecreaseFatigue() { + if m.fatigue > 0 { + m.fatigue-- + } +} + +// fatigueThreshold is our own placeholder for the "acceptable level" +// of fatigue mentioned in GoHomeAndSleepTilRested's description. The +// book hasn't given us a real number for this either - another +// guess, swap it out once you know the real one. +const fatigueThreshold = 5 + +// Fatigued reports whether the miner is still too tired to go back +// to work. +func (m *Miner) Fatigued() bool { + return m.fatigue > fatigueThreshold +} + +// Update is the real body now - the book finally showed it. Every +// tick the miner gets a little thirstier, then the state machine is +// told to run (which executes the global state, then the current +// state - see StateMachine.Update). +func (m *Miner) Update() { + m.thirst++ + m.stateMachine.Update() +} + +// ChangeState no longer does the exit/enter/swap work itself - that +// job now belongs entirely to StateMachine.ChangeState. Miner just +// passes the request along. This is exactly the cleanup the book +// promised: the agent doesn't manage state transitions by hand +// anymore, it delegates. +func (m *Miner) ChangeState(newState State[Miner]) { + m.stateMachine.ChangeState(newState) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go new file mode 100644 index 0000000..c3c2f27 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go @@ -0,0 +1,21 @@ +package westworldwithwoman + +import "fmt" + +// entityNames is a temporary stand-in for the book's real name +// lookup table. We still haven't been shown that table, so this is +// just a small map we're filling in by hand as we meet entities - +// the miner (ID 1) and now his wife Elsa (ID 2). +var entityNames = map[int]string{ + 1: "Miner Bob", + 2: "Elsa", +} + +// GetNameOfEntity looks up a friendly name for a given entity ID. +// Anything not in entityNames falls back to printing the raw ID. +func GetNameOfEntity(id int) string { + if name, ok := entityNames[id]; ok { + return name + } + return fmt.Sprintf("Entity %d", id) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_do_housework.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_do_housework.go new file mode 100644 index 0000000..36d3f92 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_do_housework.go @@ -0,0 +1,48 @@ +package westworldwithwoman + +import ( + "fmt" + "math/rand" + "sync" +) + +// doHousework is Elsa's everyday default state - the singleton +// pattern is the same one used throughout this package. +type doHousework struct{} + +var ( + doHouseworkInstance *doHousework + doHouseworkOnce sync.Once +) + +// DoHousework is the Go stand-in for "DoHousework::Instance()". +func DoHousework() *doHousework { + doHouseworkOnce.Do(func() { + doHouseworkInstance = &doHousework{} + }) + return doHouseworkInstance +} + +var _ State[Elsa] = (*doHousework)(nil) + +// chores is our own small stand-in for the book's actual chore list +// - it only ever showed two examples in its sample output ("Moppin' +// the floor", "Makin' the bed"), so that's exactly what's reused +// here. One gets picked at random each tick. +var chores = []string{ + "Moppin' the floor", + "Makin' the bed", +} + +// Enter does nothing - the book's sample output never shows a +// message when Elsa starts doing housework, only while she's doing +// it (Execute) and when she leaves the bathroom back into it. +func (s *doHousework) Enter(e *Elsa) {} + +// Execute announces one randomly chosen chore per tick. +func (s *doHousework) Execute(e *Elsa) { + fmt.Printf("\n%s: %s", GetNameOfEntity(e.ID()), chores[rand.Intn(len(chores))]) +} + +// Exit does nothing, for the same reason as Enter. +func (s *doHousework) Exit(e *Elsa) {} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_enter_mine_and_dig_for_nugget.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_enter_mine_and_dig_for_nugget.go new file mode 100644 index 0000000..f1c2136 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_enter_mine_and_dig_for_nugget.go @@ -0,0 +1,82 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// enterMineAndDigForNugget is the Go version of the singleton class +// EnterMineAndDigForNugget from the book. Same trick as the MyClass +// example: the type name is lowercase (unexported), so no code +// outside this package can even write the type's name, let alone +// build one directly. The only legal front door is the +// EnterMineAndDigForNugget() function below. +type enterMineAndDigForNugget struct{} + +var ( + enterMineAndDigForNuggetInstance *enterMineAndDigForNugget + enterMineAndDigForNuggetOnce sync.Once +) + +// EnterMineAndDigForNugget is the Go stand-in for the C++ +// "EnterMineAndDigForNugget::Instance()" static method. No matter +// how many times, or from where, this gets called, it always hands +// back the exact same object - built only the first time it's +// actually needed. sync.Once is Go's tool for "run this exactly +// once, even if many goroutines try at the same time" - it plays +// the same role as C++11's guarantee that a function-local static +// is only ever initialized once. +func EnterMineAndDigForNugget() *enterMineAndDigForNugget { + enterMineAndDigForNuggetOnce.Do(func() { + enterMineAndDigForNuggetInstance = &enterMineAndDigForNugget{} + }) + return enterMineAndDigForNuggetInstance +} + +// compile-time check: this line does nothing at runtime, it just +// makes the compiler verify that *enterMineAndDigForNugget really +// does satisfy State[Miner] (has Enter, Execute and Exit, all taking +// a *Miner). This is the Go version of the book's +// "class EnterMineAndDigForNugget : public State" - filling +// in the template's blank slot with Miner. +var _ State[Miner] = (*enterMineAndDigForNugget)(nil) + +// Enter is the Go version of EnterMineAndDigForNugget::Enter. If the +// miner isn't already at the gold mine, send him walking there. +func (s *enterMineAndDigForNugget) Enter(m *Miner) { + if m.Location() != goldmine { + fmt.Printf("\n%s: Walkin' to the gold mine", GetNameOfEntity(m.ID())) + m.ChangeLocation(goldmine) + } +} + +// Execute is the Go version of EnterMineAndDigForNugget::Execute. +// Every tick spent in this state, the miner picks up one nugget and +// gets a little more tired. If his pockets are full he heads to the +// bank; if he's gotten thirsty along the way he heads to the saloon +// instead. +func (s *enterMineAndDigForNugget) Execute(m *Miner) { + m.AddToGoldCarried(1) + + // diggin' is hard work + m.IncreaseFatigue() + + fmt.Printf("\n%s: Pickin' up a nugget", GetNameOfEntity(m.ID())) + + // if enough gold mined, go and put it in the bank + if m.PocketsFull() { + m.ChangeState(VisitBankAndDepositGold()) + } + + // if thirsty go and get a whiskey + if m.Thirsty() { + m.ChangeState(QuenchThirst()) + } +} + +// Exit is the Go version of EnterMineAndDigForNugget::Exit. It just +// announces that the miner is leaving the mine - all the actual +// state-switching already happened back in Execute. +func (s *enterMineAndDigForNugget) Exit(m *Miner) { + fmt.Printf("\n%s: Ah'm leavin' the gold mine with mah pockets full o' sweet gold", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_go_home_and_sleep_til_rested.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_go_home_and_sleep_til_rested.go new file mode 100644 index 0000000..d24829c --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_go_home_and_sleep_til_rested.go @@ -0,0 +1,59 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// goHomeAndSleepTilRested is the Go version of the (not yet pasted) +// singleton class GoHomeAndSleepTilRested from the book. Same +// pattern as enterMineAndDigForNugget: unexported type, one shared +// instance, reached only through the function below. This is the +// miner's starting state. +type goHomeAndSleepTilRested struct{} + +var ( + goHomeAndSleepTilRestedInstance *goHomeAndSleepTilRested + goHomeAndSleepTilRestedOnce sync.Once +) + +// GoHomeAndSleepTilRested is the Go stand-in for +// "GoHomeAndSleepTilRested::Instance()". +func GoHomeAndSleepTilRested() *goHomeAndSleepTilRested { + goHomeAndSleepTilRestedOnce.Do(func() { + goHomeAndSleepTilRestedInstance = &goHomeAndSleepTilRested{} + }) + return goHomeAndSleepTilRestedInstance +} + +var _ State[Miner] = (*goHomeAndSleepTilRested)(nil) + +// The book hasn't shown us this state's real code, so this is our +// own implementation based purely on its written description: "the +// miner will return to his shack and sleep until his fatigue level +// drops below an acceptable level. He will then change state to +// EnterMineAndDigForNugget." + +// Enter sends the miner home, if he isn't there already. +func (s *goHomeAndSleepTilRested) Enter(m *Miner) { + if m.Location() != shack { + fmt.Printf("\n%s: Headin' on home", GetNameOfEntity(m.ID())) + m.ChangeLocation(shack) + } +} + +// Execute sleeps off a little fatigue each tick, and once the miner +// is rested enough, sends him back to work. +func (s *goHomeAndSleepTilRested) Execute(m *Miner) { + if m.Fatigued() { + fmt.Printf("\n%s: ZZZZ... ", GetNameOfEntity(m.ID())) + m.DecreaseFatigue() + } else { + m.ChangeState(EnterMineAndDigForNugget()) + } +} + +// Exit announces that the miner is up and about again. +func (s *goHomeAndSleepTilRested) Exit(m *Miner) { + fmt.Printf("\n%s: What a great sleep! Time to find more gold", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_machine.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_machine.go new file mode 100644 index 0000000..784eda8 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_machine.go @@ -0,0 +1,131 @@ +package westworldwithwoman + +// StateMachine is the Go version of the C++ class template: +// +// template +// 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 reports whether the agent's current state is the same +// state as the one passed in. The book describes this as comparing +// state *types* (it takes "const State&", not a +// pointer, and presumably checks typeid under the hood) - but every +// concrete state in this codebase is a singleton (there's only ever +// one *doHousework, one *visitBathroom, and so on), so comparing the +// two interface values directly for equality gets the same answer: +// they're equal exactly when they're both that one shared instance +// of the same state type. +func (sm *StateMachine[T]) IsInState(st State[T]) bool { + return sm.currentState == st +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_miner_global_state.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_miner_global_state.go new file mode 100644 index 0000000..5c7d39b --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_miner_global_state.go @@ -0,0 +1,43 @@ +package westworldwithwoman + +import "sync" + +// minerGlobalState is the Go version of the (not yet pasted) +// singleton class MinerGlobalState from the book. Same pattern as +// enterMineAndDigForNugget: unexported type, one shared instance, +// reached only through the function below. This is the state whose +// Execute runs on every single tick, regardless of whatever else +// the miner is currently doing (see StateMachine.Update). +type minerGlobalState struct{} + +var ( + minerGlobalStateInstance *minerGlobalState + minerGlobalStateOnce sync.Once +) + +// MinerGlobalState is the Go stand-in for +// "MinerGlobalState::Instance()". +func MinerGlobalState() *minerGlobalState { + minerGlobalStateOnce.Do(func() { + minerGlobalStateInstance = &minerGlobalState{} + }) + return minerGlobalStateInstance +} + +var _ State[Miner] = (*minerGlobalState)(nil) + +// Enter, Execute and Exit are placeholders - the book hasn't shown +// us this state's real methods yet. This stub exists only so +// NewMiner can set it as the miner's global state and compile. + +func (s *minerGlobalState) Enter(m *Miner) { + // TODO: fill in once the book shows this method's body. +} + +func (s *minerGlobalState) Execute(m *Miner) { + // TODO: fill in once the book shows this method's body. +} + +func (s *minerGlobalState) Exit(m *Miner) { + // TODO: fill in once the book shows this method's body. +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_quench_thirst.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_quench_thirst.go new file mode 100644 index 0000000..77238d5 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_quench_thirst.go @@ -0,0 +1,53 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// quenchThirst is the Go version of the (not yet pasted) singleton +// class QuenchThirst from the book. Same pattern as +// enterMineAndDigForNugget: unexported type, one shared instance, +// reached only through the function below. +type quenchThirst struct{} + +var ( + quenchThirstInstance *quenchThirst + quenchThirstOnce sync.Once +) + +// QuenchThirst is the Go stand-in for "QuenchThirst::Instance()". +func QuenchThirst() *quenchThirst { + quenchThirstOnce.Do(func() { + quenchThirstInstance = &quenchThirst{} + }) + return quenchThirstInstance +} + +var _ State[Miner] = (*quenchThirst)(nil) + +// The book hasn't shown us this state's real code, so this is our +// own implementation based purely on its written description: "he +// visits the saloon in order to buy a whiskey. When his thirst is +// quenched, he changes state to EnterMineAndDigForNugget." + +// Enter sends the miner to the saloon, if he isn't there already. +func (s *quenchThirst) Enter(m *Miner) { + if m.Location() != saloon { + fmt.Printf("\n%s: Headin' to the saloon for a whiskey", GetNameOfEntity(m.ID())) + m.ChangeLocation(saloon) + } +} + +// Execute buys and drinks one whiskey, which is all it takes to +// quench the thirst, then goes straight back to digging. +func (s *quenchThirst) Execute(m *Miner) { + fmt.Printf("\n%s: That's mighty fine sippin' liquor", GetNameOfEntity(m.ID())) + m.BuyAndDrinkAWhiskey() + m.ChangeState(EnterMineAndDigForNugget()) +} + +// Exit announces that the miner is leaving the saloon. +func (s *quenchThirst) Exit(m *Miner) { + fmt.Printf("\n%s: Leavin' the saloon, feelin' good", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bank_and_deposit_gold.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bank_and_deposit_gold.go new file mode 100644 index 0000000..3783867 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bank_and_deposit_gold.go @@ -0,0 +1,61 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// visitBankAndDepositGold is the Go version of the (not yet pasted) +// singleton class VisitBankAndDepositGold from the book. Same +// pattern as enterMineAndDigForNugget: unexported type, one shared +// instance, reached only through the function below. +type visitBankAndDepositGold struct{} + +var ( + visitBankAndDepositGoldInstance *visitBankAndDepositGold + visitBankAndDepositGoldOnce sync.Once +) + +// VisitBankAndDepositGold is the Go stand-in for +// "VisitBankAndDepositGold::Instance()". +func VisitBankAndDepositGold() *visitBankAndDepositGold { + visitBankAndDepositGoldOnce.Do(func() { + visitBankAndDepositGoldInstance = &visitBankAndDepositGold{} + }) + return visitBankAndDepositGoldInstance +} + +var _ State[Miner] = (*visitBankAndDepositGold)(nil) + +// The book hasn't shown us this state's real code, so this is our +// own implementation based purely on its written description: "the +// miner will walk to the bank and deposit any nuggets he is +// carrying. If he then considers himself wealthy enough, he will +// change state to GoHomeAndSleepTilRested. Otherwise he will change +// state to EnterMineAndDigForNugget." + +// Enter sends the miner to the bank, if he isn't there already. +func (s *visitBankAndDepositGold) Enter(m *Miner) { + if m.Location() != bank { + fmt.Printf("\n%s: Goin' to the bank. Yes siree", GetNameOfEntity(m.ID())) + m.ChangeLocation(bank) + } +} + +// Execute deposits whatever gold the miner is carrying, then decides +// where to go next based on how wealthy that leaves him. +func (s *visitBankAndDepositGold) Execute(m *Miner) { + m.TransferGoldToBank() + fmt.Printf("\n%s: Depositing gold. Total savings now: %d", GetNameOfEntity(m.ID()), m.moneyInBank) + + if m.RichEnough() { + m.ChangeState(GoHomeAndSleepTilRested()) + } else { + m.ChangeState(EnterMineAndDigForNugget()) + } +} + +// Exit announces that the miner is leaving the bank. +func (s *visitBankAndDepositGold) Exit(m *Miner) { + fmt.Printf("\n%s: Leavin' the bank", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bathroom.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bathroom.go new file mode 100644 index 0000000..e155e48 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bathroom.go @@ -0,0 +1,47 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// visitBathroom is a "blip" state: it interrupts whatever Elsa was +// doing, does its one bit of business, and immediately hands control +// straight back to whatever she was doing before - it never leads +// anywhere new on its own. +type visitBathroom struct{} + +var ( + visitBathroomInstance *visitBathroom + visitBathroomOnce sync.Once +) + +// VisitBathroom is the Go stand-in for "VisitBathroom::Instance()". +func VisitBathroom() *visitBathroom { + visitBathroomOnce.Do(func() { + visitBathroomInstance = &visitBathroom{} + }) + return visitBathroomInstance +} + +var _ State[Elsa] = (*visitBathroom)(nil) + +// Enter announces the trip to the bathroom. +func (s *visitBathroom) Enter(e *Elsa) { + fmt.Printf("\n%s: Walkin' to the can. Need to powda mah pretty li'l nose", GetNameOfEntity(e.ID())) +} + +// Execute is the entire "blip": do the business, then immediately +// revert to whatever state was active before VisitBathroom +// interrupted it. RevertToPreviousState runs the normal ChangeState +// dance (Exit here, Enter there), so nothing extra is needed. +func (s *visitBathroom) Execute(e *Elsa) { + fmt.Printf("\n%s: Ahhhhhh! Sweet relief!", GetNameOfEntity(e.ID())) + e.FSM().RevertToPreviousState() +} + +// Exit announces leaving the bathroom - this runs automatically as +// part of the RevertToPreviousState call above. +func (s *visitBathroom) Exit(e *Elsa) { + fmt.Printf("\n%s: Leavin' the john", GetNameOfEntity(e.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_wifes_global_state.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_wifes_global_state.go new file mode 100644 index 0000000..c2050bc --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_wifes_global_state.go @@ -0,0 +1,50 @@ +package westworldwithwoman + +import ( + "math/rand" + "sync" +) + +// wifesGlobalState is Elsa's global state - its Execute runs on +// every single tick of her state machine, in addition to whatever +// her current state is doing (see StateMachine.Update). That's what +// makes it the right place for "does nature call right now" logic: +// the book says that can happen "during any state and at any time," +// which is exactly what a global state is for. +type wifesGlobalState struct{} + +var ( + wifesGlobalStateInstance *wifesGlobalState + wifesGlobalStateOnce sync.Once +) + +// WifesGlobalState is the Go stand-in for +// "WifesGlobalState::Instance()". +func WifesGlobalState() *wifesGlobalState { + wifesGlobalStateOnce.Do(func() { + wifesGlobalStateInstance = &wifesGlobalState{} + }) + return wifesGlobalStateInstance +} + +var _ State[Elsa] = (*wifesGlobalState)(nil) + +// Enter and Exit do nothing - a global state is set once, in +// NewElsa, and never changed, so these never actually get called. +// They only exist to satisfy the State[Elsa] interface. +func (s *wifesGlobalState) Enter(e *Elsa) {} +func (s *wifesGlobalState) Exit(e *Elsa) {} + +// bathroomCallChance is a 1-in-N chance, per the chapter's "1 in 10 +// chance to change to VisitBathroom state." +const bathroomCallChance = 10 + +// Execute rolls the dice every tick. If it comes up, and Elsa isn't +// already in the bathroom (checked with IsInState, so this can't +// interrupt a bathroom trip that's already happening), she breaks +// off into VisitBathroom. +func (s *wifesGlobalState) Execute(e *Elsa) { + if !e.FSM().IsInState(VisitBathroom()) && rand.Intn(bathroomCallChance) == 0 { + e.ChangeState(VisitBathroom()) + } +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/elsa.go b/chapter-2-state-driven-agent-design/westworldwithwoman/elsa.go new file mode 100644 index 0000000..5877069 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/elsa.go @@ -0,0 +1,49 @@ +package westworldwithwoman + +// Elsa is the miner's wife, introduced in this section of the book. +// She's built the exact same way Miner is: embed *BaseGameEntity for +// her ID, own a *StateMachine[Elsa] and delegate all state +// bookkeeping to it. This is the payoff of making State[T] and +// StateMachine[T] generic earlier - Elsa reuses both unchanged, +// just with the blank slot filled in as Elsa instead of Miner. +type Elsa struct { + *BaseGameEntity + + stateMachine *StateMachine[Elsa] +} + +// NewElsa sets up Elsa's state machine: DoHousework is her ordinary, +// everyday state, and WifesGlobalState is what checks - every single +// tick, no matter what she's currently doing - whether nature is +// calling. That's exactly why it has to be a global state rather +// than logic inside DoHousework: the book says "Elsa may feel the +// call of nature during any state and at any time," and a global +// state's Execute runs regardless of the current state. +func NewElsa(id int) *Elsa { + e := &Elsa{ + BaseGameEntity: NewBaseGameEntity(id), + } + + e.stateMachine = NewStateMachine(e) + e.stateMachine.SetCurrentState(DoHousework()) + e.stateMachine.SetGlobalState(WifesGlobalState()) + + return e +} + +// FSM returns Elsa's state machine. +func (e *Elsa) FSM() *StateMachine[Elsa] { + return e.stateMachine +} + +// Update satisfies the Entity interface (alongside the ID() method +// borrowed from BaseGameEntity) and just delegates to the state +// machine, same shape as Miner.Update. +func (e *Elsa) Update() { + e.stateMachine.Update() +} + +// ChangeState forwards to the state machine, same as Miner.ChangeState. +func (e *Elsa) ChangeState(newState State[Elsa]) { + e.stateMachine.ChangeState(newState) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/entity.go b/chapter-2-state-driven-agent-design/westworldwithwoman/entity.go new file mode 100644 index 0000000..2741fe1 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/entity.go @@ -0,0 +1,69 @@ +package westworldwithwoman + +// Entity is what every game object in the world must be able to do. +// The C++ code used a pure virtual function (Update()=0) to force +// every subclass to write its own Update. Go has no subclasses, so +// instead we say: "anything with an Update() and an ID() method +// counts as an Entity." A type only satisfies this interface once +// it has its own Update method - just like the C++ version couldn't +// be used until Update was implemented. +type Entity interface { + Update() + ID() int +} + +// nextValidID plays the role of the C++ "static int m_iNextValidID". +// In C++, "static" on a class member means "one shared copy for the +// whole class, not one per object." Go doesn't have that keyword for +// structs, so we just use a normal package-level variable - it's +// shared by every BaseGameEntity for the same reason: there's only +// one of it. +var nextValidID int + +// BaseGameEntity is the Go version of the C++ class BaseGameEntity. +// Go doesn't have "classes" or "inheritance" - instead, other types +// (like a Miner or a Wife later in the book) will embed this struct +// inside themselves to borrow its fields and methods, and then add +// their own Update method on top. +type BaseGameEntity struct { + id int // was "m_ID" - the entity's own unique number +} + +// NewBaseGameEntity replaces the C++ constructor: +// +// BaseGameEntity(int id) { SetID(id); } +// +// Go structs don't run any code automatically when you create them, +// so we write a plain function that builds one correctly. Any code +// that wants a BaseGameEntity should call this instead of building +// the struct by hand. +func NewBaseGameEntity(id int) *BaseGameEntity { + e := &BaseGameEntity{} + e.setID(id) + return e +} + +// setID is the Go version of the private C++ method SetID. It does +// exactly what the comment in the book says: check the requested ID +// isn't lower than the next free one, store it, then move the +// "next free ID" counter past it. +// +// The C++ code used assert() for the check, which only runs in +// debug builds and does nothing in a release build. Go has no +// assert, so we use panic - it always runs, and it stops the +// program immediately if this ever happens, which is exactly what +// a broken invariant like this deserves. +func (e *BaseGameEntity) setID(val int) { + if val < nextValidID { + panic("BaseGameEntity.setID: invalid ID") + } + e.id = val + nextValidID = e.id + 1 +} + +// ID returns the entity's unique number. This is the Go version of +// the C++ "int ID() const { return m_ID; }" - a small read-only +// accessor for the private field. +func (e *BaseGameEntity) ID() int { + return e.id +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/miner.go b/chapter-2-state-driven-agent-design/westworldwithwoman/miner.go new file mode 100644 index 0000000..a3fca36 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/miner.go @@ -0,0 +1,248 @@ +package westworldwithwoman + +// State is the Go version of the C++ class template: +// +// template +// class State +// { +// public: +// virtual void Enter(entity_type*)=0; +// virtual void Execute(entity_type*)=0; +// virtual void Exit(entity_type*)=0; +// virtual ~State(){} +// }; +// +// The book turned State into a template so it isn't stuck being a +// checklist for Miner only - any entity type can use it. Go's +// version of a template is a generic type: the "[T any]" part is a +// blank slot (called a type parameter) that gets filled in with a +// real type wherever State is used. T stands in for the book's +// entity_type. "any" just means "T can be filled in with literally +// any type" - no restrictions. +// +// C++ used "=0" (pure virtual) to say "every real state MUST provide +// these three methods, this class itself is just a checklist." Go's +// interface type does exactly that job: nothing implements State +// automatically - a type only counts as a State[T] once it has all +// three methods with these exact signatures for that particular T. +// +// There's no Go equivalent needed for "virtual ~State(){}" (the +// destructor) - Go has a garbage collector that cleans up memory on +// its own, so there's nothing to write by hand for that. +type State[T any] interface { + // Enter runs once, the moment an entity switches into this state. + Enter(entity *T) + + // Execute runs every single update step while the entity is in + // this state - this is where the actual behavior/logic lives. + Execute(entity *T) + + // Exit runs once, the moment the entity leaves this state. + Exit(entity *T) +} + +// locationType is a temporary stand-in for the C++ enum +// "location_type" (things like shack, goldmine, bank, saloon). The +// book hasn't shown us that enum yet, so this is a plain number type +// for now until you paste the real definition. +type locationType int + +// shack, goldmine, bank and saloon are the four locations the miner +// visits, matching the four states described in the book (we still +// haven't seen the book's actual enum definition, so these are our +// own stand-in values - their exact numbers don't matter, we only +// ever compare locations for equality). +const ( + shack locationType = iota + goldmine + bank + saloon +) + +// Miner is the Go version of: +// +// class Miner : public BaseGameEntity +// +// In C++, "Miner : public BaseGameEntity" means a Miner IS a +// BaseGameEntity - it inherits all of its fields and methods for +// free. Go has no inheritance. Instead we EMBED a *BaseGameEntity +// inside Miner: we stick the smaller struct inside the bigger one, +// and Go automatically lets Miner "borrow" BaseGameEntity's methods +// as if they were its own. That's why Miner gets an ID() method +// without us writing one ourselves - it's borrowed. +type Miner struct { + *BaseGameEntity + + // stateMachine is the Go version of + // "StateMachine* m_pStateMachine". All the bookkeeping + // that used to live directly on Miner (currentState, and the + // exit/enter dance in ChangeState) now belongs to this instead - + // Miner just delegates to it. + stateMachine *StateMachine[Miner] + + // location is where the miner currently is. + location locationType + + // goldCarried: how many gold nuggets the miner has in his pockets. + goldCarried int + + // moneyInBank: how much money the miner has deposited at the bank. + moneyInBank int + + // thirst: the higher this number, the thirstier the miner is. + thirst int + + // fatigue: the higher this number, the more tired the miner is. + fatigue int +} + +// NewMiner is the Go stand-in for the C++ constructor "Miner(int id)". +// C++'s member initializer list (the "m_Location(shack), ..." part +// before the constructor's { }) is just how C++ sets a struct's +// starting field values before the body runs - in Go we set the same +// starting values with an ordinary struct literal instead. +func NewMiner(id int) *Miner { + m := &Miner{ + BaseGameEntity: NewBaseGameEntity(id), + location: shack, + goldCarried: 0, + moneyInBank: 0, + thirst: 0, + fatigue: 0, + } + + // set up state machine + m.stateMachine = NewStateMachine(m) + m.stateMachine.SetCurrentState(GoHomeAndSleepTilRested()) + m.stateMachine.SetGlobalState(MinerGlobalState()) + + return m +} + +// FSM returns the miner's state machine. This is the Go version of +// "GetFSM()const" - Go convention drops the "Get" prefix that +// C++/Java getters use, so it's just FSM(). +func (m *Miner) FSM() *StateMachine[Miner] { + return m.stateMachine +} + +// Location returns where the miner currently is. This is the getter +// the book's Enter/Execute/Exit methods call to check on a miner +// before deciding what to do. +func (m *Miner) Location() locationType { + return m.location +} + +// ChangeLocation moves the miner to a new location. This is the +// setter half of the pair - states call this to actually send a +// miner somewhere. +func (m *Miner) ChangeLocation(loc locationType) { + m.location = loc +} + +// AddToGoldCarried adds amount to how much gold the miner is +// currently carrying in his pockets (separate from what's already +// safely deposited at the bank). +func (m *Miner) AddToGoldCarried(amount int) { + m.goldCarried += amount +} + +// IncreaseFatigue bumps the miner's tiredness up by one notch. +func (m *Miner) IncreaseFatigue() { + m.fatigue++ +} + +// maxNuggets is a placeholder for a constant the book hasn't shown +// us the exact value of yet (something like "const int MaxNuggets"). +// It's how many gold nuggets a miner's pockets can hold before his +// pockets count as "full." Replace this once you paste the real +// value from the book. +const maxNuggets = 3 + +// PocketsFull reports whether the miner is carrying as much gold as +// his pockets can hold. +func (m *Miner) PocketsFull() bool { + return m.goldCarried >= maxNuggets +} + +// thirstThreshold is a placeholder for a constant the book hasn't +// shown us the exact value of yet (it's called something like +// "ComfortLevel" there). It's the point past which the miner's +// thirst counts as "too thirsty." Replace this once you paste the +// real value from the book. +const thirstThreshold = 5 + +// Thirsty reports whether the miner's thirst has crossed the +// threshold where he needs a drink. +func (m *Miner) Thirsty() bool { + return m.thirst > thirstThreshold +} + +// BuyAndDrinkAWhiskey simulates buying and drinking a whiskey: +// thirst drops straight back to zero. We're inventing this method +// ourselves (the book hasn't shown the QuenchThirst state's real +// Execute body to us), based purely on the chapter's written +// description: "he visits the saloon in order to buy a whiskey. When +// his thirst is quenched, ...". Named differently from the +// QuenchThirst state itself (below) to avoid confusing the two. +func (m *Miner) BuyAndDrinkAWhiskey() { + m.thirst = 0 +} + +// TransferGoldToBank moves everything the miner is carrying into his +// bank balance, and empties his pockets back out. +func (m *Miner) TransferGoldToBank() { + m.moneyInBank += m.goldCarried + m.goldCarried = 0 +} + +// comfortableWealth is our own placeholder for how much money in the +// bank counts as "wealthy enough" to head home and rest, per the +// book's description of VisitBankAndDepositGold. The book hasn't +// given us a real number for this, so this is a guess - swap it out +// if you find the book's actual value. +const comfortableWealth = 6 + +// RichEnough reports whether the miner has banked enough money to +// feel comfortable stopping work for a while. +func (m *Miner) RichEnough() bool { + return m.moneyInBank >= comfortableWealth +} + +// DecreaseFatigue lowers the miner's tiredness by one notch, without +// letting it go below zero. +func (m *Miner) DecreaseFatigue() { + if m.fatigue > 0 { + m.fatigue-- + } +} + +// fatigueThreshold is our own placeholder for the "acceptable level" +// of fatigue mentioned in GoHomeAndSleepTilRested's description. The +// book hasn't given us a real number for this either - another +// guess, swap it out once you know the real one. +const fatigueThreshold = 5 + +// Fatigued reports whether the miner is still too tired to go back +// to work. +func (m *Miner) Fatigued() bool { + return m.fatigue > fatigueThreshold +} + +// Update is the real body now - the book finally showed it. Every +// tick the miner gets a little thirstier, then the state machine is +// told to run (which executes the global state, then the current +// state - see StateMachine.Update). +func (m *Miner) Update() { + m.thirst++ + m.stateMachine.Update() +} + +// ChangeState no longer does the exit/enter/swap work itself - that +// job now belongs entirely to StateMachine.ChangeState. Miner just +// passes the request along. This is exactly the cleanup the book +// promised: the agent doesn't manage state transitions by hand +// anymore, it delegates. +func (m *Miner) ChangeState(newState State[Miner]) { + m.stateMachine.ChangeState(newState) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/names.go b/chapter-2-state-driven-agent-design/westworldwithwoman/names.go new file mode 100644 index 0000000..c3c2f27 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/names.go @@ -0,0 +1,21 @@ +package westworldwithwoman + +import "fmt" + +// entityNames is a temporary stand-in for the book's real name +// lookup table. We still haven't been shown that table, so this is +// just a small map we're filling in by hand as we meet entities - +// the miner (ID 1) and now his wife Elsa (ID 2). +var entityNames = map[int]string{ + 1: "Miner Bob", + 2: "Elsa", +} + +// GetNameOfEntity looks up a friendly name for a given entity ID. +// Anything not in entityNames falls back to printing the raw ID. +func GetNameOfEntity(id int) string { + if name, ok := entityNames[id]; ok { + return name + } + return fmt.Sprintf("Entity %d", id) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_do_housework.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_do_housework.go new file mode 100644 index 0000000..36d3f92 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_do_housework.go @@ -0,0 +1,48 @@ +package westworldwithwoman + +import ( + "fmt" + "math/rand" + "sync" +) + +// doHousework is Elsa's everyday default state - the singleton +// pattern is the same one used throughout this package. +type doHousework struct{} + +var ( + doHouseworkInstance *doHousework + doHouseworkOnce sync.Once +) + +// DoHousework is the Go stand-in for "DoHousework::Instance()". +func DoHousework() *doHousework { + doHouseworkOnce.Do(func() { + doHouseworkInstance = &doHousework{} + }) + return doHouseworkInstance +} + +var _ State[Elsa] = (*doHousework)(nil) + +// chores is our own small stand-in for the book's actual chore list +// - it only ever showed two examples in its sample output ("Moppin' +// the floor", "Makin' the bed"), so that's exactly what's reused +// here. One gets picked at random each tick. +var chores = []string{ + "Moppin' the floor", + "Makin' the bed", +} + +// Enter does nothing - the book's sample output never shows a +// message when Elsa starts doing housework, only while she's doing +// it (Execute) and when she leaves the bathroom back into it. +func (s *doHousework) Enter(e *Elsa) {} + +// Execute announces one randomly chosen chore per tick. +func (s *doHousework) Execute(e *Elsa) { + fmt.Printf("\n%s: %s", GetNameOfEntity(e.ID()), chores[rand.Intn(len(chores))]) +} + +// Exit does nothing, for the same reason as Enter. +func (s *doHousework) Exit(e *Elsa) {} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_enter_mine_and_dig_for_nugget.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_enter_mine_and_dig_for_nugget.go new file mode 100644 index 0000000..f1c2136 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_enter_mine_and_dig_for_nugget.go @@ -0,0 +1,82 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// enterMineAndDigForNugget is the Go version of the singleton class +// EnterMineAndDigForNugget from the book. Same trick as the MyClass +// example: the type name is lowercase (unexported), so no code +// outside this package can even write the type's name, let alone +// build one directly. The only legal front door is the +// EnterMineAndDigForNugget() function below. +type enterMineAndDigForNugget struct{} + +var ( + enterMineAndDigForNuggetInstance *enterMineAndDigForNugget + enterMineAndDigForNuggetOnce sync.Once +) + +// EnterMineAndDigForNugget is the Go stand-in for the C++ +// "EnterMineAndDigForNugget::Instance()" static method. No matter +// how many times, or from where, this gets called, it always hands +// back the exact same object - built only the first time it's +// actually needed. sync.Once is Go's tool for "run this exactly +// once, even if many goroutines try at the same time" - it plays +// the same role as C++11's guarantee that a function-local static +// is only ever initialized once. +func EnterMineAndDigForNugget() *enterMineAndDigForNugget { + enterMineAndDigForNuggetOnce.Do(func() { + enterMineAndDigForNuggetInstance = &enterMineAndDigForNugget{} + }) + return enterMineAndDigForNuggetInstance +} + +// compile-time check: this line does nothing at runtime, it just +// makes the compiler verify that *enterMineAndDigForNugget really +// does satisfy State[Miner] (has Enter, Execute and Exit, all taking +// a *Miner). This is the Go version of the book's +// "class EnterMineAndDigForNugget : public State" - filling +// in the template's blank slot with Miner. +var _ State[Miner] = (*enterMineAndDigForNugget)(nil) + +// Enter is the Go version of EnterMineAndDigForNugget::Enter. If the +// miner isn't already at the gold mine, send him walking there. +func (s *enterMineAndDigForNugget) Enter(m *Miner) { + if m.Location() != goldmine { + fmt.Printf("\n%s: Walkin' to the gold mine", GetNameOfEntity(m.ID())) + m.ChangeLocation(goldmine) + } +} + +// Execute is the Go version of EnterMineAndDigForNugget::Execute. +// Every tick spent in this state, the miner picks up one nugget and +// gets a little more tired. If his pockets are full he heads to the +// bank; if he's gotten thirsty along the way he heads to the saloon +// instead. +func (s *enterMineAndDigForNugget) Execute(m *Miner) { + m.AddToGoldCarried(1) + + // diggin' is hard work + m.IncreaseFatigue() + + fmt.Printf("\n%s: Pickin' up a nugget", GetNameOfEntity(m.ID())) + + // if enough gold mined, go and put it in the bank + if m.PocketsFull() { + m.ChangeState(VisitBankAndDepositGold()) + } + + // if thirsty go and get a whiskey + if m.Thirsty() { + m.ChangeState(QuenchThirst()) + } +} + +// Exit is the Go version of EnterMineAndDigForNugget::Exit. It just +// announces that the miner is leaving the mine - all the actual +// state-switching already happened back in Execute. +func (s *enterMineAndDigForNugget) Exit(m *Miner) { + fmt.Printf("\n%s: Ah'm leavin' the gold mine with mah pockets full o' sweet gold", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_go_home_and_sleep_til_rested.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_go_home_and_sleep_til_rested.go new file mode 100644 index 0000000..d24829c --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_go_home_and_sleep_til_rested.go @@ -0,0 +1,59 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// goHomeAndSleepTilRested is the Go version of the (not yet pasted) +// singleton class GoHomeAndSleepTilRested from the book. Same +// pattern as enterMineAndDigForNugget: unexported type, one shared +// instance, reached only through the function below. This is the +// miner's starting state. +type goHomeAndSleepTilRested struct{} + +var ( + goHomeAndSleepTilRestedInstance *goHomeAndSleepTilRested + goHomeAndSleepTilRestedOnce sync.Once +) + +// GoHomeAndSleepTilRested is the Go stand-in for +// "GoHomeAndSleepTilRested::Instance()". +func GoHomeAndSleepTilRested() *goHomeAndSleepTilRested { + goHomeAndSleepTilRestedOnce.Do(func() { + goHomeAndSleepTilRestedInstance = &goHomeAndSleepTilRested{} + }) + return goHomeAndSleepTilRestedInstance +} + +var _ State[Miner] = (*goHomeAndSleepTilRested)(nil) + +// The book hasn't shown us this state's real code, so this is our +// own implementation based purely on its written description: "the +// miner will return to his shack and sleep until his fatigue level +// drops below an acceptable level. He will then change state to +// EnterMineAndDigForNugget." + +// Enter sends the miner home, if he isn't there already. +func (s *goHomeAndSleepTilRested) Enter(m *Miner) { + if m.Location() != shack { + fmt.Printf("\n%s: Headin' on home", GetNameOfEntity(m.ID())) + m.ChangeLocation(shack) + } +} + +// Execute sleeps off a little fatigue each tick, and once the miner +// is rested enough, sends him back to work. +func (s *goHomeAndSleepTilRested) Execute(m *Miner) { + if m.Fatigued() { + fmt.Printf("\n%s: ZZZZ... ", GetNameOfEntity(m.ID())) + m.DecreaseFatigue() + } else { + m.ChangeState(EnterMineAndDigForNugget()) + } +} + +// Exit announces that the miner is up and about again. +func (s *goHomeAndSleepTilRested) Exit(m *Miner) { + fmt.Printf("\n%s: What a great sleep! Time to find more gold", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_machine.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_machine.go new file mode 100644 index 0000000..784eda8 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_machine.go @@ -0,0 +1,131 @@ +package westworldwithwoman + +// StateMachine is the Go version of the C++ class template: +// +// template +// 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 reports whether the agent's current state is the same +// state as the one passed in. The book describes this as comparing +// state *types* (it takes "const State&", not a +// pointer, and presumably checks typeid under the hood) - but every +// concrete state in this codebase is a singleton (there's only ever +// one *doHousework, one *visitBathroom, and so on), so comparing the +// two interface values directly for equality gets the same answer: +// they're equal exactly when they're both that one shared instance +// of the same state type. +func (sm *StateMachine[T]) IsInState(st State[T]) bool { + return sm.currentState == st +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_miner_global_state.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_miner_global_state.go new file mode 100644 index 0000000..5c7d39b --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_miner_global_state.go @@ -0,0 +1,43 @@ +package westworldwithwoman + +import "sync" + +// minerGlobalState is the Go version of the (not yet pasted) +// singleton class MinerGlobalState from the book. Same pattern as +// enterMineAndDigForNugget: unexported type, one shared instance, +// reached only through the function below. This is the state whose +// Execute runs on every single tick, regardless of whatever else +// the miner is currently doing (see StateMachine.Update). +type minerGlobalState struct{} + +var ( + minerGlobalStateInstance *minerGlobalState + minerGlobalStateOnce sync.Once +) + +// MinerGlobalState is the Go stand-in for +// "MinerGlobalState::Instance()". +func MinerGlobalState() *minerGlobalState { + minerGlobalStateOnce.Do(func() { + minerGlobalStateInstance = &minerGlobalState{} + }) + return minerGlobalStateInstance +} + +var _ State[Miner] = (*minerGlobalState)(nil) + +// Enter, Execute and Exit are placeholders - the book hasn't shown +// us this state's real methods yet. This stub exists only so +// NewMiner can set it as the miner's global state and compile. + +func (s *minerGlobalState) Enter(m *Miner) { + // TODO: fill in once the book shows this method's body. +} + +func (s *minerGlobalState) Execute(m *Miner) { + // TODO: fill in once the book shows this method's body. +} + +func (s *minerGlobalState) Exit(m *Miner) { + // TODO: fill in once the book shows this method's body. +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_quench_thirst.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_quench_thirst.go new file mode 100644 index 0000000..77238d5 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_quench_thirst.go @@ -0,0 +1,53 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// quenchThirst is the Go version of the (not yet pasted) singleton +// class QuenchThirst from the book. Same pattern as +// enterMineAndDigForNugget: unexported type, one shared instance, +// reached only through the function below. +type quenchThirst struct{} + +var ( + quenchThirstInstance *quenchThirst + quenchThirstOnce sync.Once +) + +// QuenchThirst is the Go stand-in for "QuenchThirst::Instance()". +func QuenchThirst() *quenchThirst { + quenchThirstOnce.Do(func() { + quenchThirstInstance = &quenchThirst{} + }) + return quenchThirstInstance +} + +var _ State[Miner] = (*quenchThirst)(nil) + +// The book hasn't shown us this state's real code, so this is our +// own implementation based purely on its written description: "he +// visits the saloon in order to buy a whiskey. When his thirst is +// quenched, he changes state to EnterMineAndDigForNugget." + +// Enter sends the miner to the saloon, if he isn't there already. +func (s *quenchThirst) Enter(m *Miner) { + if m.Location() != saloon { + fmt.Printf("\n%s: Headin' to the saloon for a whiskey", GetNameOfEntity(m.ID())) + m.ChangeLocation(saloon) + } +} + +// Execute buys and drinks one whiskey, which is all it takes to +// quench the thirst, then goes straight back to digging. +func (s *quenchThirst) Execute(m *Miner) { + fmt.Printf("\n%s: That's mighty fine sippin' liquor", GetNameOfEntity(m.ID())) + m.BuyAndDrinkAWhiskey() + m.ChangeState(EnterMineAndDigForNugget()) +} + +// Exit announces that the miner is leaving the saloon. +func (s *quenchThirst) Exit(m *Miner) { + fmt.Printf("\n%s: Leavin' the saloon, feelin' good", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_visit_bank_and_deposit_gold.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_visit_bank_and_deposit_gold.go new file mode 100644 index 0000000..3783867 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_visit_bank_and_deposit_gold.go @@ -0,0 +1,61 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// visitBankAndDepositGold is the Go version of the (not yet pasted) +// singleton class VisitBankAndDepositGold from the book. Same +// pattern as enterMineAndDigForNugget: unexported type, one shared +// instance, reached only through the function below. +type visitBankAndDepositGold struct{} + +var ( + visitBankAndDepositGoldInstance *visitBankAndDepositGold + visitBankAndDepositGoldOnce sync.Once +) + +// VisitBankAndDepositGold is the Go stand-in for +// "VisitBankAndDepositGold::Instance()". +func VisitBankAndDepositGold() *visitBankAndDepositGold { + visitBankAndDepositGoldOnce.Do(func() { + visitBankAndDepositGoldInstance = &visitBankAndDepositGold{} + }) + return visitBankAndDepositGoldInstance +} + +var _ State[Miner] = (*visitBankAndDepositGold)(nil) + +// The book hasn't shown us this state's real code, so this is our +// own implementation based purely on its written description: "the +// miner will walk to the bank and deposit any nuggets he is +// carrying. If he then considers himself wealthy enough, he will +// change state to GoHomeAndSleepTilRested. Otherwise he will change +// state to EnterMineAndDigForNugget." + +// Enter sends the miner to the bank, if he isn't there already. +func (s *visitBankAndDepositGold) Enter(m *Miner) { + if m.Location() != bank { + fmt.Printf("\n%s: Goin' to the bank. Yes siree", GetNameOfEntity(m.ID())) + m.ChangeLocation(bank) + } +} + +// Execute deposits whatever gold the miner is carrying, then decides +// where to go next based on how wealthy that leaves him. +func (s *visitBankAndDepositGold) Execute(m *Miner) { + m.TransferGoldToBank() + fmt.Printf("\n%s: Depositing gold. Total savings now: %d", GetNameOfEntity(m.ID()), m.moneyInBank) + + if m.RichEnough() { + m.ChangeState(GoHomeAndSleepTilRested()) + } else { + m.ChangeState(EnterMineAndDigForNugget()) + } +} + +// Exit announces that the miner is leaving the bank. +func (s *visitBankAndDepositGold) Exit(m *Miner) { + fmt.Printf("\n%s: Leavin' the bank", GetNameOfEntity(m.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_visit_bathroom.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_visit_bathroom.go new file mode 100644 index 0000000..e155e48 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_visit_bathroom.go @@ -0,0 +1,47 @@ +package westworldwithwoman + +import ( + "fmt" + "sync" +) + +// visitBathroom is a "blip" state: it interrupts whatever Elsa was +// doing, does its one bit of business, and immediately hands control +// straight back to whatever she was doing before - it never leads +// anywhere new on its own. +type visitBathroom struct{} + +var ( + visitBathroomInstance *visitBathroom + visitBathroomOnce sync.Once +) + +// VisitBathroom is the Go stand-in for "VisitBathroom::Instance()". +func VisitBathroom() *visitBathroom { + visitBathroomOnce.Do(func() { + visitBathroomInstance = &visitBathroom{} + }) + return visitBathroomInstance +} + +var _ State[Elsa] = (*visitBathroom)(nil) + +// Enter announces the trip to the bathroom. +func (s *visitBathroom) Enter(e *Elsa) { + fmt.Printf("\n%s: Walkin' to the can. Need to powda mah pretty li'l nose", GetNameOfEntity(e.ID())) +} + +// Execute is the entire "blip": do the business, then immediately +// revert to whatever state was active before VisitBathroom +// interrupted it. RevertToPreviousState runs the normal ChangeState +// dance (Exit here, Enter there), so nothing extra is needed. +func (s *visitBathroom) Execute(e *Elsa) { + fmt.Printf("\n%s: Ahhhhhh! Sweet relief!", GetNameOfEntity(e.ID())) + e.FSM().RevertToPreviousState() +} + +// Exit announces leaving the bathroom - this runs automatically as +// part of the RevertToPreviousState call above. +func (s *visitBathroom) Exit(e *Elsa) { + fmt.Printf("\n%s: Leavin' the john", GetNameOfEntity(e.ID())) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithwoman/state_wifes_global_state.go b/chapter-2-state-driven-agent-design/westworldwithwoman/state_wifes_global_state.go new file mode 100644 index 0000000..c2050bc --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithwoman/state_wifes_global_state.go @@ -0,0 +1,50 @@ +package westworldwithwoman + +import ( + "math/rand" + "sync" +) + +// wifesGlobalState is Elsa's global state - its Execute runs on +// every single tick of her state machine, in addition to whatever +// her current state is doing (see StateMachine.Update). That's what +// makes it the right place for "does nature call right now" logic: +// the book says that can happen "during any state and at any time," +// which is exactly what a global state is for. +type wifesGlobalState struct{} + +var ( + wifesGlobalStateInstance *wifesGlobalState + wifesGlobalStateOnce sync.Once +) + +// WifesGlobalState is the Go stand-in for +// "WifesGlobalState::Instance()". +func WifesGlobalState() *wifesGlobalState { + wifesGlobalStateOnce.Do(func() { + wifesGlobalStateInstance = &wifesGlobalState{} + }) + return wifesGlobalStateInstance +} + +var _ State[Elsa] = (*wifesGlobalState)(nil) + +// Enter and Exit do nothing - a global state is set once, in +// NewElsa, and never changed, so these never actually get called. +// They only exist to satisfy the State[Elsa] interface. +func (s *wifesGlobalState) Enter(e *Elsa) {} +func (s *wifesGlobalState) Exit(e *Elsa) {} + +// bathroomCallChance is a 1-in-N chance, per the chapter's "1 in 10 +// chance to change to VisitBathroom state." +const bathroomCallChance = 10 + +// Execute rolls the dice every tick. If it comes up, and Elsa isn't +// already in the bathroom (checked with IsInState, so this can't +// interrupt a bathroom trip that's already happening), she breaks +// off into VisitBathroom. +func (s *wifesGlobalState) Execute(e *Elsa) { + if !e.FSM().IsInState(VisitBathroom()) && rand.Intn(bathroomCallChance) == 0 { + e.ChangeState(VisitBathroom()) + } +}