diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/clock.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/clock.go new file mode 100644 index 0000000..35fc2aa --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/clock.go @@ -0,0 +1,40 @@ +package westworldwithmessaging + +import ( + "sync" + "time" +) + +// clockSingleton is our own stand-in for whatever timer/clock utility +// class the book uses elsewhere (it only shows up here as +// "Clock->GetCurrentTime()" - the book hasn't shown us that class's +// actual source, likely a separate timer file). All DispatchMessage +// and DispatchDelayedMessages actually need is a steadily increasing +// number of seconds, so this just measures time elapsed since the +// clock was first used. +type clockSingleton struct { + start time.Time +} + +var ( + clockInstance *clockSingleton + clockOnce sync.Once +) + +func newClock() *clockSingleton { + clockOnce.Do(func() { + clockInstance = &clockSingleton{start: time.Now()} + }) + return clockInstance +} + +// Clock is a shorthand for the clock singleton, same "#define"-style +// convenience pattern as EntityMgr and Dispatch. +var Clock = newClock() + +// GetCurrentTime returns the number of seconds elapsed since the +// clock started, matching the book's +// "double CurrentTime = Clock->GetCurrentTime();". +func (c *clockSingleton) GetCurrentTime() float64 { + return time.Since(c.start).Seconds() +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go index 5877069..a85ccda 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging // 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 @@ -28,6 +28,10 @@ func NewElsa(id int) *Elsa { e.stateMachine.SetCurrentState(DoHousework()) e.stateMachine.SetGlobalState(WifesGlobalState()) + // Register with the entity manager automatically, same reasoning + // as NewMiner - one less manual step for every caller to remember. + EntityMgr.RegisterEntity(e) + return e } @@ -47,3 +51,9 @@ func (e *Elsa) Update() { func (e *Elsa) ChangeState(newState State[Elsa]) { e.stateMachine.ChangeState(newState) } + +// HandleMessage forwards to the state machine, same as +// Miner.HandleMessage. +func (e *Elsa) HandleMessage(msg Telegram) bool { + return e.stateMachine.HandleMessage(msg) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/entity.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/entity.go index 2741fe1..40c3991 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/entity.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/entity.go @@ -1,15 +1,21 @@ -package westworldwithwoman +package westworldwithmessaging // 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. +// The C++ code used pure virtual functions (Update()=0, and now +// HandleMessage()=0) to force every subclass to write its own +// versions. Go has no subclasses, so instead we say: "anything with +// Update(), ID() and HandleMessage() methods counts as an Entity." A +// type only satisfies this interface once it has all three - just +// like the C++ version couldn't be used until both were implemented. type Entity interface { Update() ID() int + + // HandleMessage lets an entity respond to a Telegram sent to it + // by the message dispatcher. It returns true if the message was + // actually handled, false otherwise - callers use that to decide + // whether to try routing the message somewhere else instead. + HandleMessage(msg Telegram) bool } // nextValidID plays the role of the C++ "static int m_iNextValidID". diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/entity_manager.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/entity_manager.go new file mode 100644 index 0000000..4f261bc --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/entity_manager.go @@ -0,0 +1,70 @@ +package westworldwithmessaging + +import "sync" + +// entityManager is the Go version of the singleton class +// EntityManager. Same singleton pattern used throughout this +// package: an unexported type, one shared instance, reached only +// through the EntityManager() function below. +// +// The C++ version stores "BaseGameEntity*" pointers in its map. In +// C++, a BaseGameEntity* pointing at an actual Miner still calls +// Miner's real Update() (that's what "virtual" functions do - even +// through a base-class pointer, the real subclass's version runs). +// Go doesn't have virtual functions or base-class pointers, but our +// Entity interface (from entity.go: Update() and ID()) gives us the +// exact same behavior - a variable of type Entity holding a *Miner +// still calls *Miner's own Update(). So Entity is what we store here +// instead of *BaseGameEntity, to get the same real-subclass-runs +// behavior C++ gets from virtual dispatch. +type entityManager struct { + // entityMap cross-references entities by their ID number, same + // as the book's std::map m_EntityMap. (The + // book's own comment above that field actually describes a + // std::vector instead - a leftover mismatch in the book's text + // itself. We're matching the real field, the map.) + entityMap map[int]Entity +} + +var ( + entityManagerInstance *entityManager + entityManagerOnce sync.Once +) + +// EntityManager is the Go stand-in for "EntityManager::Instance()". +func EntityManager() *entityManager { + entityManagerOnce.Do(func() { + entityManagerInstance = &entityManager{ + entityMap: make(map[int]Entity), + } + }) + return entityManagerInstance +} + +// EntityMgr is a shorthand for EntityManager(), matching the book's +// "#define EntityMgr EntityManager::Instance()" macro. Go has no +// preprocessor and no macros, so instead this is just a plain +// package-level variable, set up once when the package is loaded - +// EntityMgr.RegisterEntity(...) reads the same as the book's +// EntityMgr->RegisterEntity(...), just with a dot instead of an +// arrow (Go doesn't distinguish "." for values from "->" for +// pointers the way C++ does - it's always "."). +var EntityMgr = EntityManager() + +// RegisterEntity stores an entity so it can be looked up later by +// its ID. +func (m *entityManager) RegisterEntity(newEntity Entity) { + m.entityMap[newEntity.ID()] = newEntity +} + +// GetEntityFromID returns the entity with the given ID, or nil if +// none is registered under that ID (same as the C++ version +// implicitly returning nullptr for a lookup miss). +func (m *entityManager) GetEntityFromID(id int) Entity { + return m.entityMap[id] +} + +// RemoveEntity removes an entity from the registry. +func (m *entityManager) RemoveEntity(entity Entity) { + delete(m.entityMap, entity.ID()) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go new file mode 100644 index 0000000..e1f4401 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go @@ -0,0 +1,158 @@ +package westworldwithmessaging + +import ( + "sort" + "sync" +) + +// messageDispatcher is the Go version of the singleton class +// MessageDispatcher. Same singleton pattern as everything else in +// this package: unexported type, one shared instance, reached only +// through the MessageDispatcher() function below. +type messageDispatcher struct { + // priorityQueue holds telegrams that haven't gone out yet because + // they were sent with a delay, mirroring the book's + // "std::set PriorityQ". A std::set keeps its contents + // sorted (here, by DispatchTime) and automatically drops + // duplicates. Go has no built-in sorted-set container, so we + // reproduce both properties by hand: insertIntoPriorityQueue + // (below) always keeps this slice sorted by DispatchTime, and + // skips inserting anything that looks like a near-duplicate of + // something already queued (see telegramsMatch in telegram.go). + // index 0 is always the next telegram due to go out. + priorityQueue []Telegram +} + +var ( + messageDispatcherInstance *messageDispatcher + messageDispatcherOnce sync.Once +) + +// MessageDispatcher is the Go stand-in for +// "MessageDispatcher::Instance()". +func MessageDispatcher() *messageDispatcher { + messageDispatcherOnce.Do(func() { + messageDispatcherInstance = &messageDispatcher{} + }) + return messageDispatcherInstance +} + +// Dispatch is a shorthand for MessageDispatcher(), matching the +// book's "#define Dispatch MessageDispatcher::Instance()" macro - +// same trick as EntityMgr earlier: Go has no preprocessor, so this +// is a plain package-level variable instead, set up once when the +// package loads. Dispatch.DispatchMessage(...) reads the same as the +// book's Dispatch->DispatchMessage(...). +var Dispatch = MessageDispatcher() + +// discharge is the Go version of the private C++ method Discharge. +// It's what actually hands a telegram to its receiving entity - both +// DispatchMessage (for immediate messages) and +// DispatchDelayedMessages (once a delayed one comes due) call this +// to do the real delivery. The book never pasted this method's body +// directly, but we now have everything it needs: Entity.HandleMessage +// (added this round), which is exactly "the message handling method +// of the receiving entity" the book described back when DispatchMessage +// was introduced. +// +// One thing we added that the book's description didn't mention: a +// nil check. GetEntityFromID returns nil for an unregistered ID, and +// calling a method on a nil Entity would panic - the book's C++ +// equivalent (calling through a null BaseGameEntity*) would crash +// the same way, so this guard is a real improvement, not just a +// style choice. +func (d *messageDispatcher) discharge(receiver Entity, msg Telegram) { + if receiver == nil { + return + } + receiver.HandleMessage(msg) +} + +// insertIntoPriorityQueue is our Go stand-in for +// "PriorityQ.insert(telegram)". A C++ std::set does two things on +// insert: it keeps everything sorted, and it silently refuses to add +// something that's already there (per whatever == means for the +// contained type). We reproduce both by hand here: skip the insert +// entirely if an equivalent telegram (telegramsMatch, see +// telegram.go) is already queued, otherwise insert it at the correct +// sorted position by DispatchTime. +func (d *messageDispatcher) insertIntoPriorityQueue(t Telegram) { + for _, existing := range d.priorityQueue { + if telegramsMatch(existing, t) { + return + } + } + + i := sort.Search(len(d.priorityQueue), func(i int) bool { + return d.priorityQueue[i].DispatchTime >= t.DispatchTime + }) + d.priorityQueue = append(d.priorityQueue, Telegram{}) + copy(d.priorityQueue[i+1:], d.priorityQueue[i:]) + d.priorityQueue[i] = t +} + +// DispatchMessage sends a message to another entity, either right +// now (delay <= 0) or at some point in the future (delay > 0, added +// to the current time to get the telegram's DispatchTime). +func (d *messageDispatcher) DispatchMessage(delay float64, sender int, receiver int, msg int, extraInfo any) { + // get a pointer to the receiver of the message + pReceiver := EntityMgr.GetEntityFromID(receiver) + + // create the telegram + telegram := Telegram{ + Sender: sender, + Receiver: receiver, + Msg: msg, + DispatchTime: 0, + ExtraInfo: extraInfo, + } + + // if there is no delay, route the telegram immediately + if delay <= 0.0 { + // send the telegram to the recipient + d.discharge(pReceiver, telegram) + } else { + // calculate the time when the telegram should be dispatched + currentTime := Clock.GetCurrentTime() + telegram.DispatchTime = currentTime + delay + + // and put it in the queue + d.insertIntoPriorityQueue(telegram) + } +} + +// DispatchDelayedMessages is meant to be called once per pass through +// the main game loop: it checks the front of priorityQueue and +// discharges any telegram whose DispatchTime has arrived. +// +// One difference from the book's version worth flagging: the C++ +// code checks PriorityQ.begin()->DispatchTime without first checking +// whether PriorityQ is empty - calling begin() on an empty std::set +// and dereferencing it is undefined behavior in C++. Indexing an +// empty Go slice would panic the same way, so we added the +// len(d.priorityQueue) > 0 check ourselves to actually be safe, +// rather than translate that bug faithfully. +func (d *messageDispatcher) DispatchDelayedMessages() { + // first get current time + currentTime := Clock.GetCurrentTime() + + // now peek at the queue to see if any telegrams need dispatching. + // remove all telegrams from the front of the queue that have gone + // past their sell-by date + for len(d.priorityQueue) > 0 && + d.priorityQueue[0].DispatchTime < currentTime && + d.priorityQueue[0].DispatchTime > 0 { + + // read the telegram from the front of the queue + telegram := d.priorityQueue[0] + + // find the recipient + pReceiver := EntityMgr.GetEntityFromID(telegram.Receiver) + + // send the telegram to the recipient + d.discharge(pReceiver, telegram) + + // and remove it from the queue + d.priorityQueue = d.priorityQueue[1:] + } +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/message_types.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_types.go new file mode 100644 index 0000000..de030e1 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_types.go @@ -0,0 +1,24 @@ +package westworldwithmessaging + +// MessageType is the Go version of the C++ enum: +// +// enum message_type +// { +// Msg_HiHoneyImHome, +// Msg_StewReady +// }; +// +// from the book's "MessageTypes.h". Same trick used for locationType +// back in the westworld package: a named int type plus a block of +// auto-numbered constants. +type MessageType int + +// MsgHiHoneyImHome and MsgStewReady are the two message types the +// book has shown us so far - more get added here as the book +// introduces them. (Go naming convention drops underscores from +// constant names, hence "MsgHiHoneyImHome" instead of the C++ +// "Msg_HiHoneyImHome".) +const ( + MsgHiHoneyImHome MessageType = iota + MsgStewReady +) diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/miner.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/miner.go index a3fca36..dbcabbc 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/miner.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/miner.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging // State is the Go version of the C++ class template: // @@ -39,6 +39,14 @@ type State[T any] interface { // 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 @@ -116,6 +124,13 @@ func NewMiner(id int) *Miner { 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 } @@ -246,3 +261,11 @@ func (m *Miner) Update() { 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) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go index c3c2f27..440f8b5 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import "fmt" diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_do_housework.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_do_housework.go index 36d3f92..525e9bc 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_do_housework.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_do_housework.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import ( "fmt" @@ -46,3 +46,12 @@ func (s *doHousework) Execute(e *Elsa) { // Exit does nothing, for the same reason as Enter. func (s *doHousework) Exit(e *Elsa) {} + +// OnMessage is required now that State[T] includes it, so that every +// state satisfies the interface. This state doesn't do anything +// special with messages yet - false means "I didn't handle it," +// which lets StateMachine.HandleMessage fall through and try the +// global state instead. +func (s *doHousework) OnMessage(e *Elsa, msg Telegram) bool { + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_enter_mine_and_dig_for_nugget.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_enter_mine_and_dig_for_nugget.go index f1c2136..90025ba 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_enter_mine_and_dig_for_nugget.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_enter_mine_and_dig_for_nugget.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import ( "fmt" @@ -80,3 +80,10 @@ func (s *enterMineAndDigForNugget) Execute(m *Miner) { 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())) } + +// OnMessage is required now that State[T] includes it. This state +// doesn't handle any messages yet - false ("not handled") is the +// correct default until the book shows otherwise. +func (s *enterMineAndDigForNugget) OnMessage(m *Miner, msg Telegram) bool { + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_go_home_and_sleep_til_rested.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_go_home_and_sleep_til_rested.go index d24829c..899f8dc 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_go_home_and_sleep_til_rested.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_go_home_and_sleep_til_rested.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import ( "fmt" @@ -57,3 +57,12 @@ func (s *goHomeAndSleepTilRested) Execute(m *Miner) { func (s *goHomeAndSleepTilRested) Exit(m *Miner) { fmt.Printf("\n%s: What a great sleep! Time to find more gold", GetNameOfEntity(m.ID())) } + +// OnMessage is required now that State[T] includes it. This state +// doesn't handle any messages yet (the book hasn't shown us that a +// sleeping miner reacts to anything, like a "stew's ready" message, +// though that seems like a likely candidate later), so false ("not +// handled") is the correct default for now. +func (s *goHomeAndSleepTilRested) OnMessage(m *Miner, msg Telegram) bool { + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_machine.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_machine.go index 784eda8..8eb82de 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_machine.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_machine.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging // StateMachine is the Go version of the C++ class template: // @@ -129,3 +129,25 @@ func (sm *StateMachine[T]) PreviousState() State[T] { func (sm *StateMachine[T]) IsInState(st State[T]) bool { return sm.currentState == st } + +// HandleMessage is the Go version of StateMachine::HandleMessage. A +// telegram is offered to the current state first; if that state +// doesn't have code in place to deal with this particular message +// (OnMessage returns false), it's offered to the global state +// instead. The C++ version was marked const - same as Update, Go has +// no equivalent keyword and doesn't need one. +func (sm *StateMachine[T]) HandleMessage(msg Telegram) bool { + // first see if the current state is valid and that it can handle + // the message + if sm.currentState != nil && sm.currentState.OnMessage(sm.owner, msg) { + return true + } + + // if not, and if a global state has been implemented, send + // the message to the global state + if sm.globalState != nil && sm.globalState.OnMessage(sm.owner, msg) { + return true + } + + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_miner_global_state.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_miner_global_state.go index 5c7d39b..c4dec4f 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_miner_global_state.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_miner_global_state.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import "sync" @@ -41,3 +41,11 @@ func (s *minerGlobalState) Execute(m *Miner) { func (s *minerGlobalState) Exit(m *Miner) { // TODO: fill in once the book shows this method's body. } + +// OnMessage is required now that State[T] includes it. This state +// doesn't handle any messages yet, so false ("not handled") is the +// correct default until the book shows otherwise. +func (s *minerGlobalState) OnMessage(m *Miner, msg Telegram) bool { + // TODO: fill in once the book shows this method's body. + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_quench_thirst.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_quench_thirst.go index 77238d5..5ef504c 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_quench_thirst.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_quench_thirst.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import ( "fmt" @@ -51,3 +51,10 @@ func (s *quenchThirst) Execute(m *Miner) { func (s *quenchThirst) Exit(m *Miner) { fmt.Printf("\n%s: Leavin' the saloon, feelin' good", GetNameOfEntity(m.ID())) } + +// OnMessage is required now that State[T] includes it. This state +// doesn't handle any messages yet - false ("not handled") is the +// correct default until the book shows otherwise. +func (s *quenchThirst) OnMessage(m *Miner, msg Telegram) bool { + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bank_and_deposit_gold.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bank_and_deposit_gold.go index 3783867..098ba48 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bank_and_deposit_gold.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bank_and_deposit_gold.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import ( "fmt" @@ -59,3 +59,10 @@ func (s *visitBankAndDepositGold) Execute(m *Miner) { func (s *visitBankAndDepositGold) Exit(m *Miner) { fmt.Printf("\n%s: Leavin' the bank", GetNameOfEntity(m.ID())) } + +// OnMessage is required now that State[T] includes it. This state +// doesn't handle any messages yet - false ("not handled") is the +// correct default until the book shows otherwise. +func (s *visitBankAndDepositGold) OnMessage(m *Miner, msg Telegram) bool { + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bathroom.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bathroom.go index e155e48..112e2d7 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bathroom.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_visit_bathroom.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import ( "fmt" @@ -45,3 +45,10 @@ func (s *visitBathroom) Execute(e *Elsa) { func (s *visitBathroom) Exit(e *Elsa) { fmt.Printf("\n%s: Leavin' the john", GetNameOfEntity(e.ID())) } + +// OnMessage is required now that State[T] includes it. This state +// doesn't handle any messages yet - false ("not handled") is the +// correct default until the book shows otherwise. +func (s *visitBathroom) OnMessage(e *Elsa, msg Telegram) bool { + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_wifes_global_state.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_wifes_global_state.go index c2050bc..537053b 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_wifes_global_state.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_wifes_global_state.go @@ -1,4 +1,4 @@ -package westworldwithwoman +package westworldwithmessaging import ( "math/rand" @@ -48,3 +48,14 @@ func (s *wifesGlobalState) Execute(e *Elsa) { e.ChangeState(VisitBathroom()) } } + +// OnMessage is required now that State[T] includes it. This state +// doesn't handle any messages yet - false ("not handled") is the +// correct default until the book shows otherwise. (Note: our Elsa +// only has DoHousework and VisitBathroom, not a cooking state, so +// the book's stew-related messages between Miner and Elsa may not +// map cleanly onto this simplified version - worth revisiting if +// that comes up.) +func (s *wifesGlobalState) OnMessage(e *Elsa, msg Telegram) bool { + return false +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/telegram.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/telegram.go new file mode 100644 index 0000000..cc24388 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/telegram.go @@ -0,0 +1,68 @@ +package westworldwithmessaging + +import "math" + +// Telegram is the Go version of the C++ struct: +// +// struct Telegram +// { +// int Sender; +// int Receiver; +// int Msg; +// double DispatchTime; +// void* ExtraInfo; +// }; +// +// It's one message passed between entities through the dispatcher / +// entity manager we'll build next: who sent it, who it's for, what +// it says, when it should actually go out, and any extra data riding +// along with it. The book's "/* CONSTRUCTORS OMITTED */" means we +// haven't been shown how one gets built yet, so this is just the +// plain data shape for now, no constructor. +type Telegram struct { + // Sender is the ID of the entity that sent this telegram. + Sender int + + // Receiver is the ID of the entity meant to receive this telegram. + Receiver int + + // Msg is the message itself. The book keeps this as a plain int + // rather than the MessageType enum, presumably so the dispatcher + // can stay generic and doesn't need to know about every possible + // message type - we're matching that choice exactly rather than + // tightening it to MessageType ourselves. + Msg int + + // DispatchTime supports delayed messages: instead of going out + // immediately, a telegram can be stamped with the time it should + // actually be delivered. C++'s "double" becomes Go's float64 - + // Go has no bare "double" keyword, float64 is its 64-bit + // floating-point type. + DispatchTime float64 + + // ExtraInfo carries any additional data that should travel with + // the message. C++'s "void*" (a pointer with no fixed type - + // "could point to anything") becomes Go's "any" (an alias for + // interface{} - "could hold a value of any type"). Whoever reads + // ExtraInfo later type-asserts it back to something concrete. + ExtraInfo any +} + +// smallestDelay is our stand-in for the book's "a quarter of a +// second" tolerance - it doesn't give us an exact named constant for +// this, just that written description. +const smallestDelay = 0.25 + +// telegramsMatch is our own Go version of the behavior the book +// describes for Telegram.h's overloaded == operator (we haven't been +// shown that file's actual code, only this description): two +// telegrams count as the same message if they're headed the same +// direction with the same content, and their dispatch times are less +// than smallestDelay apart. This is what keeps a burst of near- +// identical messages from flooding an entity's queue. +func telegramsMatch(a, b Telegram) bool { + return a.Sender == b.Sender && + a.Receiver == b.Receiver && + a.Msg == b.Msg && + math.Abs(a.DispatchTime-b.DispatchTime) < smallestDelay +}