diff options
Diffstat (limited to 'internal/game/sys_escape_test.go')
| -rw-r--r-- | internal/game/sys_escape_test.go | 225 |
1 files changed, 214 insertions, 11 deletions
diff --git a/internal/game/sys_escape_test.go b/internal/game/sys_escape_test.go index f07701c..f8cd2e1 100644 --- a/internal/game/sys_escape_test.go +++ b/internal/game/sys_escape_test.go @@ -18,9 +18,12 @@ type escapeTestConn struct { } func (c *escapeTestConn) ReadMessage() (string, error) { return "", nil } -func (c *escapeTestConn) Write(b []byte) (int, error) { c.data = append(c.data, b...); return len(b), nil } -func (c *escapeTestConn) Close() error { return nil } -func (c *escapeTestConn) SetEcho(bool) error { return nil } +func (c *escapeTestConn) Write(b []byte) (int, error) { + c.data = append(c.data, b...) + return len(b), nil +} +func (c *escapeTestConn) Close() error { return nil } +func (c *escapeTestConn) SetEcho(bool) error { return nil } func escapeTestGame() *Game { tmpDir, err := os.MkdirTemp("", "escape_test") @@ -125,14 +128,18 @@ func TestBeginEscapeMoveWithCapeOfAgility(t *testing.T) { p := player.New("cape") p.Equipment[item.SlotBack] = capeOfAgilityID - // moveTicks returns 0 for Cape of Agility, which triggers the instant - // completeMove path in beginEscapeMove. The sys_energy_test.go tests - // already cover moveTicks returning 0 for Cape. This test verifies - // the call site doesn't regress. - ticks := g.moveTicks(p, false) + // moveTicks returns 0 for Cape of Agility. beginEscapeMove is only ever + // called when an escape has been stashed, and Cape-of-Agility / GodMode + // never enter the stash path (doMove's ticks<=0 short-circuit fires + // first), so this test just verifies the moveTicks call site compiles + // against the two-value signature. + ticks, usesEnergy := g.moveTicks(p, false) if ticks != 0 { t.Errorf("Cape of Agility should give 0 ticks, got %d", ticks) } + if usesEnergy { + t.Error("Cape of Agility should not use energy") + } } func TestApplyMobHitCancelsEscape(t *testing.T) { @@ -240,14 +247,210 @@ func TestEscapeClearedOnStop(t *testing.T) { } } -func TestPlayerActionDisplayShowsPreparingToFlee(t *testing.T) { +// TestPlayerActionDisplayNoPreparingToFlee verifies the (previously-unreachable) +// "preparing to flee" display branch has been removed. While waiting to flee +// the player is still in combat, so playerActionDisplay's in-combat branch +// governs (returning "fighting a X" — exercised indirectly through combat +// tests). A pure EscapeDir set with no combat engagement now yields "". +func TestPlayerActionDisplayNoPreparingToFlee(t *testing.T) { g := escapeTestGame() p := player.New("fleer") p.EscapeDir = "north" display := g.playerActionDisplay(p) - if display != "preparing to flee" { - t.Errorf("expected 'preparing to flee', got %q", display) + if display == "preparing to flee" { + t.Errorf("the stale 'preparing to flee' branch should be removed; got %q", display) + } +} + +// TestEscapeReliefFiresOnThirdHit verifies the soft-lock relief: the third +// consecutive escape-canceling mob hit in the same combat triggers the flee +// as if the mob had missed, instead of cancelling. +func TestEscapeReliefFiresOnThirdHit(t *testing.T) { + g := escapeTestGame() + p := player.New("relief") + p.HP = 100 + p.EscapeDir = "north" + p.EscapeTarget = 11 + + mob := &world.MobInstance{ + InstanceID: "relief_mob", + DefID: "relief_def", + Name: "a goblin", + } + g.Combat.Enter(p.Name, "relief_mob") + sess := escapeTestSession(p) + + // Three escape-canceling hits. Between hits the test re-arms the escape + // (in the real flow the player re-issues a direction). maxHit=1 makes + // RollDamage deterministic (always 1) so HP=100 survives cleanly. + for i := 0; i < 3; i++ { + p.EscapeDir = "north" + p.EscapeTarget = 11 + g.applyMobHit(sess, p, mob, "slash", 1) + } + + if p.EscapeFailCount != 0 { + t.Errorf("after relief fired, EscapeFailCount should reset, got %d", p.EscapeFailCount) + } + if p.EscapeDir != "" { + t.Errorf("relief should have cleared EscapeDir, got %q", p.EscapeDir) + } + if p.MoveDirection != "north" { + t.Errorf("relief should set MoveDirection=north, got %q", p.MoveDirection) + } + if p.MoveTicks == 0 { + t.Error("relief should set MoveTicks > 0 to actually flee") + } +} + +// TestEscapeReliefDoesNotFireBeforeThirdHit verifies the relief waits until +// the 3rd hit; the 1st and 2nd should still cancel normally. +func TestEscapeReliefDoesNotFireBeforeThirdHit(t *testing.T) { + g := escapeTestGame() + p := player.New("patient") + p.HP = 100 + p.EscapeDir = "north" + p.EscapeTarget = 11 + + mob := &world.MobInstance{ + InstanceID: "relief_mob2", + DefID: "relief_def2", + Name: "a goblin", + } + g.Combat.Enter(p.Name, "relief_mob2") + sess := escapeTestSession(p) + + // First hit: cancels normally. + g.applyMobHit(sess, p, mob, "slash", 1) + if p.EscapeFailCount != 1 { + t.Errorf("after 1st hit, EscapeFailCount=%d want 1", p.EscapeFailCount) + } + if p.EscapeDir != "" { + t.Error("1st hit should cancel escape (ClearEscape)") + } + if p.MoveTicks != 0 { + t.Error("1st hit should not start movement") + } + + // Second hit: cancels again. + p.EscapeDir = "north" + p.EscapeTarget = 11 + g.applyMobHit(sess, p, mob, "slash", 1) + if p.EscapeFailCount != 2 { + t.Errorf("after 2nd hit, EscapeFailCount=%d want 2", p.EscapeFailCount) + } + if p.EscapeDir != "" { + t.Error("2nd hit should cancel escape (ClearEscape)") + } + if p.MoveTicks != 0 { + t.Error("2nd hit should not start movement") + } +} + +// TestEscapeFailCountResetsOnSuccessfulMissFlee verifies the counter resets +// after a successful flee path (miss) — so the next flee attempt in the same +// combat starts fresh at 0. +func TestEscapeFailCountResetsOnSuccessfulMissFlee(t *testing.T) { + g := escapeTestGame() + p := player.New("reclucky") + p.HP = 100 + + g.Combat.Enter(p.Name, "relief_mob3") + mob := &world.MobInstance{ + InstanceID: "relief_mob3", + DefID: "relief_def3", + Name: "a goblin", + } + + sess := escapeTestSession(p) + + // Two escape-canceling hits. + p.EscapeDir = "north" + p.EscapeTarget = 11 + g.applyMobHit(sess, p, mob, "slash", 1) + p.EscapeDir = "north" + p.EscapeTarget = 11 + g.applyMobHit(sess, p, mob, "slash", 1) + if p.EscapeFailCount != 2 { + t.Fatalf("expected EscapeFailCount=2 after 2 hits, got %d", p.EscapeFailCount) + } + + // Then a miss triggers a successful flee — counter must reset. + p.EscapeDir = "north" + p.EscapeTarget = 11 + g.applyMobMiss(sess, p, mob) + if p.EscapeFailCount != 0 { + t.Errorf("EscapeFailCount should reset on successful miss-flee, got %d", p.EscapeFailCount) + } + if p.MoveTicks == 0 { + t.Error("miss should have fired beginEscapeMove (MoveTicks > 0)") + } +} + +// TestEscapeFailCountResetsOnCombatEntry verifies the counter resets when a +// new combat starts (startCombat), so a fresh engagement begins at 0. +func TestEscapeFailCountResetsOnCombatEntry(t *testing.T) { + g := escapeTestGame() + p := player.New("reset") + p.HP = 100 + p.EscapeFailCount = 7 // carryover from prior engagement + + mob := &world.MobInstance{ + InstanceID: "fresh_mob", + DefID: "fresh_def", + Name: "a goblin", + HP: 10, + } + sess := escapeTestSession(p) + g.startCombat(sess, p, mob) + + if p.EscapeFailCount != 0 { + t.Errorf("startCombat should reset EscapeFailCount, got %d", p.EscapeFailCount) + } +} + +// TestEscapeReliefSuppressedOnKillingBlow verifies that when the 3rd hit +// would simultaneously kill the player (HP drops to 0), the relief is +// suppressed — the player dies cleanly (the live mob-attack tick subscriber +// notices HP<=0 and calls endCombat → killPlayer) rather than seeing a +// misleading "power through" message. +// +// We exercise applyMobHit directly (no subscriber), so killPlayer is not +// invoked here; we instead verify that the relief branch did NOT fire +// (MoveTicks stays 0). +func TestEscapeReliefSuppressedOnKillingBlow(t *testing.T) { + g := escapeTestGame() + p := player.New("doomed") + p.HP = 3 // exactly enough to absorb three maxHit==1 (deterministic) hits + + mob := &world.MobInstance{ + InstanceID: "lethal_mob", + DefID: "lethal_def", + Name: "a goblin", + } + g.Combat.Enter(p.Name, "lethal_mob") + sess := escapeTestSession(p) + + // maxHit=1 → RollDamage returns exactly 1 each call; deterministic. + for i := 0; i < 2; i++ { + p.EscapeDir = "north" + p.EscapeTarget = 11 + g.applyMobHit(sess, p, mob, "slash", 1) + } + // Before the 3rd hit: HP=1. After damage: HP=0 → relief must NOT fire. + p.EscapeDir = "north" + p.EscapeTarget = 11 + g.applyMobHit(sess, p, mob, "slash", 1) + + if p.EscapeDir != "" { + t.Error("killing blow should still ClearEscape (EscapeDir)") + } + if p.MoveTicks != 0 { + t.Error("killing blow should NOT start movement (relief must be suppressed)") + } + if p.EscapeFailCount != 3 { + t.Errorf("EscapeFailCount after killing blow should be 3, got %d", p.EscapeFailCount) } } |
