Files

637 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package game
import (
"strings"
"testing"
)
// --- Tier 4 ---
func countApplesIn(deck []Card) int {
n := 0
for _, c := range deck {
if c.Food == FoodApple {
n++
}
}
return n
}
func TestSquirrelBuyAndSell(t *testing.T) {
g, _, _ := testGame(t)
p := current(g)
g.ShopRow[0] = g.realPet(t, "Squirrel")
if err := g.Buy(p.ID, 0); err != nil {
t.Fatal(err)
}
if countApplesIn(p.Deck) != 2 {
t.Fatalf("buying a Squirrel should add 2 apples, got %d", countApplesIn(p.Deck))
}
// Sell it back on the next turn: 1 base + 2 extra = 3 more apples.
if err := g.Buy(current(g).ID, 0); err != nil { // opponent spends a coin
t.Fatal(err)
}
var squirrelID string
for _, c := range p.Deck {
if c.Name == "Squirrel" {
squirrelID = c.ID
}
}
if err := g.Sell(p.ID, []string{squirrelID}); err != nil {
t.Fatal(err)
}
if countApplesIn(p.Deck) != 5 {
t.Fatalf("selling a Squirrel should add 3 apples (1 base + 2 extra), got %d total", countApplesIn(p.Deck))
}
}
// Turtle's faint shields the next friendly hit completely.
func TestTurtleShieldsNextFriendlyHit(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Turtle"), g.pet("Ally", 3)}, // turtle: 2
[]Card{g.pet("Big", 5)},
)
// Turtle dies to the 5 (Big takes 2). Ally's first hit is fully
// blocked; Ally hits back for 3: Big at 5 damage = dead. Ally wins
// unscathed.
shields := eventsOfType(res, "shield")
if len(shields) != 1 || shields[0].Seat != 0 {
t.Fatalf("expected one shield block for seat 0: %+v", shields)
}
clashes := eventsOfType(res, "clash")
final := clashes[len(clashes)-1]
if final.Damage[0] != 0 || !final.Died[1] {
t.Fatalf("ally should take 0 damage while Big dies: %+v", final)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Pineapple is a perk that gives its pet "Play: throw 3 Rocks".
func TestPineapplePerkThrowsRocks(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 }
res := forceBattle(t, g,
[]Card{g.realFood(t, "Pineapple"), g.pet("Holder", 2)},
[]Card{g.pet("Tank", 3)},
)
rocks := eventsOfType(res, "rock")
if len(rocks) != 1 || rocks[0].Roll != 3 || !rocks[0].TargetDied {
t.Fatalf("pineapple should throw 3 rocks and kill the 3-power tank: %+v", rocks)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Rooster summons a bee per apple it ate, capped at 3.
func TestRoosterBeesPerEatenAppleCapped(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.newApple(), g.newApple(), g.newApple(), g.newApple(), g.realPet(t, "Rooster")}, // 4+4=8
[]Card{g.pet("Colossus", 9)},
)
summons := eventsOfType(res, "summon")
if len(summons) != 3 {
t.Fatalf("rooster ate 4 apples but summons cap at 3 bees: %+v", summons)
}
for _, s := range summons {
if s.Card.Name != "Bee" {
t.Fatalf("rooster summons bees: %+v", s)
}
}
// Colossus (9) takes 8 from rooster, dies to the first bee; seat 0
// still has bees to field and wins.
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Bison's Battle Prep needs a Triple this round.
func TestBisonBattlePrepRequiresTriple(t *testing.T) {
for _, tripled := range []bool{false, true} {
g, p1, _ := testGame(t)
p1.Deck = append(p1.Deck, g.realPet(t, "Bison"))
if tripled {
var ids []string
for range 3 {
c := g.pet("Fodder", 1)
c.Suit = SuitBlue
p1.Deck = append(p1.Deck, c)
ids = append(ids, c.ID)
}
g.Turn = p1.Seat
if err := g.TradeStart(p1.ID, ids); err != nil {
t.Fatal(err)
}
if err := g.TradeChoose(p1.ID, 0); err != nil {
t.Fatal(err)
}
}
passShop(t, g)
want := 0
if tripled {
want = 3
}
// Count only apples (the trade-chosen card may have added some via
// its own Buy effect — exclude by counting before/after? Instead,
// require at least; strict equality only in the no-trade case).
got := countApplesIn(p1.Deck)
if !tripled && got != want {
t.Fatalf("no triple: bison should add no apples, got %d", got)
}
if tripled && got < want {
t.Fatalf("tripled: bison should add 3 apples, got %d", got)
}
}
}
// Blowfish volleys 3 rocks at the enemy only when the next pet plays.
func TestBlowfishDelayedRocksEnemyOnly(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 }
res := forceBattle(t, g,
[]Card{g.realPet(t, "Blowfish"), g.pet("Next", 2)}, // blowfish: 3
[]Card{g.pet("Tank", 6)},
)
// Blowfish dies (takes 6), Tank keeps 3 damage. Next plays → 3 rocks
// at the enemy only: Tank at 6 damage = dead. Seat 0 wins; Next is
// untouched.
rocks := eventsOfType(res, "rock")
if len(rocks) != 1 || rocks[0].Target != 1 || rocks[0].Roll != 3 {
t.Fatalf("blowfish should volley the enemy only: %+v", rocks)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// A fainting Blowfish is set aside as a card (with its own ID), then released
// once the next pet plays and its volley resolves.
func TestBlowfishSetAsideThenRelease(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 }
blowfish := g.realPet(t, "Blowfish")
res := forceBattle(t, g,
[]Card{blowfish, g.pet("Next", 2)},
[]Card{g.pet("Tank", 6)},
)
setasides := eventsOfType(res, "setaside")
if len(setasides) != 1 || setasides[0].Seat != 0 || setasides[0].Card == nil ||
setasides[0].Card.ID != blowfish.ID {
t.Fatalf("blowfish should be set aside once for seat 0: %+v", setasides)
}
releases := eventsOfType(res, "release")
if len(releases) != 1 || releases[0].Seat != 0 || releases[0].Card == nil ||
releases[0].Card.ID != blowfish.ID {
t.Fatalf("blowfish should be released once for seat 0: %+v", releases)
}
// setaside on faint, then release after the volley: setaside precedes the
// rock, which precedes the release.
idx := func(typ string) int {
for i, ev := range res.Events {
if ev.Type == typ {
return i
}
}
return -1
}
if !(idx("setaside") < idx("rock") && idx("rock") < idx("release")) {
t.Fatalf("expected setaside < rock < release, got %d < %d < %d",
idx("setaside"), idx("rock"), idx("release"))
}
}
// Skunk strips the enemy pet's foods; a wounded veteran can faint from the
// power loss.
func TestSkunkStripCanKill(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.pet("Chip", 3), g.realPet(t, "Skunk")},
[]Card{g.newApple(), g.newApple(), g.pet("Wall", 2)}, // 2+2 = 4 power
)
// Chip (3) dies to Wall (4); Wall carries 3 damage (1 health). Skunk
// plays and strips both apples: Wall drops to 2 power with 3 damage —
// it faints on the spot, no clash needed.
strips := eventsOfType(res, "strip")
if len(strips) != 1 || strips[0].Target != 1 || !strips[0].TargetDied {
t.Fatalf("strip should kill the buffed veteran: %+v", strips)
}
if len(eventsOfType(res, "clash")) != 1 {
t.Fatal("only the first clash should happen")
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Hippo heals 1 damage whenever an enemy pet faints.
func TestHippoHealsOnEnemyFaint(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Hippo")}, // 4 power
[]Card{g.pet("Chip", 2), g.pet("Chip2", 2)},
)
heals := eventsOfType(res, "heal")
if len(heals) != 2 {
t.Fatalf("hippo should heal on each enemy faint: %+v", heals)
}
if heals[0].DamageAfter != 1 || heals[1].DamageAfter != 2 {
t.Fatalf("hippo damage should go 2→1 then 3→2: %+v", heals)
}
if res.WinnerSeat != 0 {
t.Fatalf("hippo should win, got %d", res.WinnerSeat)
}
}
// --- Tier 5 ---
// Monkey starts the battle with 3 apples attached to the first pet.
func TestMonkeyApplesInPlay(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.pet("First", 2), g.realPet(t, "Monkey")}, // monkey: 3
[]Card{g.pet("Enemy", 5)},
)
preps := eventsOfType(res, "prep")
if len(preps) != 3 || preps[0].Seat != 0 {
t.Fatalf("monkey should start 3 apples in play for seat 0: %+v", preps)
}
// First fights at 2+3=5 vs 5: both die. Monkey (3) then beats nothing —
// enemy is out. Seat 0 wins.
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Rhino rocks every enemy pet as it's played.
func TestRhinoRocksEnemyPlays(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 }
res := forceBattle(t, g,
[]Card{g.realPet(t, "Rhino")}, // 5 power
[]Card{g.pet("A", 1), g.pet("B", 6)},
)
// A is played → rhino rocks it dead (1 damage ≥ 1 power) before any
// clash. B is played → rocked for 1, then clashes: B (6, dmg 1) takes
// 5 → dead at 6; rhino takes 6 → dead at 5. Both out: draw.
rocks := eventsOfType(res, "rock")
if len(rocks) != 2 || rocks[0].Seat != 0 || !rocks[0].TargetDied {
t.Fatalf("rhino should rock each played enemy pet: %+v", rocks)
}
if res.WinnerSeat != -1 {
t.Fatalf("expected draw, got %d", res.WinnerSeat)
}
}
// Crocodile's set-aside fires when the enemy plays their last pet.
func TestCrocodileLastPetRocks(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 }
res := forceBattle(t, g,
[]Card{g.realPet(t, "Crocodile"), g.pet("Anchor", 9)}, // croc: 4
[]Card{g.pet("Tank", 4), g.pet("Last", 4)},
)
// Croc and Tank (4v4) kill each other; croc sets aside. Anchor and
// Last are played — Last is the enemy's last pet, so croc volleys 3
// rocks (3 damage): Last at 3 damage. Clash: Last dies (3+9), Anchor
// takes 4. Seat 0 wins.
rocks := eventsOfType(res, "rock")
if len(rocks) != 1 || rocks[0].Target != 1 || rocks[0].Roll != 3 {
t.Fatalf("crocodile should volley the enemy's last pet: %+v", rocks)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Scorpion KOs whatever its clash attack hurts — but a fully prevented
// attack (Garlic on a 1-power hit) KOs nothing, and its rocks never KO.
func TestScorpionKnockout(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Scorpion")}, // 1 power
[]Card{g.pet("Giant", 9)},
)
clash := eventsOfType(res, "clash")[0]
if !clash.Died[1] {
t.Fatalf("scorpion's hit should KO the giant: %+v", clash)
}
if !clash.Died[0] {
t.Fatal("the giant's 9 attack still kills the scorpion")
}
if res.WinnerSeat != -1 {
t.Fatalf("expected mutual destruction draw, got %d", res.WinnerSeat)
}
}
func TestGarlicBlocksScorpionKO(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Scorpion")},
[]Card{g.realFood(t, "Garlic"), g.pet("Guard", 9)},
)
// Scorpion's 1 attack is fully prevented by garlic: no hurt, no KO.
clash := eventsOfType(res, "clash")[0]
if clash.Died[1] {
t.Fatalf("garlic should negate the scorpion's KO: %+v", clash)
}
if res.WinnerSeat != 1 {
t.Fatalf("guard should win, got %d", res.WinnerSeat)
}
}
// Seal only pays out if it carries a perk.
func TestSealNeedsPerk(t *testing.T) {
g, _, _ := testGame(t)
// Without a perk: nothing.
res := forceBattle(t, g,
[]Card{g.realPet(t, "Seal")},
[]Card{g.pet("Enemy", 9)},
)
if len(eventsOfType(res, "summon")) != 0 {
t.Fatal("seal without a perk should summon nothing")
}
// With honey attached: 3 apples onto the deck.
g2, _, _ := testGame(t)
res2 := forceBattle(t, g2,
[]Card{g2.newHoney(t), g2.realPet(t, "Seal"), g2.pet("Heir", 3)},
[]Card{g2.pet("Enemy", 9)},
)
apples := 0
for _, s := range eventsOfType(res2, "summon") {
if s.Card.Food == FoodApple && s.Seat == 0 {
apples++
}
}
if apples != 3 {
t.Fatalf("perked seal should push 3 apples onto its deck, got %d", apples)
}
}
// Shark eats an apple per fainted friendly pet.
func TestSharkEatsPerFaintedPet(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.pet("A", 1), g.pet("B", 1), g.realPet(t, "Shark")}, // shark: 1
[]Card{g.pet("Wall", 3)},
)
// A and B faint chipping Wall to 2 damage (1 health). Shark plays with
// 2 fainted friends → eats 2 → 3 power. Clash: Wall dies (3 more
// damage), shark survives 3 damage? 3 damage vs 3 power = fainted too.
eats := eventsOfType(res, "eat")
if len(eats) != 1 || eats[0].Bonus != 2 {
t.Fatalf("shark should eat 2 apples: %+v", eats)
}
if res.WinnerSeat != -1 {
t.Fatalf("expected draw (both die in final clash), got %d", res.WinnerSeat)
}
}
// Turkey's aura buffs later friendly bees.
func TestTurkeyBuffsBees(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Turkey"), g.tier1(t, "Cricket")}, // turkey 4, cricket 1
[]Card{g.pet("Wall", 6)},
)
// Turkey dies (Wall at 4 damage, 2 health), aura set. Cricket dies
// (Wall at 5 damage, 1 health), summons its bee. The bee plays at
// 1+1=2 power: kills Wall and survives with 6 damage? No — bee takes
// 6, dies; Wall takes 2, dies at 7 ≥ 6. Draw... but the bee had 2
// power so Wall dies; bee dies too. Draw.
if res.WinnerSeat != -1 {
t.Fatalf("expected draw, got %d", res.WinnerSeat)
}
// Verify the aura actually applied: final clash damage on Wall is
// 5+2=7 (a 1-power bee would leave it at 6 exactly = still dead...);
// check damage total instead.
clashes := eventsOfType(res, "clash")
final := clashes[len(clashes)-1]
if final.Damage[1] != 7 {
t.Fatalf("buffed bee should hit for 2 (wall at 7 damage), got %d", final.Damage[1])
}
}
// Chili mills the enemy deck down to the next non-Bee pet.
func TestChiliMillsEnemyDeck(t *testing.T) {
g, _, _ := testGame(t)
bee := g.newBee()
res := forceBattle(t, g,
[]Card{g.realFood(t, "Chili"), g.pet("Holder", 3)},
[]Card{g.pet("Front", 3), g.newApple(), bee, g.pet("Real", 3)},
)
// Holder (with chili) and Front play. Chili mills: apple, bee are
// discarded; Real stays on top. Clash: Holder and Front trade (3v3,
// both die). Real vs nothing — seat 1 wins.
mills := eventsOfType(res, "mill")
if len(mills) != 2 || mills[0].Seat != 1 {
t.Fatalf("chili should mill exactly the apple and bee: %+v", mills)
}
if mills[0].Card.Food != FoodApple || !isBee(*mills[1].Card) {
t.Fatalf("milled cards should be the apple then the bee: %+v", mills)
}
if res.WinnerSeat != 1 {
t.Fatalf("seat 1's Real pet should carry the day, got %d", res.WinnerSeat)
}
}
// --- Tier 6 ---
// Gorilla is hurt only every other hit.
func TestGorillaAlternatingShield(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Gorilla")}, // 6 power
[]Card{g.pet("A", 2), g.pet("B", 2), g.pet("C", 2)},
)
// Hit 1: takes 2 (damage 2), gains shield. Hit 2: blocked. Hit 3:
// takes 2 (damage 4), gains shield. Gorilla kills all three.
shields := eventsOfType(res, "shield")
if len(shields) != 1 || shields[0].Seat != 0 {
t.Fatalf("gorilla should block exactly the second hit: %+v", shields)
}
clashes := eventsOfType(res, "clash")
if len(clashes) != 3 || clashes[2].Damage[0] != 4 {
t.Fatalf("gorilla should end at 4 damage after 3 clashes: %+v", clashes)
}
if res.WinnerSeat != 0 {
t.Fatalf("gorilla should win, got %d", res.WinnerSeat)
}
}
// Melon blocks the first hit against its pet.
func TestMelonBlocksFirstHit(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realFood(t, "Melon"), g.pet("Holder", 3)},
[]Card{g.pet("Big", 5)},
)
// Hit 1 blocked; holder hits Big for 3. Hit 2: holder takes 5, dies;
// Big takes 3 more → 6 ≥ 5, dies. Draw.
shields := eventsOfType(res, "shield")
if len(shields) != 1 || shields[0].Seat != 0 {
t.Fatalf("melon should block the first hit: %+v", shields)
}
if res.WinnerSeat != -1 {
t.Fatalf("expected draw, got %d", res.WinnerSeat)
}
}
// A used Melon names itself in the log and is released from the play area so
// the fan clears.
func TestMelonBlockNamesSourceAndClears(t *testing.T) {
g, _, _ := testGame(t)
melon := g.realFood(t, "Melon")
res := forceBattle(t, g,
[]Card{melon, g.pet("Holder", 3)},
[]Card{g.pet("Big", 5)},
)
shields := eventsOfType(res, "shield")
if len(shields) != 1 || !strings.Contains(shields[0].Text, "Melon") ||
strings.Contains(shields[0].Text, "shield") {
t.Fatalf("shield line should name the Melon, not say 'shield': %q", shields[0].Text)
}
releases := eventsOfType(res, "release")
if len(releases) != 1 || releases[0].Card == nil || releases[0].Card.ID != melon.ID {
t.Fatalf("the spent Melon should be released from the play area: %+v", releases)
}
}
// With both a set-aside Turtle and a Melon protecting the same pet, the
// borrowed Turtle shield is spent first so its card clears the board.
func TestTurtleShieldSpentBeforeMelon(t *testing.T) {
g, _, _ := testGame(t)
turtle := g.realPet(t, "Turtle") // power 2
res := forceBattle(t, g,
[]Card{turtle, g.realFood(t, "Melon"), g.pet("Holder", 3)},
[]Card{g.pet("Big", 5)},
)
// Turtle (2) dies to Big (5), setting aside a shield; Big keeps 2 damage.
// Holder plays with Melon (own shield). The next hit is absorbed by the
// Turtle set-aside first, releasing the Turtle; Holder then downs Big and
// its Melon shield is never spent.
releases := eventsOfType(res, "release")
if len(releases) != 1 || releases[0].Card == nil || releases[0].Card.ID != turtle.ID {
t.Fatalf("the Turtle shield should be spent (and released) before the Melon: %+v", releases)
}
shields := eventsOfType(res, "shield")
if len(shields) != 1 || !strings.Contains(shields[0].Text, "Turtle") {
t.Fatalf("the block should be credited to the Turtle: %+v", shields)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win with the Melon still in reserve, got %d", res.WinnerSeat)
}
}
// Leopard throws rocks equal to its power, apples included.
func TestLeopardRocksEqualPower(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 }
res := forceBattle(t, g,
[]Card{g.newApple(), g.newApple(), g.newApple(), g.realPet(t, "Leopard")}, // 4+3=7
[]Card{g.pet("Fortress", 7)},
)
rocks := eventsOfType(res, "rock")
if len(rocks) != 1 || rocks[0].Roll != 7 || !rocks[0].TargetDied {
t.Fatalf("leopard should throw 7 rocks (7 damage) and level the fortress: %+v", rocks)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Mammoth's aura buffs all later friendly pets.
func TestMammothBuffsPets(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Mammoth"), g.pet("Heir", 3)}, // mammoth: 4
[]Card{g.pet("Wall", 8)},
)
// Mammoth dies (Wall at 4 damage). Heir plays at 3+1=4: dies to the 8,
// but Wall ends at 8 damage ≥ 8 → dead too. Draw proves the +1.
if res.WinnerSeat != -1 {
t.Fatalf("expected draw (aura makes exactly lethal), got %d", res.WinnerSeat)
}
}
// Cat doubles the apples in hand after adding two.
func TestCatDoublesApples(t *testing.T) {
g, _, _ := testGame(t)
p := current(g)
p.Deck = append(p.Deck, g.newApple()) // 1 apple already in hand
g.ShopRow[0] = g.realPet(t, "Cat")
if err := g.Buy(p.ID, 0); err != nil {
t.Fatal(err)
}
// 1 existing + 2 added = 3, doubled = 6.
if countApplesIn(p.Deck) != 6 {
t.Fatalf("cat should leave 6 apples ((1+2)×2), got %d", countApplesIn(p.Deck))
}
}
// Snake volleys on EVERY friendly pet play.
func TestSnakeRecurringRocks(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 }
res := forceBattle(t, g,
[]Card{g.realPet(t, "Snake"), g.pet("A", 2), g.pet("B", 2)}, // snake: 2
[]Card{g.pet("Wall", 9)},
)
// Snake dies. A plays → 2 rocks (2 damage) → Wall 2+2... sequence:
// snake clash: wall 2 damage. A plays → rocks 2 → wall 4. Clash: A
// dies, wall 6. B plays → rocks 2 → wall 8. Clash: B dies, wall 10 ≥ 9
// → dead. Both out → draw, with TWO rock volleys proving recurrence.
rocks := eventsOfType(res, "rock")
if len(rocks) != 2 {
t.Fatalf("snake should volley on each friendly play: %+v", rocks)
}
// The set-aside Snake threw the rocks, not the pet (A/B) in play.
for _, r := range rocks {
if !strings.Contains(r.Text, "Snake") {
t.Fatalf("rock event should credit Snake as the source, got %q", r.Text)
}
}
if res.WinnerSeat != -1 {
t.Fatalf("expected draw, got %d", res.WinnerSeat)
}
}
// Wolverine steals up to 3 apples, which can faint a wounded victim.
func TestWolverineStealsApples(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.pet("Chip", 4), g.realPet(t, "Wolverine")}, // wolverine: 5
[]Card{g.newApple(), g.newApple(), g.newApple(), g.newApple(), g.pet("Hoard", 2)}, // 2+4=6
)
// Chip (4) dies to Hoard (6); Hoard carries 4 damage (2 health).
// Wolverine plays, steals 3 apples: Hoard drops to 3 power with 4
// damage → faints. Wolverine ends at 5+3=8 power.
steals := eventsOfType(res, "steal")
if len(steals) != 1 || steals[0].Count != 3 || !steals[0].TargetDied {
t.Fatalf("wolverine should steal 3 apples and kill the hoard: %+v", steals)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Fly summons three bees on faint.
func TestFlySummonsThreeBees(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.realPet(t, "Fly")}, // 4 power
[]Card{g.pet("Wall", 7)},
)
summons := eventsOfType(res, "summon")
if len(summons) != 3 {
t.Fatalf("fly should summon 3 bees: %+v", summons)
}
// Wall takes 4+1+1+1 = 7 → dies with the last bee. Draw.
if res.WinnerSeat != -1 {
t.Fatalf("expected draw, got %d", res.WinnerSeat)
}
}