From 4bc1daaf259e0c19c5fe4a12ec8bc7f6e5a735c1 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 5 Oct 2026 20:00:02 -0400 Subject: feat: terminal talk action, typed pai gow poker phase --- internal/game/correct_audit_fixes_test.go | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) (limited to 'internal/game/correct_audit_fixes_test.go') diff --git a/internal/game/correct_audit_fixes_test.go b/internal/game/correct_audit_fixes_test.go index c32b1e5..03facc0 100644 --- a/internal/game/correct_audit_fixes_test.go +++ b/internal/game/correct_audit_fixes_test.go @@ -166,6 +166,100 @@ func TestGatherYieldsDrop(t *testing.T) { }) } +// End-to-end burn: the missing coverage the audit called out. Start a fire +// with a tinderbox and tick until it catches. +func TestBurnCreatesFire(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + p := h.seedCharacter("alice", "alice") + writeSeedFile(t, filepath.Join(h.dir, "items", "logs.yaml"), + []byte("id: logs\nname: logs\nburn_ticks: 100\nfire_level: 1\nfire_xp: 10\n")) + writeSeedFile(t, filepath.Join(h.dir, "items", "tinderbox.yaml"), + []byte("id: tinderbox\nname: tinderbox\ntool:\n type: fire\n speed: 1\n")) + sess := &net.Session{Conn: newChanConn(), Player: p} + sess.SetState(net.StateGame) + h.g.RunOnOwner(func() { + h.g.ItemStore.Reload(h.dir) + p.SetInvSlot(0, &player.InventorySlot{ItemID: "logs", Quantity: 5}) + p.SetInvSlot(1, &player.InventorySlot{ItemID: "tinderbox", Quantity: 1}) + h.g.startBurn(sess, p, "logs", false) + if p.Action == nil || p.Action.Type != behavior.TypeBurn { + t.Fatalf("burn action did not start: %s", sess.Conn.(*chanConn).Bytes()) + } + // Each attempt is two calls (phase 1 then the roll); 200 calls is ~100 + // attempts at >=5% success, so failure is effectively impossible. + for i := 0; i < 200 && p.Action != nil; i++ { + h.g.advanceBurn(sess, p) + } + if p.Action != nil { + t.Fatal("burn never completed") + } + if !h.g.hasFireObject(1) { + t.Fatalf("no fire created (output: %s)", sess.Conn.(*chanConn).Bytes()) + } + }) +} + +// Regression: a terminal talk node (messages, no options, no goto) must clear +// the talk action, or it lingers as a stale action that suppresses the prompt +// and no-ops every tick. +func TestTerminalTalkNodeClearsAction(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + p := h.seedCharacter("alice", "alice") + writeSeedFile(t, filepath.Join(h.dir, "objects", "statue.yaml"), + []byte("id: statue\nname: statue\ntalk:\n nodes:\n start:\n messages: [The statue says nothing more.]\n")) + sess := &net.Session{Conn: newChanConn(), Player: p} + sess.SetState(net.StateGame) + h.g.RunOnOwner(func() { + h.g.ObjectStore.Reload(h.dir) + obj, err := h.g.ObjectStore.Load("statue") + if err != nil { + t.Fatal(err) + } + h.g.startTalk(sess, p, obj) + if p.Action != nil { + t.Fatalf("terminal talk node left a stale action: %+v", p.Action) + } + if sess.State() != net.StateGame { + t.Fatalf("state = %v, want StateGame", sess.State()) + } + }) +} + +// End-to-end object steal: the missing coverage the audit called out. +func TestObjectStealYieldsLoot(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + p := h.seedCharacter("alice", "alice") + writeSeedFile(t, filepath.Join(h.dir, "objects", "market_stall.yaml"), + []byte("id: market_stall\nname: market stall\nsteal:\n level: 1\n xp: 10\n speed: 1\n drops:\n - item_id: stolen_coins\n weight: 100\n")) + writeSeedFile(t, filepath.Join(h.dir, "items", "stolen_coins.yaml"), + []byte("id: stolen_coins\nname: stolen coins\nstackable: true\n")) + sess := &net.Session{Conn: newChanConn(), Player: p} + sess.SetState(net.StateGame) + h.g.RunOnOwner(func() { + h.g.ItemStore.Reload(h.dir) + h.g.ObjectStore.Reload(h.dir) + h.g.World.EnsureObjectStates(1, []string{"market_stall"}) + obj, err := h.g.ObjectStore.Load("market_stall") + if err != nil { + t.Fatal(err) + } + h.g.startSteal(sess, p, "object", nil, obj) + if p.Action == nil || p.Action.Type != behavior.TypeSteal { + t.Fatalf("steal action did not start: %s", sess.Conn.(*chanConn).Bytes()) + } + // ~50% success per attempt; 200 attempts makes failure negligible. + for i := 0; i < 200 && p.CountItem("stolen_coins") == 0; i++ { + h.g.advanceSteal(sess, p) + } + if got := p.CountItem("stolen_coins"); got == 0 { + t.Fatalf("steal yielded no loot (output: %s)", sess.Conn.(*chanConn).Bytes()) + } + }) +} + func TestPlayerDataFileNameCanonicalizesPrefixes(t *testing.T) { for _, in := range []string{ "players/characters/alice.yaml", -- cgit v1.2.3