637 lines
21 KiB
Go
637 lines
21 KiB
Go
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)
|
||
}
|
||
}
|