package westworldwithmessaging // 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) // OnMessage runs when the entity receives a Telegram while this // state is active (current or global). It returns true if this // state actually dealt with the message, false if it doesn't // care about that kind of message - StateMachine.HandleMessage // uses that return value to decide whether to also try routing // the message to the global state. OnMessage(entity *T, msg Telegram) bool } // 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()) // The book registers an entity as a separate step at the call // site ("Miner* Bob = new Miner(...); EntityMgr->RegisterEntity(Bob);"). // We're folding that into the constructor instead, so every miner // is automatically registered and reachable by ID the moment it // exists - nobody can forget the second step. EntityMgr.RegisterEntity(m) 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) } // HandleMessage is the Go version of Miner::HandleMessage - it does // nothing itself, just routes the telegram straight to the state // machine, which knows how to try the current state first and the // global state second. func (m *Miner) HandleMessage(msg Telegram) bool { return m.stateMachine.HandleMessage(msg) }