Files
super-auto-pets-board-game/internal/game/tier456_test.go
T

546 lines
18 KiB
Go
Raw 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 "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)
}
}