Break a 9-clash pet deadlock by fainting both combatants.

This commit is contained in:
Greyson Parrelli
2026-07-23 08:49:51 -04:00
parent c669ac1453
commit ec68689586
2 changed files with 58 additions and 0 deletions
+33
View File
@@ -440,6 +440,14 @@ func (g *Game) resolveBattle() {
// The exchange loop terminates: every clash kills at least one pet or // The exchange loop terminates: every clash kills at least one pet or
// is a detected stalemate, and summons/auras are finite. The guard is // is a detected stalemate, and summons/auras are finite. The guard is
// just insurance as effects get richer. // just insurance as effects get richer.
//
// clashStreak counts how many times the current pair has clashed without
// either fainting. Some effects (e.g. eat-an-apple-when-hurt) can make two
// pets un-killable, looping forever; if they trade blows deadlockLimit
// times in a row, both drop so the battle can proceed. It resets whenever
// the matchup changes (a fresh pet enters).
const deadlockLimit = 9
clashStreak := 0
for range 10_000 { for range 10_000 {
// Reveal until every side has a pet in play or runs out. Play // Reveal until every side has a pet in play or runs out. Play
// effects queue up and resolve after all reveals (simultaneous). // effects queue up and resolve after all reveals (simultaneous).
@@ -525,6 +533,15 @@ func (g *Game) resolveBattle() {
} }
} }
// A fresh pet on either side is a new matchup, so the deadlock streak
// starts over.
for _, np := range newlyPlayed {
if np {
clashStreak = 0
break
}
}
// Battle over? A side that couldn't field a pet is out; no further // Battle over? A side that couldn't field a pet is out; no further
// effects resolve. // effects resolve.
anyOut := false anyOut := false
@@ -657,9 +674,25 @@ func (g *Game) resolveBattle() {
if dealtB > 0 && ua.hasKnockout() { if dealtB > 0 && ua.hasKnockout() {
ub.Damage = max(ub.Damage, ub.Power()) ub.Damage = max(ub.Damage, ub.Power())
} }
// Deadlock guard: if the pair keeps surviving, count the exchanges and
// force a mutual knockout once they hit the limit.
deadlocked := false
if ua.Alive() && ub.Alive() {
clashStreak++
if clashStreak >= deadlockLimit {
deadlocked = true
ua.Damage = max(ua.Damage, ua.Power())
ub.Damage = max(ub.Damage, ub.Power())
}
} else {
clashStreak = 0
}
clashTxt := fmt.Sprintf("%s's %s and %s's %s trade blows.", clashTxt := fmt.Sprintf("%s's %s and %s's %s trade blows.",
pname(0), ua.Card.Name, pname(1), ub.Card.Name) pname(0), ua.Card.Name, pname(1), ub.Card.Name)
switch da, db := !ua.Alive(), !ub.Alive(); { switch da, db := !ua.Alive(), !ub.Alive(); {
case deadlocked:
clashTxt = fmt.Sprintf("%s's %s and %s's %s are deadlocked after %d clashes — both faint.",
pname(0), ua.Card.Name, pname(1), ub.Card.Name, deadlockLimit)
case da && db: case da && db:
clashTxt += " Both faint." clashTxt += " Both faint."
case da: case da:
+25
View File
@@ -736,3 +736,28 @@ func TestSkunkStripFaintsDebuffedPet(t *testing.T) {
t.Fatalf("skunk should win once the stripped pet faints, got %d", res.WinnerSeat) t.Fatalf("skunk should win once the stripped pet faints, got %d", res.WinnerSeat)
} }
} }
// Two pets that block every hit could clash forever; the deadlock rule makes
// them both faint after 9 clashes so the battle can proceed.
func TestBattleDeadlockBreaksAfterNineClashes(t *testing.T) {
g, _, _ := testGame(t)
wall := func(name string) Card {
return Card{
ID: g.newCardID(), Kind: KindPet, Name: name, Tier: 1, Power: 3, Suit: SuitRed,
Effects: []Effect{{Trigger: TriggerPlay, Action: ActionShieldSelf, Count: 20}},
}
}
res := forceBattle(t, g, []Card{wall("Wall")}, []Card{wall("Fort")})
clashes := eventsOfType(res, "clash")
if len(clashes) != 9 {
t.Fatalf("deadlock should resolve after 9 clashes, got %d", len(clashes))
}
last := clashes[len(clashes)-1]
if !last.Died[0] || !last.Died[1] {
t.Fatalf("the 9th clash should faint both pets: %+v", last)
}
if res.WinnerSeat != -1 {
t.Fatalf("a deadlock is a draw, got winner=%d", res.WinnerSeat)
}
}