Initial pass at Golden Pack.
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user