feat: westworldwithwoman. add elsa
This commit is contained in:
1 parent
2764849943
commit
0e7482e04c
28 files changed
+1946
No files matched your search
File renamed without changes.
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package westworldwithwoman
|
||||||
|
|
||||||
|
// State is the Go version of the C++ class template:
|
||||||
|
//
|
||||||
|
// template <class entity_type>
|
||||||
|
// 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<Miner>* 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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) {}
|
||||||
+82
@@ -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<Miner>" - 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()))
|
||||||
|
}
|
||||||
+59
@@ -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()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package westworldwithwoman
|
||||||
|
|
||||||
|
// 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 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<entity_type>&", 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
|
||||||
|
}
|
||||||
+43
@@ -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.
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
|
}
|
||||||
+61
@@ -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()))
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
|
}
|
||||||
+50
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package westworldwithwoman
|
||||||
|
|
||||||
|
// State is the Go version of the C++ class template:
|
||||||
|
//
|
||||||
|
// template <class entity_type>
|
||||||
|
// 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<Miner>* 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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) {}
|
||||||
+82
@@ -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<Miner>" - 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()))
|
||||||
|
}
|
||||||
+59
@@ -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()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package westworldwithwoman
|
||||||
|
|
||||||
|
// 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 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<entity_type>&", 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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
|
}
|
||||||
+61
@@ -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()))
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user