feat: westworldwithmessaging. add entitymanager, telegram, messagehandlers, delayedmessages, expand baseentity
This commit is contained in:
1 parent
0e7482e04c
commit
129dd5d8b8
18 files changed
+505
-19
No files matched your search
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
// Elsa is the miner's wife, introduced in this section of the book.
|
// 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
|
// 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.SetCurrentState(DoHousework())
|
||||||
e.stateMachine.SetGlobalState(WifesGlobalState())
|
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
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,3 +51,9 @@ func (e *Elsa) Update() {
|
|||||||
func (e *Elsa) ChangeState(newState State[Elsa]) {
|
func (e *Elsa) ChangeState(newState State[Elsa]) {
|
||||||
e.stateMachine.ChangeState(newState)
|
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)
|
||||||
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
// Entity is what every game object in the world must be able to do.
|
// 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
|
// The C++ code used pure virtual functions (Update()=0, and now
|
||||||
// every subclass to write its own Update. Go has no subclasses, so
|
// HandleMessage()=0) to force every subclass to write its own
|
||||||
// instead we say: "anything with an Update() and an ID() method
|
// versions. Go has no subclasses, so instead we say: "anything with
|
||||||
// counts as an Entity." A type only satisfies this interface once
|
// Update(), ID() and HandleMessage() methods counts as an Entity." A
|
||||||
// it has its own Update method - just like the C++ version couldn't
|
// type only satisfies this interface once it has all three - just
|
||||||
// be used until Update was implemented.
|
// like the C++ version couldn't be used until both were implemented.
|
||||||
type Entity interface {
|
type Entity interface {
|
||||||
Update()
|
Update()
|
||||||
ID() int
|
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".
|
// nextValidID plays the role of the C++ "static int m_iNextValidID".
|
||||||
|
|||||||
@@ -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<int, BaseGameEntity*> 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())
|
||||||
|
}
|
||||||
@@ -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<Telegram> 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:]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
// State is the Go version of the C++ class template:
|
// 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 runs once, the moment the entity leaves this state.
|
||||||
Exit(entity *T)
|
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
|
// 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.SetCurrentState(GoHomeAndSleepTilRested())
|
||||||
m.stateMachine.SetGlobalState(MinerGlobalState())
|
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
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,3 +261,11 @@ func (m *Miner) Update() {
|
|||||||
func (m *Miner) ChangeState(newState State[Miner]) {
|
func (m *Miner) ChangeState(newState State[Miner]) {
|
||||||
m.stateMachine.ChangeState(newState)
|
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)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -46,3 +46,12 @@ func (s *doHousework) Execute(e *Elsa) {
|
|||||||
|
|
||||||
// Exit does nothing, for the same reason as Enter.
|
// Exit does nothing, for the same reason as Enter.
|
||||||
func (s *doHousework) Exit(e *Elsa) {}
|
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
|
||||||
|
}
|
||||||
+8
-1
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -80,3 +80,10 @@ func (s *enterMineAndDigForNugget) Execute(m *Miner) {
|
|||||||
func (s *enterMineAndDigForNugget) Exit(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()))
|
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
|
||||||
|
}
|
||||||
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -57,3 +57,12 @@ func (s *goHomeAndSleepTilRested) Execute(m *Miner) {
|
|||||||
func (s *goHomeAndSleepTilRested) Exit(m *Miner) {
|
func (s *goHomeAndSleepTilRested) Exit(m *Miner) {
|
||||||
fmt.Printf("\n%s: What a great sleep! Time to find more gold", GetNameOfEntity(m.ID()))
|
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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
// StateMachine is the Go version of the C++ class template:
|
// 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 {
|
func (sm *StateMachine[T]) IsInState(st State[T]) bool {
|
||||||
return sm.currentState == st
|
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
|
||||||
|
}
|
||||||
+9
-1
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
@@ -41,3 +41,11 @@ func (s *minerGlobalState) Execute(m *Miner) {
|
|||||||
func (s *minerGlobalState) Exit(m *Miner) {
|
func (s *minerGlobalState) Exit(m *Miner) {
|
||||||
// TODO: fill in once the book shows this method's body.
|
// 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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -51,3 +51,10 @@ func (s *quenchThirst) Execute(m *Miner) {
|
|||||||
func (s *quenchThirst) Exit(m *Miner) {
|
func (s *quenchThirst) Exit(m *Miner) {
|
||||||
fmt.Printf("\n%s: Leavin' the saloon, feelin' good", GetNameOfEntity(m.ID()))
|
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
|
||||||
|
}
|
||||||
+8
-1
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -59,3 +59,10 @@ func (s *visitBankAndDepositGold) Execute(m *Miner) {
|
|||||||
func (s *visitBankAndDepositGold) Exit(m *Miner) {
|
func (s *visitBankAndDepositGold) Exit(m *Miner) {
|
||||||
fmt.Printf("\n%s: Leavin' the bank", GetNameOfEntity(m.ID()))
|
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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -45,3 +45,10 @@ func (s *visitBathroom) Execute(e *Elsa) {
|
|||||||
func (s *visitBathroom) Exit(e *Elsa) {
|
func (s *visitBathroom) Exit(e *Elsa) {
|
||||||
fmt.Printf("\n%s: Leavin' the john", GetNameOfEntity(e.ID()))
|
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
|
||||||
|
}
|
||||||
+12
-1
@@ -1,4 +1,4 @@
|
|||||||
package westworldwithwoman
|
package westworldwithmessaging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -48,3 +48,14 @@ func (s *wifesGlobalState) Execute(e *Elsa) {
|
|||||||
e.ChangeState(VisitBathroom())
|
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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in new issue
Block a user