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

423 lines
13 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"
// --- Golden pack test helpers ---
// goldenGame builds a started 2-player game on the Golden pack, bypassing the
// lobby and the Playable gate (tiers 4-6 aren't printed yet, so the pack isn't
// selectable in a real lobby — but tiers 1-3 are fully exercisable here).
func goldenGame(t *testing.T) (*Game, *Player, *Player) {
t.Helper()
g := New()
g.Pack = "golden"
g.buildDecks()
p1, err := g.AddPlayer("Alice")
if err != nil {
t.Fatal(err)
}
p2, err := g.AddPlayer("Bob")
if err != nil {
t.Fatal(err)
}
g.start()
g.PrioritySeat = p1.Seat
g.Turn = p1.Seat
return g, p1, p2
}
// goldenPet mints a copy of a Golden pack pet (with effects) by name.
func (g *Game) goldenPet(t *testing.T, name string) Card {
t.Helper()
for tierIdx, tier := range goldenPetTiers {
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 golden pet named %s", name)
return Card{}
}
// goldenFood mints a Golden pack food (Avocado) by name.
func (g *Game) goldenFood(t *testing.T, name string) Card {
t.Helper()
for tierIdx, tier := range goldenFoodTiers {
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 golden food named %s", name)
return Card{}
}
// suitedPet mints a plain effect-less pet with a chosen suit.
func (g *Game) suitedPet(name string, power int, suit Suit) Card {
return Card{ID: g.newCardID(), Kind: KindPet, Name: name, Tier: 1, Power: power, Suit: suit}
}
// --- Trumpets & Golden Retriever ---
// A fainting Groundhog banks a Trumpet; with no cards left, the side fields a
// Golden Retriever whose Power equals its Trumpets — and it wins.
func TestGroundhogTrumpetThenGoldenRetriever(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Groundhog")}, // pow 1, faint: +1 trumpet
[]Card{g.pet("Weak", 1)},
)
trumpets := eventsOfType(res, "trumpet")
if len(trumpets) != 1 || trumpets[0].Seat != 0 || trumpets[0].Count != 1 {
t.Fatalf("expected one +1 trumpet event for seat 0: %+v", trumpets)
}
var gr *BattleEvent
for i, ev := range res.Events {
if ev.Type == "reveal" && ev.Card != nil && ev.Card.Name == "Golden Retriever" {
gr = &res.Events[i]
}
}
if gr == nil {
t.Fatal("expected a Golden Retriever to be summoned")
}
if gr.Card.Power != 1 || gr.Count != 1 {
t.Fatalf("Golden Retriever should have Power 1 from 1 trumpet: power=%d count=%d", gr.Card.Power, gr.Count)
}
if res.WinnerSeat != 0 {
t.Fatalf("the Golden Retriever should win for seat 0, got %d", res.WinnerSeat)
}
}
// Pied Tamarin spends a banked Trumpet on entry to throw 2 Rocks.
func TestPiedTamarinSpendsTrumpetForRocks(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 2 } // each rock deals 2
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Groundhog"), g.goldenPet(t, "Pied Tamarin")},
[]Card{g.pet("Weak", 1), g.pet("Tank", 3)},
)
// Groundhog trades with Weak (+1 trumpet). Pied Tamarin enters, spends the
// trumpet, throws 2 rocks (2+2=4) and kills the 3-power Tank.
spends := 0
for _, ev := range eventsOfType(res, "trumpet") {
if ev.Count < 0 {
spends++
}
}
if spends != 1 {
t.Fatalf("expected exactly one trumpet spend, got %d: %+v", spends, eventsOfType(res, "trumpet"))
}
rocks := eventsOfType(res, "rock")
if len(rocks) != 1 || rocks[0].Roll != 4 || !rocks[0].TargetDied {
t.Fatalf("Pied Tamarin should throw 2 rocks (roll 4) and kill the Tank: %+v", rocks)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Without a Trumpet, Pied Tamarin's paid Play effect does nothing.
func TestPiedTamarinNoTrumpetNoRocks(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 2 }
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Pied Tamarin")},
[]Card{g.pet("Tank", 3)},
)
if rocks := eventsOfType(res, "rock"); len(rocks) != 0 {
t.Fatalf("no trumpet means no rocks: %+v", rocks)
}
}
// Honduran White Bat banks a Trumpet per distinct suit among friendly fainted
// pets.
func TestHonduranBatTrumpetPerFaintedSuit(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{
g.suitedPet("Red", 1, SuitRed),
g.suitedPet("Blue", 1, SuitBlue),
g.goldenPet(t, "Honduran White Bat"),
},
[]Card{g.pet("K1", 1), g.pet("K2", 1), g.pet("Last", 1)},
)
// Red and Blue trade with K1/K2 (two distinct fainted suits). The Bat
// enters and banks 2 Trumpets.
var gain *BattleEvent
for i, ev := range res.Events {
if ev.Type == "trumpet" && ev.Seat == 0 && ev.Count > 0 {
gain = &res.Events[i]
}
}
if gain == nil || gain.Count != 2 {
t.Fatalf("Bat should bank 2 trumpets for 2 unique fainted suits: %+v", eventsOfType(res, "trumpet"))
}
}
// Flea drains Trumpets from the opponent when it faints.
func TestFleaDrainsEnemyTrumpets(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Flea")}, // pow 2
[]Card{g.goldenPet(t, "Groundhog"), g.pet("Big", 5)}, // Groundhog banks a trumpet for seat 1
)
// Flea kills Groundhog (seat 1 +1 trumpet), survives, then dies to Big and
// drains that trumpet back down to 0.
var drain *BattleEvent
for i, ev := range res.Events {
if ev.Type == "trumpet" && ev.Seat == 1 && ev.Count < 0 {
drain = &res.Events[i]
}
}
if drain == nil {
t.Fatalf("Flea should drain a trumpet from seat 1: %+v", eventsOfType(res, "trumpet"))
}
}
// --- Cone Snail ---
// Cone Snail's faint sets aside a one-shot 2-damage prevention for the next
// friendly hit.
func TestConeSnailPreventsTwoDamage(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Cone Snail"), g.pet("Ally", 5)},
[]Card{g.pet("Big", 6)},
)
// Cone Snail dies to the 6 (Big takes 1). Ally enters; Big's next hit of 6
// is shaved to 4, so Ally (5) survives and kills Big.
prevents := eventsOfType(res, "prevent")
if len(prevents) != 1 || prevents[0].Seat != 0 || prevents[0].Count != 2 {
t.Fatalf("expected one 2-damage prevention for seat 0: %+v", prevents)
}
clashes := eventsOfType(res, "clash")
final := clashes[len(clashes)-1]
if final.Damage[0] != 4 || !final.Died[1] {
t.Fatalf("Ally should take 4 (6-2) and Big should die: %+v", final)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// --- Bulldog ---
// Bulldog eats an apple after attacking, but only once per battle.
func TestBulldogAfterAttackOncePerRound(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Bulldog")}, // pow 2
[]Card{g.pet("W1", 1), g.pet("W2", 1)},
)
eats := eventsOfType(res, "eat")
if len(eats) != 1 || eats[0].Seat != 0 || eats[0].Bonus != 1 {
t.Fatalf("Bulldog should eat exactly one apple across the battle: %+v", eats)
}
if res.WinnerSeat != 0 {
t.Fatalf("Bulldog should survive and win, got %d", res.WinnerSeat)
}
}
// --- Bear ---
// Bear's faint drops a Bee on the bottom of BOTH decks.
func TestBearBeeToBothDecks(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Bear")}, // pow 4
[]Card{g.pet("Big", 5)},
)
seats := map[int]bool{}
for _, ev := range eventsOfType(res, "summon") {
if ev.Card != nil && ev.Card.Name == "Bee" {
seats[ev.Seat] = true
}
}
if !seats[0] || !seats[1] {
t.Fatalf("Bear should summon a Bee onto both decks, saw seats %v", seats)
}
}
// --- Royal Flycatcher ---
// Royal Flycatcher throws one Rock per enemy pet already fainted.
func TestRoyalFlycatcherRocksPerEnemyFaint(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 2 }
res := forceBattle(t, g,
[]Card{g.pet("K1", 1), g.pet("K2", 1), g.goldenPet(t, "Royal Flycatcher")},
[]Card{g.pet("E1", 1), g.pet("E2", 1), g.pet("Tank", 3)},
)
// K1/K2 trade with E1/E2 (2 enemy faints). Royal Flycatcher enters and
// throws 2 rocks (2+2=4), killing the 3-power Tank.
rocks := eventsOfType(res, "rock")
if len(rocks) != 1 || len(rocks[0].Dice) != 2 || !rocks[0].TargetDied {
t.Fatalf("Royal Flycatcher should throw 2 rocks (one per enemy faint) and kill Tank: %+v", rocks)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// --- Shop-time effects ---
// Selling a Chipmunk yields its base apple plus 2 extra.
func TestChipmunkSellApples(t *testing.T) {
g, p1, p2 := goldenGame(t)
chip := g.goldenPet(t, "Chipmunk")
p1.Deck = append(p1.Deck, chip)
// Hand the turn back to seat 0 after seat 1 acts.
_ = p2
g.Turn = 0
if err := g.Sell(p1.ID, []string{chip.ID}); err != nil {
t.Fatal(err)
}
if countApplesIn(p1.Deck) != 3 {
t.Fatalf("selling a Chipmunk should add 3 apples (1 base + 2 extra), got %d", countApplesIn(p1.Deck))
}
}
// Opossum's sell adds an apple per Faint pet currently in the shop.
func TestOpossumSellApplesPerShopFaint(t *testing.T) {
g, p1, _ := goldenGame(t)
op := g.goldenPet(t, "Opossum")
p1.Deck = append(p1.Deck, op)
g.ShopRow = []Card{
g.goldenPet(t, "Groundhog"), // faint
g.goldenPet(t, "Cone Snail"), // faint
g.goldenPet(t, "Bulldog"), // after-attack, no faint
g.goldenPet(t, "Pied Tamarin"), // play, no faint
}
g.Turn = 0
if err := g.Sell(p1.ID, []string{op.ID}); err != nil {
t.Fatal(err)
}
// 1 base apple + 2 (one per faint pet in shop).
if countApplesIn(p1.Deck) != 3 {
t.Fatalf("selling Opossum with 2 shop faint pets should add 3 apples, got %d", countApplesIn(p1.Deck))
}
}
// Stoat's sell pulls the top of the current tier's shop deck for free.
func TestStoatSellBuysTopOfDeck(t *testing.T) {
g, p1, _ := goldenGame(t)
stoat := g.goldenPet(t, "Stoat")
p1.Deck = append(p1.Deck, stoat)
g.Turn = 0
deckBefore := len(g.ShopDecks[g.Round-1])
petsBefore := p1.PetCount()
if err := g.Sell(p1.ID, []string{stoat.ID}); err != nil {
t.Fatal(err)
}
if got := len(g.ShopDecks[g.Round-1]); got != deckBefore-1 {
t.Fatalf("Stoat should draw one card from the tier deck: before %d, after %d", deckBefore, got)
}
// Stoat itself became an apple, but a fresh pet came from the deck, so the
// pet count is unchanged (1 Stoat, +1 grabbed pet, assuming a pet on top).
if p1.PetCount() < petsBefore {
t.Fatalf("Stoat should have replaced itself with a grabbed pet: before %d, after %d", petsBefore, p1.PetCount())
}
}
// Hercules Beetle banks apples-in-play only from round 3 on, and the battle
// consumes them onto the first pet.
func TestHerculesBeetleApplesInPlay(t *testing.T) {
g, p1, _ := goldenGame(t)
// Round 2: no effect.
g.Round = 2
hb2 := g.goldenPet(t, "Hercules Beetle")
p1.Deck = append(p1.Deck, hb2)
g.Turn = 0
if err := g.Sell(p1.ID, []string{hb2.ID}); err != nil {
t.Fatal(err)
}
if p1.PendingApplesInPlay != 0 {
t.Fatalf("Hercules Beetle should do nothing before round 3, got %d", p1.PendingApplesInPlay)
}
// Round 3: banks 3.
g.Round = 3
hb3 := g.goldenPet(t, "Hercules Beetle")
p1.Deck = append(p1.Deck, hb3)
g.Turn = 0
if err := g.Sell(p1.ID, []string{hb3.ID}); err != nil {
t.Fatal(err)
}
if p1.PendingApplesInPlay != 3 {
t.Fatalf("Hercules Beetle in round 3 should bank 3 apples-in-play, got %d", p1.PendingApplesInPlay)
}
// The battle seeds those apples onto the first pet, then resets.
res := forceBattle(t, g,
[]Card{g.pet("Hero", 3)},
[]Card{g.pet("Foe", 1)},
)
if preps := eventsOfType(res, "prep"); len(preps) != 3 {
t.Fatalf("expected 3 apples-in-play prep events, got %d", len(preps))
}
if p1.PendingApplesInPlay != 0 {
t.Fatalf("PendingApplesInPlay should reset after the battle, got %d", p1.PendingApplesInPlay)
}
}
// --- Avocado ---
// Buying an Avocado sets it aside (not into the deck); a later buy can spend it
// instead of a coin.
func TestAvocadoBuyAndDiscardToBuy(t *testing.T) {
g, p1, _ := goldenGame(t)
g.ShopRow[0] = g.goldenFood(t, "Avocado")
g.Turn = 0
coins := p1.Coins
if err := g.Buy(p1.ID, 0); err != nil {
t.Fatal(err)
}
if p1.Avocados != 1 {
t.Fatalf("buying an Avocado should set aside 1, got %d", p1.Avocados)
}
if p1.Coins != coins-1 {
t.Fatalf("buying an Avocado still costs a coin: before %d, after %d", coins, p1.Coins)
}
for _, c := range p1.Deck {
if c.Food == FoodAvocado {
t.Fatal("the Avocado should not be in the deck")
}
}
// Now discard the Avocado to buy the card in slot 1, spending no coin.
g.Turn = 0
deckLen := len(p1.Deck)
coins = p1.Coins
if err := g.BuyAvocado(p1.ID, 1); err != nil {
t.Fatal(err)
}
if p1.Avocados != 0 {
t.Fatalf("the discard should consume the Avocado, got %d", p1.Avocados)
}
if p1.Coins != coins {
t.Fatalf("a discard-buy should spend no coin: before %d, after %d", coins, p1.Coins)
}
if len(p1.Deck) != deckLen+1 {
t.Fatalf("the discard-buy should add a card to the deck: before %d, after %d", deckLen, len(p1.Deck))
}
}
// BuyAvocado is refused with no Avocado in the stash.
func TestBuyAvocadoRequiresToken(t *testing.T) {
g, p1, _ := goldenGame(t)
g.Turn = 0
if err := g.BuyAvocado(p1.ID, 0); err == nil {
t.Fatal("expected an error buying with no Avocado")
}
}