diff --git a/chapter-2-state-driven-agent-design/cmd/westworldwithmessaging/main.go b/chapter-2-state-driven-agent-design/cmd/westworldwithmessaging/main.go new file mode 100644 index 0000000..d6fa96e --- /dev/null +++ b/chapter-2-state-driven-agent-design/cmd/westworldwithmessaging/main.go @@ -0,0 +1,37 @@ +// Command westworldwithmessaging runs the full messaging demo: Miner +// Bob and Elsa tick forward together, and each iteration also flushes +// any delayed telegrams that have come due (Dispatch.DispatchDelayedMessages). +// +// Our Clock measures real wall-clock time, same as the book's, and +// Elsa's CookStew reminder uses a real 1.5 second delay - so this +// loop sleeps a little each tick and runs for several seconds of +// real time, giving that delayed message an actual chance to fire +// before the program ends. A tight, sleep-free loop would finish in +// microseconds and the stew would never get made. +package main + +import ( + "fmt" + "time" + + "git.jjstudio.me/jonas/Programming-Game-AI-by-Example-Go/chapter-2-state-driven-agent-design/westworldwithmessaging" +) + +func main() { + bob := westworldwithmessaging.NewMiner(1) + elsa := westworldwithmessaging.NewElsa(2) + + const ( + numUpdateSteps = 40 + tickDelay = 100 * time.Millisecond + ) + + for range numUpdateSteps { + westworldwithmessaging.Dispatch.DispatchDelayedMessages() + bob.Update() + elsa.Update() + time.Sleep(tickDelay) + } + + fmt.Println() +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go index a85ccda..46d98a0 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/elsa.go @@ -10,6 +10,11 @@ type Elsa struct { *BaseGameEntity stateMachine *StateMachine[Elsa] + + // cooking tracks whether there's already a stew in the oven, so + // CookStew.Enter doesn't put a second one in or send a second + // reminder message to herself. + cooking bool } // NewElsa sets up Elsa's state machine: DoHousework is her ordinary, @@ -40,6 +45,16 @@ func (e *Elsa) FSM() *StateMachine[Elsa] { return e.stateMachine } +// Cooking reports whether there's already a stew in the oven. +func (e *Elsa) Cooking() bool { + return e.cooking +} + +// SetCooking updates whether there's a stew in the oven. +func (e *Elsa) SetCooking(v bool) { + e.cooking = v +} + // Update satisfies the Entity interface (alongside the ID() method // borrowed from BaseGameEntity) and just delegates to the state // machine, same shape as Miner.Update. diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go index e1f4401..facab5b 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_dispatcher.go @@ -1,10 +1,21 @@ package westworldwithmessaging import ( + "fmt" "sort" "sync" ) +// SendMsgImmediately and NoAdditionalInfo are the Go versions of the +// book's "SEND_MSG_IMMEDIATELY" and "NO_ADDITIONAL_INFO" - both +// defined as 0 in the book's MessageDispatcher.h purely so call +// sites read clearly instead of showing a bare 0/NULL. NoAdditionalInfo +// is nil rather than 0, since Go's nil is what an empty "any" (our +// ExtraInfo field's type) actually looks like. +const SendMsgImmediately = 0.0 + +var NoAdditionalInfo any = nil + // 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 @@ -109,6 +120,9 @@ func (d *messageDispatcher) DispatchMessage(delay float64, sender int, receiver // if there is no delay, route the telegram immediately if delay <= 0.0 { + fmt.Printf("\nInstant telegram dispatched at time: %.5f by %s for %s. Msg is %s", + Clock.GetCurrentTime(), GetNameOfEntity(sender), GetNameOfEntity(receiver), GetNameOfMessage(MessageType(msg))) + // send the telegram to the recipient d.discharge(pReceiver, telegram) } else { @@ -118,6 +132,9 @@ func (d *messageDispatcher) DispatchMessage(delay float64, sender int, receiver // and put it in the queue d.insertIntoPriorityQueue(telegram) + + fmt.Printf("\nDelayed telegram from %s recorded at time %.5f for %s. Msg is %s", + GetNameOfEntity(sender), currentTime, GetNameOfEntity(receiver), GetNameOfMessage(MessageType(msg))) } } @@ -146,6 +163,9 @@ func (d *messageDispatcher) DispatchDelayedMessages() { // read the telegram from the front of the queue telegram := d.priorityQueue[0] + fmt.Printf("\nQueued telegram ready for dispatch: Sent to %s. Msg is %s", + GetNameOfEntity(telegram.Receiver), GetNameOfMessage(MessageType(telegram.Msg))) + // find the recipient pReceiver := EntityMgr.GetEntityFromID(telegram.Receiver) diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/message_types.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_types.go index de030e1..a1ff8ab 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/message_types.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/message_types.go @@ -1,5 +1,7 @@ package westworldwithmessaging +import "fmt" + // MessageType is the Go version of the C++ enum: // // enum message_type @@ -22,3 +24,24 @@ const ( MsgHiHoneyImHome MessageType = iota MsgStewReady ) + +// messageNames is our own stand-in for however the book actually +// renders a message type in its dispatcher log lines (the sample +// output shows names like "HiHoneyImHome" and "StewReady" - the enum +// constant with the "Msg_" prefix dropped). We haven't been shown +// the book's real helper for this, so this recreates exactly that +// naming. +var messageNames = map[MessageType]string{ + MsgHiHoneyImHome: "HiHoneyImHome", + MsgStewReady: "StewReady", +} + +// GetNameOfMessage looks up a friendly name for a message type, for +// use in log output. Anything not in messageNames falls back to +// printing its raw number. +func GetNameOfMessage(msg MessageType) string { + if name, ok := messageNames[msg]; ok { + return name + } + return fmt.Sprintf("Msg %d", int(msg)) +} diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go index 440f8b5..4c52ace 100644 --- a/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/names.go @@ -2,13 +2,23 @@ package westworldwithmessaging import "fmt" +// entMinerBob and entElsa are our own stand-ins for the book's +// enumerated entity IDs (referenced as "ent_Miner_Bob" and "ent_Elsa" +// - presumably from an enum in a header we haven't been shown). Since +// this demo only ever has these two fixed characters, plain named +// constants do the same job: giving call sites a readable name +// instead of a bare number. +const ( + entMinerBob = 1 + entElsa = 2 +) + // 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). +// just a small map we're filling in by hand as we meet entities. var entityNames = map[int]string{ - 1: "Miner Bob", - 2: "Elsa", + entMinerBob: "Miner Bob", + entElsa: "Elsa", } // GetNameOfEntity looks up a friendly name for a given entity ID. diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_cook_stew.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_cook_stew.go new file mode 100644 index 0000000..06a5d31 --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_cook_stew.go @@ -0,0 +1,90 @@ +package westworldwithmessaging + +import ( + "fmt" + "sync" +) + +// cookStew is the Go version of the (not yet pasted) singleton class +// CookStew from the book. Same pattern as every other state: +// unexported type, one shared instance, reached only through the +// function below. +type cookStew struct{} + +var ( + cookStewInstance *cookStew + cookStewOnce sync.Once +) + +// CookStew is the Go stand-in for "CookStew::Instance()". +func CookStew() *cookStew { + cookStewOnce.Do(func() { + cookStewInstance = &cookStew{} + }) + return cookStewInstance +} + +var _ State[Elsa] = (*cookStew)(nil) + +// Execute and Exit weren't pasted to us directly either, but a +// sample run of the book's program makes their behavior clear +// enough to fill in: Execute prints "Fussin' over food" once per +// tick while the stew is in the oven (waiting for the delayed +// Msg_StewReady reminder to arrive), and Exit announces taking the +// stew out and putting it on the table, right before switching back +// to DoHousework. + +// Enter puts the stew in the oven (if one isn't already cooking) and +// sends a delayed message to herself, reminding her to take it out +// once it's ready. +func (s *cookStew) Enter(e *Elsa) { + if !e.Cooking() { + fmt.Printf("\n%s: Puttin' the stew in the oven", GetNameOfEntity(e.ID())) + + // send a delayed message to myself so that I know when to take + // the stew out of the oven + Dispatch.DispatchMessage( + 1.5, // time delay + e.ID(), // sender ID + e.ID(), // receiver ID + int(MsgStewReady), // the message + NoAdditionalInfo, // no extra info attached + ) + e.SetCooking(true) + } +} + +func (s *cookStew) Execute(e *Elsa) { + fmt.Printf("\n%s: Fussin' over food", GetNameOfEntity(e.ID())) +} + +func (s *cookStew) Exit(e *Elsa) { + fmt.Printf("\n%s: Puttin' the stew on the table", GetNameOfEntity(e.ID())) +} + +// OnMessage handles the delayed reminder Enter sent to herself: once +// the stew's ready, take it out, let Bob know dinner's on the table, +// and go back to the ordinary housework state. +func (s *cookStew) OnMessage(e *Elsa, msg Telegram) bool { + switch MessageType(msg.Msg) { + case MsgStewReady: + fmt.Printf("\nMessage received by %s at time: %.5f", GetNameOfEntity(e.ID()), Clock.GetCurrentTime()) + fmt.Printf("\n%s: Stew ready! Let's eat", GetNameOfEntity(e.ID())) + + // let hubby know the stew is ready + Dispatch.DispatchMessage( + SendMsgImmediately, + e.ID(), + entMinerBob, + int(MsgStewReady), + NoAdditionalInfo, + ) + + e.SetCooking(false) + e.FSM().ChangeState(DoHousework()) + + return true + } + + return false +} 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 525e9bc..141f1df 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 @@ -26,25 +26,33 @@ func DoHousework() *doHousework { 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. +// - its sample output shows three examples ("Moppin' the floor", +// "Makin' the bed", "Washin' the dishes"), so that's exactly what's +// reused here. One gets picked at random each tick. var chores = []string{ "Moppin' the floor", "Makin' the bed", + "Washin' the dishes", } -// 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) {} +// Enter announces that Elsa is back to her ordinary routine. A +// sample run of the book's program shows this line printed right +// after she finishes cooking and switches back to this state - an +// earlier, more abridged sample output we saw didn't show this line +// after a bathroom-break revert, but that sample was explicitly +// described as just "a sample," not a complete transcript, so this +// message firing every time Enter runs (bathroom revert included) +// isn't a contradiction. +func (s *doHousework) Enter(e *Elsa) { + fmt.Printf("\n%s: Time to do some more housework!", GetNameOfEntity(e.ID())) +} // 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. +// Exit does nothing - no sample output ever shows a message here. func (s *doHousework) Exit(e *Elsa) {} // OnMessage is required now that State[T] includes it, so that every diff --git a/chapter-2-state-driven-agent-design/westworldwithmessaging/state_eat_stew.go b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_eat_stew.go new file mode 100644 index 0000000..5ca112c --- /dev/null +++ b/chapter-2-state-driven-agent-design/westworldwithmessaging/state_eat_stew.go @@ -0,0 +1,62 @@ +package westworldwithmessaging + +import ( + "fmt" + "sync" +) + +// eatStew is the Go version of the (not yet pasted) singleton class +// EatStew from the book. Same pattern as every other state: +// unexported type, one shared instance, reached only through the +// function below. +type eatStew struct{} + +var ( + eatStewInstance *eatStew + eatStewOnce sync.Once +) + +// EatStew is the Go stand-in for "EatStew::Instance()". +func EatStew() *eatStew { + eatStewOnce.Do(func() { + eatStewInstance = &eatStew{} + }) + return eatStewInstance +} + +var _ State[Miner] = (*eatStew)(nil) + +// The book never pasted this state's actual source - it only ever +// referenced EatStew::Instance() as a switch target. This whole +// implementation is reverse-engineered from a sample run of the +// finished program, which shows this exact sequence: Bob enters +// (one line), eats (one line, then immediately heads back to +// whatever he was doing - a single-tick state, same shape as +// QuenchThirst), and leaving prints one more line before he's back +// in GoHomeAndSleepTilRested. + +// Enter announces that dinner smells good. +func (s *eatStew) Enter(m *Miner) { + fmt.Printf("\n%s: Smells reaaal goood, Elsa!", GetNameOfEntity(m.ID())) +} + +// Execute is the entire meal: announce it, then head back to +// whatever state he was in before dinner interrupted him. The sample +// output never shows more than one "Tastes real good too!" line, so +// - like QuenchThirst - this doesn't loop across multiple ticks. +func (s *eatStew) Execute(m *Miner) { + fmt.Printf("\n%s: Tastes real good too!", GetNameOfEntity(m.ID())) + m.ChangeState(GoHomeAndSleepTilRested()) +} + +// Exit thanks Elsa for the meal on the way out. +func (s *eatStew) Exit(m *Miner) { + fmt.Printf("\n%s: Thank ya li'l lady. Ah better get back to whatever ah wuz doin'", GetNameOfEntity(m.ID())) +} + +// OnMessage is required now that State[T] includes it. The sample +// output doesn't show Bob reacting to any message while eating, so +// false ("not handled") is the correct default. +func (s *eatStew) 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 899f8dc..1f7aa2e 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 @@ -28,41 +28,64 @@ func GoHomeAndSleepTilRested() *goHomeAndSleepTilRested { 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. +// Enter sends the miner home, if he isn't there already, and lets +// Elsa know he's arrived by dispatching a message to her. func (s *goHomeAndSleepTilRested) Enter(m *Miner) { if m.Location() != shack { - fmt.Printf("\n%s: Headin' on home", GetNameOfEntity(m.ID())) + fmt.Printf("\n%s: Walkin' home", GetNameOfEntity(m.ID())) m.ChangeLocation(shack) + + // let the wife know I'm home + Dispatch.DispatchMessage( + SendMsgImmediately, // time delay + m.ID(), // ID of sender + entElsa, // ID/name of recipient + int(MsgHiHoneyImHome), // the message + NoAdditionalInfo, // no extra info attached + ) } } // Execute sleeps off a little fatigue each tick, and once the miner -// is rested enough, sends him back to work. +// is rested enough, announces it and sends him back to work. A +// sample run of the book's program shows this exact message +// ("All mah fatigue has drained away...") printed right before the +// switch to digging - and no message at all when this state is +// instead exited early via a Msg_StewReady interruption (see +// OnMessage below) - which is why this line lives here in Execute's +// else-branch rather than in Exit: Exit runs either way, but this +// text is specific to waking up rested, not to leaving for any +// reason. func (s *goHomeAndSleepTilRested) Execute(m *Miner) { if m.Fatigued() { fmt.Printf("\n%s: ZZZZ... ", GetNameOfEntity(m.ID())) m.DecreaseFatigue() } else { + fmt.Printf("\n%s: All mah fatigue has drained away. Time to find more gold!", GetNameOfEntity(m.ID())) 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())) -} +// Exit does nothing - a sample run of the book's program shows no +// message printed here, whether the miner leaves because he's +// rested or because dinner's ready. +func (s *goHomeAndSleepTilRested) Exit(m *Miner) {} -// 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. +// OnMessage is what makes Bob only respond to Elsa's dinner call +// while he's actually home to hear it: this state only exists while +// he's in the shack, so putting the Msg_StewReady handling here +// (rather than in MinerGlobalState) is exactly how the book +// guarantees "he will only respond to this message if he is in the +// GoHomeAndSleepTilRested state." func (s *goHomeAndSleepTilRested) OnMessage(m *Miner, msg Telegram) bool { + switch MessageType(msg.Msg) { + case MsgStewReady: + fmt.Printf("\nMessage handled by %s at time: %.5f", GetNameOfEntity(m.ID()), Clock.GetCurrentTime()) + fmt.Printf("\n%s: Okay hun, ahm a-comin'!", GetNameOfEntity(m.ID())) + m.ChangeState(EatStew()) + return true + } + + // send message to global message handler 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 537053b..5b7518b 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,6 +1,7 @@ package westworldwithmessaging import ( + "fmt" "math/rand" "sync" ) @@ -49,13 +50,19 @@ func (s *wifesGlobalState) Execute(e *Elsa) { } } -// 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.) +// OnMessage handles messages that should get through no matter what +// Elsa is currently doing - the book puts Msg_HiHoneyImHome here +// specifically because she should react to it "when in any state," +// same reasoning as why the bathroom-call check above lives in the +// global state rather than in DoHousework. func (s *wifesGlobalState) OnMessage(e *Elsa, msg Telegram) bool { + switch MessageType(msg.Msg) { + case MsgHiHoneyImHome: + fmt.Printf("\nMessage handled by %s at time: %.5f", GetNameOfEntity(e.ID()), Clock.GetCurrentTime()) + fmt.Printf("\n%s: Hi honey. Let me make you some of mah fine country stew", GetNameOfEntity(e.ID())) + e.FSM().ChangeState(CookStew()) + return true + } + return false }