package game import ( "slices" "testing" ) // testGame builds a started 2-player game without going through the lobby. func testGame(t *testing.T) (*Game, *Player, *Player) { t.Helper() g := New() p1, err := g.AddPlayer("Alice") if err != nil { t.Fatal(err) } p2, err := g.AddPlayer("Bob") if err != nil { t.Fatal(err) } if g.Phase != PhaseShop { t.Fatalf("expected shop phase after both players join, got %s", g.Phase) } // Pin the (otherwise random) priority token to seat 0 so tests are // deterministic: seat 0 shops first and wins battle simultaneity races. g.PrioritySeat = p1.Seat g.Turn = p1.Seat return g, p1, p2 } // pet mints a plain effect-less pet for battle scenarios. func (g *Game) pet(name string, power int) Card { return Card{ID: g.newCardID(), Kind: KindPet, Name: name, Tier: 1, Power: power, Suit: SuitRed} } // realPet mints a copy of a real pet (with its effects) by name, searching // all tiers. func (g *Game) realPet(t *testing.T, name string) Card { t.Helper() for tierIdx, tier := range petTiers { for _, tmpl := range tier { if tmpl.Name == name { return Card{ ID: g.newCardID(), Kind: KindPet, Name: tmpl.Name, Tier: tierIdx + 1, Power: tmpl.Power, Suit: tmpl.Suits[0], Effects: tmpl.Effects, EffectText: tmpl.EffectText, } } } } t.Fatalf("no pet named %s", name) return Card{} } // tier1 is kept as a shorthand for realPet. func (g *Game) tier1(t *testing.T, name string) Card { return g.realPet(t, name) } // realFood mints a shop food card (Honey, Garlic, ...) from its template. func (g *Game) realFood(t *testing.T, name string) Card { t.Helper() for tierIdx, tier := range foodTiers { for _, f := range tier { if f.Name == name { return Card{ ID: g.newCardID(), Kind: KindFood, Name: f.Name, Tier: tierIdx + 1, Food: f.Food, Perk: f.Perk, Effects: f.Effects, EffectText: f.EffectText, } } } } t.Fatalf("no food named %s", name) return Card{} } func (g *Game) newHoney(t *testing.T) Card { return g.realFood(t, "Honey") } // forceBattle sets both decks, arranges them in current order, and resolves. func forceBattle(t *testing.T, g *Game, d1, d2 []Card) *BattleResult { t.Helper() g.Players[0].Deck = d1 g.Players[1].Deck = d2 g.Phase = PhaseArrange g.Players[0].Ready = false g.Players[1].Ready = false for _, p := range g.Players { ids := make([]string, len(p.Deck)) for i, c := range p.Deck { ids[i] = c.ID } if err := g.SubmitOrder(p.ID, ids); err != nil { t.Fatal(err) } } if g.Phase != PhaseBattle { t.Fatalf("expected battle phase, got %s", g.Phase) } return g.Battle } func eventsOfType(res *BattleResult, typ string) []BattleEvent { var out []BattleEvent for _, ev := range res.Events { if ev.Type == typ { out = append(out, ev) } } return out } // The worked example from the rules: a 3-power pet fights a 5-power pet. The // 3 dies, the 5 survives with 3 damage markers (2 health left, 5 attack). // Then a 2-power pet trades with it: both die. func TestBattleDamageMarkers(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.pet("Three", 3), g.pet("Two", 2)}, []Card{g.pet("Five", 5)}, ) clashes := eventsOfType(res, "clash") if len(clashes) != 2 { t.Fatalf("expected 2 clashes, got %d", len(clashes)) } first := clashes[0] if !first.Died[0] || first.Died[1] { t.Fatalf("first clash: 3-power should die, 5-power should survive: %+v", first) } if first.Damage[1] != 3 { t.Fatalf("5-power pet should carry 3 damage, has %d", first.Damage[1]) } second := clashes[1] if !second.Died[0] || !second.Died[1] { t.Fatalf("second clash: both should die (5 attack kills the 2; 3+2 damage kills the 5): %+v", second) } if res.WinnerSeat != -1 { t.Fatalf("battle should be a draw, winner=%d", res.WinnerSeat) } if g.Players[0].Trophies != 0 || g.Players[1].Trophies != 0 { t.Fatal("no trophies on a draw") } } func TestBattleEqualPowerBothDie(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.pet("A", 4)}, []Card{g.pet("B", 4)}, ) clashes := eventsOfType(res, "clash") if len(clashes) != 1 || !clashes[0].Died[0] || !clashes[0].Died[1] { t.Fatalf("equal power pets should both die: %+v", clashes) } if res.WinnerSeat != -1 { t.Fatal("expected a draw") } } func TestBattleWinnerGetsTrophy(t *testing.T) { g, _, p2 := testGame(t) res := forceBattle(t, g, []Card{g.pet("Small", 1)}, []Card{g.pet("Big", 5)}, ) if res.WinnerSeat != 1 { t.Fatalf("seat 1 should win, got %d", res.WinnerSeat) } if res.Trophies != 1 || p2.Trophies != 1 { t.Fatalf("round 1 win should award 1 trophy, got %d/%d", res.Trophies, p2.Trophies) } } func TestBattleFinalRoundWorthTwoTrophies(t *testing.T) { g, _, p2 := testGame(t) g.Round = MaxRounds res := forceBattle(t, g, []Card{g.pet("Small", 1)}, []Card{g.pet("Big", 5)}, ) if res.Trophies != 2 || p2.Trophies != 2 { t.Fatalf("final round win should award 2 trophies, got %d/%d", res.Trophies, p2.Trophies) } } // Foods attach to the next pet revealed beneath them; each apple adds 1 // power. 3+2 apples = 5 power draws with a plain 5. func TestBattleApplesBuffNextPet(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.newApple(), g.newApple(), g.pet("Buffed", 3)}, []Card{g.pet("Enemy", 5)}, ) clash := eventsOfType(res, "clash")[0] if !clash.Died[0] || !clash.Died[1] { t.Fatalf("5 vs 3+2apples should kill both: %+v", clash) } if res.WinnerSeat != -1 { t.Fatal("expected draw") } } // A side whose stack holds only foods can't field a pet and loses without a // single clash. func TestBattleFoodOnlyLoses(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.pet("Solo", 1)}, []Card{g.newApple()}, ) if len(eventsOfType(res, "clash")) != 0 { t.Fatal("expected no clashes") } if res.WinnerSeat != 0 { t.Fatalf("seat 0 should win by default, got %d", res.WinnerSeat) } } // Ant's faint puts an apple on top of its owner's stack, buffing the next // pet revealed: Ant(1)+Follower(2) beats a plain 2. func TestAntFaintAddsApple(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.tier1(t, "Ant"), g.pet("Follower", 2)}, []Card{g.pet("Enemy", 2)}, ) summons := eventsOfType(res, "summon") if len(summons) != 1 || summons[0].Seat != 0 || summons[0].Card.Food != FoodApple { t.Fatalf("ant faint should summon an apple for seat 0: %+v", summons) } // Follower fights at 3 power vs enemy at 2 power with 1 damage: enemy // dies, follower survives (2 damage < 3 power). if res.WinnerSeat != 0 { t.Fatalf("apple-buffed follower should win, got seat %d", res.WinnerSeat) } } // Cricket's faint summons a bee, which fights as a real 1-power pet. func TestCricketFaintSummonsBee(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.tier1(t, "Cricket")}, []Card{g.pet("Enemy", 3)}, ) summons := eventsOfType(res, "summon") if len(summons) != 1 || summons[0].Card.Name != "Bee" { t.Fatalf("cricket faint should summon a bee: %+v", summons) } beeRevealed := false for _, ev := range eventsOfType(res, "reveal") { if ev.Card.Name == "Bee" { beeRevealed = true } } if !beeRevealed { t.Fatal("the summoned bee should be revealed and fight") } // Cricket (1) then Bee (1) chip the 3-power enemy for 2 total; enemy // survives with 2 damage and wins. if res.WinnerSeat != 1 { t.Fatalf("enemy should survive with 2 damage, got winner %d", res.WinnerSeat) } if len(eventsOfType(res, "clash")) != 2 { t.Fatal("expected two clashes: cricket then bee") } } // Mosquito's rock lands before the clash and can kill outright: with a // rigged roll of 1, a 1-power enemy dies to the rock and the mosquito never // takes damage. func TestMosquitoRockKillsBeforeClash(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 1 } res := forceBattle(t, g, []Card{g.tier1(t, "Mosquito")}, []Card{g.pet("Weakling", 1)}, ) rocks := eventsOfType(res, "rock") if len(rocks) != 1 || rocks[0].Seat != 0 || rocks[0].Target != 1 { t.Fatalf("expected one rock from seat 0 at seat 1: %+v", rocks) } if rocks[0].Roll != 1 || !rocks[0].TargetDied || rocks[0].DamageAfter != 1 { t.Fatalf("rock should kill the 1-power pet: %+v", rocks[0]) } if len(eventsOfType(res, "clash")) != 0 { t.Fatal("no clash should happen; the rock already won it") } if res.WinnerSeat != 0 { t.Fatalf("mosquito should win, got %d", res.WinnerSeat) } } // A rock can roll blank faces: 0 damage, no effect, then a normal clash. func TestMosquitoRockCanMiss(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 0 } res := forceBattle(t, g, []Card{g.tier1(t, "Mosquito")}, []Card{g.pet("Weakling", 1)}, ) rocks := eventsOfType(res, "rock") if len(rocks) != 1 || rocks[0].Roll != 0 || rocks[0].TargetDied || rocks[0].DamageAfter != 0 { t.Fatalf("blank roll should do nothing: %+v", rocks) } if len(eventsOfType(res, "clash")) != 1 { t.Fatal("the clash should still happen after a miss") } if res.WinnerSeat != 0 { t.Fatalf("mosquito still wins the clash 2v1, got %d", res.WinnerSeat) } } // Two mosquitos rock each other simultaneously (roll 1 each); both survive // (2 power, 1 damage), then clash and both die. func TestMosquitoMirror(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 1 } res := forceBattle(t, g, []Card{g.tier1(t, "Mosquito")}, []Card{g.tier1(t, "Mosquito")}, ) if len(eventsOfType(res, "rock")) != 2 { t.Fatal("both mosquitos should throw rocks") } clash := eventsOfType(res, "clash")[0] if !clash.Died[0] || !clash.Died[1] { t.Fatalf("both wounded mosquitos should die in the clash: %+v", clash) } if res.WinnerSeat != -1 { t.Fatal("expected a draw") } } // Flamingo's faint stacks two apples: the follower fights at +2. func TestFlamingoFaintAddsTwoApples(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realPet(t, "Flamingo"), g.pet("Follower", 2)}, []Card{g.pet("Enemy", 3)}, ) if len(eventsOfType(res, "summon")) != 2 { t.Fatalf("flamingo should summon 2 apples: %+v", eventsOfType(res, "summon")) } // Follower at 4 power vs enemy at 3 power carrying 1 damage: enemy // dies, follower survives (3 damage < 4 power). if res.WinnerSeat != 0 { t.Fatalf("buffed follower should win, got %d", res.WinnerSeat) } } // Peacock eats an apple every time it's hurt and survives. func TestPeacockEatsWhenHurt(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realPet(t, "Peacock")}, // 2 power []Card{g.pet("Chip", 1), g.pet("Chip2", 1)}, ) // Clash 1: peacock takes 1 (alive at 2 power), eats → 3 power. // Clash 2: takes 1 more (2 damage < 3 power), eats → 4 power. eats := eventsOfType(res, "eat") if len(eats) != 2 { t.Fatalf("peacock should eat twice, got %+v", eats) } if eats[0].Bonus != 1 || eats[1].Bonus != 2 { t.Fatalf("bonus should grow 1 then 2: %+v", eats) } if res.WinnerSeat != 0 { t.Fatalf("peacock should survive and win, got %d", res.WinnerSeat) } } // Rat's faint puts a bee on the ENEMY's stack, where it fights for them. func TestRatFaintSummonsBeeForEnemy(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realPet(t, "Rat"), g.pet("Closer", 9)}, // rat: 4 power []Card{g.pet("Equal", 4)}, ) summons := eventsOfType(res, "summon") if len(summons) != 1 || summons[0].Seat != 1 || summons[0].Card.Name != "Bee" { t.Fatalf("rat should summon a bee on the enemy stack: %+v", summons) } // Rat and Equal trade (both die). The bee fights for seat 1 next and // loses to the Closer. beeRevealedBySeat1 := false for _, ev := range eventsOfType(res, "reveal") { if ev.Card.Name == "Bee" && ev.Seat == 1 { beeRevealedBySeat1 = true } } if !beeRevealedBySeat1 { t.Fatal("the enemy should reveal and field the rat's bee") } if res.WinnerSeat != 0 { t.Fatalf("seat 0 should win, got %d", res.WinnerSeat) } } // Spider pushes a bee then an apple: the apple ends up on top, so it buffs // the bee to 2 power. func TestSpiderFaintBeeGetsApple(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realPet(t, "Spider")}, // 2 power []Card{g.pet("Enemy", 3)}, ) summons := eventsOfType(res, "summon") if len(summons) != 2 || summons[0].Card.Name != "Bee" || summons[1].Card.Food != FoodApple { t.Fatalf("spider should summon bee then apple: %+v", summons) } // Enemy (3 power) takes 2 from spider, then fights the 2-power bee // (1+apple): both die. Draw. if res.WinnerSeat != -1 { t.Fatalf("expected draw, got %d", res.WinnerSeat) } } // Honey is a perk: the pet it's attached to summons a bee when it faints, // and only the last-applied perk counts. func TestHoneyPerk(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.newHoney(t), g.newHoney(t), g.pet("Bear", 2)}, []Card{g.pet("Enemy", 3)}, ) summons := eventsOfType(res, "summon") if len(summons) != 1 || summons[0].Card.Name != "Bee" || summons[0].Seat != 0 { t.Fatalf("exactly one honey (the last-applied) should trigger: %+v", summons) } // Bear dies to the 3; enemy carries 2 damage; the bee finishes it and // dies too: draw. if res.WinnerSeat != -1 { t.Fatalf("expected draw, got %d", res.WinnerSeat) } } // Garlic prevents 1 damage from every attack that hits its pet. func TestGarlicPreventsDamage(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realFood(t, "Garlic"), g.pet("Tank", 2)}, []Card{g.pet("Equal", 2)}, ) // Tank takes 2-1=1 (survives at 2 power); Equal takes 2 and dies. clash := eventsOfType(res, "clash")[0] if clash.Died[0] || !clash.Died[1] { t.Fatalf("garlic tank should survive the equal-power clash: %+v", clash) } if clash.Damage[0] != 1 { t.Fatalf("garlic should reduce the hit to 1, got %d", clash.Damage[0]) } if res.WinnerSeat != 0 { t.Fatalf("garlic side should win, got %d", res.WinnerSeat) } } // Two garlic'd 1-power pets can never hurt each other: the battle must end // as a stalemate draw instead of looping forever. func TestGarlicStalemateIsDraw(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realFood(t, "Garlic"), g.pet("A", 1)}, []Card{g.realFood(t, "Garlic"), g.pet("B", 1)}, ) if res.WinnerSeat != -1 { t.Fatalf("stalemate should be a draw, got %d", res.WinnerSeat) } if len(res.Events) > 10 { t.Fatalf("stalemate should end immediately, got %d events", len(res.Events)) } } // Dolphin throws 3 rocks on play (rigged to roll 1 each = 3 damage). func TestDolphinThrowsThreeRocks(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 1 } res := forceBattle(t, g, []Card{g.realPet(t, "Dolphin")}, []Card{g.pet("Tank", 3)}, ) rocks := eventsOfType(res, "rock") if len(rocks) != 1 || rocks[0].Roll != 3 || !rocks[0].TargetDied { t.Fatalf("dolphin should roll 3 dice for 3 damage and kill the tank: %+v", rocks) } if res.WinnerSeat != 0 { t.Fatalf("dolphin should win, got %d", res.WinnerSeat) } } // Camel pushes an apple onto its own stack whenever it's hurt and survives. func TestCamelHurtSummonsApple(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realPet(t, "Camel"), g.pet("Ally", 2)}, // camel: 3 power []Card{g.pet("Chip", 1), g.pet("Chip2", 3)}, ) // Clash 1: camel takes 1 (survives) → apple onto A's stack. Clash 2 vs // Chip2: both die. A reveals apple + Ally (3 power) and wins. summons := eventsOfType(res, "summon") if len(summons) != 1 || summons[0].Seat != 0 || summons[0].Card.Food != FoodApple { t.Fatalf("camel should summon one apple onto its own stack: %+v", summons) } if res.WinnerSeat != 0 { t.Fatalf("apple-buffed ally should win, got %d", res.WinnerSeat) } } // Sheep faints into two bees that fight on. func TestSheepFaintSummonsTwoBees(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.realPet(t, "Sheep")}, // 3 power []Card{g.pet("Big", 5)}, ) summons := eventsOfType(res, "summon") if len(summons) != 2 || summons[0].Card.Name != "Bee" || summons[1].Card.Name != "Bee" { t.Fatalf("sheep should summon 2 bees: %+v", summons) } // Big (5) takes 3 from sheep, then 1 from each bee: 5 total = dead; the // second bee dies with it. Draw. if res.WinnerSeat != -1 { t.Fatalf("expected draw, got %d", res.WinnerSeat) } } // Dodo recycles up to 3 of its attached apples onto the deck when it faints. func TestDodoRecyclesApples(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.newApple(), g.newApple(), g.realPet(t, "Dodo"), g.pet("Heir", 5)}, // dodo: 3+2=5 []Card{g.pet("Big", 6)}, ) // Dodo (5) dies to the 6; Big carries 5 damage. The two apples come // back on top: Heir fights at 5+2=7, survives Big's 6 attack, kills it. summons := eventsOfType(res, "summon") if len(summons) != 2 { t.Fatalf("dodo should recycle exactly its 2 apples: %+v", summons) } for _, s := range summons { if s.Seat != 0 || s.Card.Food != FoodApple { t.Fatalf("recycled cards should be seat 0 apples: %+v", s) } } if res.WinnerSeat != 0 { t.Fatalf("recycled apples should carry the win, got %d", res.WinnerSeat) } } // Dog eats one apple per friendly bee that has fainted this battle. func TestDogEatsPerFaintedBee(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.tier1(t, "Cricket"), g.realPet(t, "Dog")}, // cricket 1, dog 2 []Card{g.pet("Chip", 1), g.pet("Wall", 2)}, ) // Cricket and Chip trade; cricket's bee then dies to Wall (1 friendly // bee fainted, Wall at 1 damage). Dog plays, eats 1 apple → 3 power, // and beats the wounded Wall. eats := eventsOfType(res, "eat") if len(eats) != 1 || eats[0].Seat != 0 || eats[0].Bonus != 1 { t.Fatalf("dog should eat exactly 1 apple: %+v", eats) } if res.WinnerSeat != 0 { t.Fatalf("dog should win, got %d", res.WinnerSeat) } } // Badger sets aside on faint; when the next friendly pet plays it throws 2 // rocks at EACH active pet — enemy and friend alike. func TestBadgerDelayedRocksHitBothSides(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 1 } res := forceBattle(t, g, []Card{g.realPet(t, "Badger"), g.pet("Next", 3)}, // badger: 3 []Card{g.pet("Tank", 5)}, ) // Clash: badger dies (takes 5), Tank keeps 3 damage (2 health). Next // plays → badger throws 2 rocks (2 damage) at Next AND at Tank: Next // drops to 1 health, Tank dies at 5 damage. Seat 0 wins. rocks := eventsOfType(res, "rock") if len(rocks) != 2 { t.Fatalf("badger should produce one rock volley per active pet: %+v", rocks) } hitSelf, hitEnemy := false, false for _, r := range rocks { if r.Roll != 2 { t.Fatalf("each volley should roll 2 dice = 2 damage: %+v", r) } if r.Target == 0 { hitSelf = true if r.TargetDied { t.Fatalf("Next (3 power) should survive 2 self-damage: %+v", r) } } if r.Target == 1 { hitEnemy = true if !r.TargetDied { t.Fatalf("Tank (5 power, 3 damage) should die to 2 more: %+v", r) } } } if !hitSelf || !hitEnemy { t.Fatalf("badger must hit both sides: %+v", rocks) } if res.WinnerSeat != 0 { t.Fatalf("seat 0 should win, got %d", res.WinnerSeat) } } // Temporary cards (apples, bees) leave the deck once the battle is // acknowledged; pets stay. func TestTemporariesExpireAfterBattle(t *testing.T) { g, p1, p2 := testGame(t) forceBattle(t, g, []Card{g.newApple(), g.pet("Keeper", 3)}, []Card{g.pet("Enemy", 1)}, ) if err := g.AcknowledgeBattle(p1.ID); err != nil { t.Fatal(err) } if err := g.AcknowledgeBattle(p2.ID); err != nil { t.Fatal(err) } if slices.ContainsFunc(p1.Deck, func(c Card) bool { return c.Temporary }) { t.Fatalf("temporary cards should be gone after the battle: %+v", p1.Deck) } if len(p1.Deck) != 1 || p1.Deck[0].Name != "Keeper" { t.Fatalf("pets should survive the round: %+v", p1.Deck) } } // TestPriorityTokenInitialAssignment checks the token starts on a real seat // and that seat opens the shop. func TestPriorityTokenInitialAssignment(t *testing.T) { g := New() if _, err := g.AddPlayer("Alice"); err != nil { t.Fatal(err) } if _, err := g.AddPlayer("Bob"); err != nil { t.Fatal(err) } if g.PrioritySeat < 0 || g.PrioritySeat >= len(g.Players) { t.Fatalf("priority token should start on a real seat, got %d", g.PrioritySeat) } if g.Turn != g.PrioritySeat { t.Fatalf("priority holder should open the shop: turn=%d priority=%d", g.Turn, g.PrioritySeat) } } // TestPriorityWinsRockRace: two pets that both throw rocks on play would each // kill the other, so whoever has priority throws first and survives. Flipping // the token flips the winner, proving the token — not seat order — decides. func TestPriorityWinsRockRace(t *testing.T) { for _, priority := range []int{0, 1} { g, _, _ := testGame(t) g.RollDie = func() int { return 2 } // 3 dice = 6, lethal to a 2-power pet g.PrioritySeat = priority res := forceBattle(t, g, []Card{g.realPet(t, "Dolphin")}, []Card{g.realPet(t, "Dolphin")}) if res.WinnerSeat != priority { t.Fatalf("priority seat %d should win the rock race, winner=%d", priority, res.WinnerSeat) } } } // TestPriorityTokenTransfer: a winner who holds the token hands it to the // loser; a loser who holds it keeps it; a draw leaves it put. func TestPriorityTokenTransfer(t *testing.T) { // Winner holds it -> passes to the loser. g, _, _ := testGame(t) g.PrioritySeat = 0 forceBattle(t, g, []Card{g.pet("Champ", 9)}, []Card{g.pet("Chump", 1)}) if g.Battle.WinnerSeat != 0 { t.Fatalf("seat 0 should win, got %d", g.Battle.WinnerSeat) } if g.PrioritySeat != 1 { t.Fatalf("winner should hand the token to the loser, priority=%d", g.PrioritySeat) } // Loser holds it -> keeps it. g, _, _ = testGame(t) g.PrioritySeat = 1 forceBattle(t, g, []Card{g.pet("Champ", 9)}, []Card{g.pet("Chump", 1)}) if g.Battle.WinnerSeat != 0 { t.Fatalf("seat 0 should win, got %d", g.Battle.WinnerSeat) } if g.PrioritySeat != 1 { t.Fatalf("a losing token holder should keep it, priority=%d", g.PrioritySeat) } // Draw -> token stays put. Equal-power pets trade lethal blows at once. g, _, _ = testGame(t) g.PrioritySeat = 0 forceBattle(t, g, []Card{g.pet("A", 3)}, []Card{g.pet("B", 3)}) if g.Battle.WinnerSeat != -1 { t.Fatalf("mutual KO should draw, got %d", g.Battle.WinnerSeat) } if g.PrioritySeat != 0 { t.Fatalf("a draw should leave the token put, priority=%d", g.PrioritySeat) } }