Add pets for tiers 4-6.

This commit is contained in:
Greyson Parrelli
2026-07-23 00:10:20 -04:00
parent 8d43bbf61e
commit 10ac457e2e
11 changed files with 1334 additions and 163 deletions
+545
View File
@@ -0,0 +1,545 @@
package game
import "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)
}
}
spendAllCoins(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)
}
}
// 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)
}
}
// 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)
}
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)
}
}