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

359 lines
11 KiB
Go

package game
import "testing"
// --- Nurse Shark: the mid-battle decision channel ---
// Nurse Shark suspends the battle to ask how many Trumpets to spend, then
// resumes and throws two rocks per Trumpet spent.
func TestNurseSharkSuspendsAndResumes(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 2 } // each rock deals 2
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Nyala"), g.goldenPet(t, "Nurse Shark")}, // Nyala faint: +2 trumpets
[]Card{g.pet("Big", 4), g.pet("Tank", 6)},
)
// Nyala trades with Big (banking 2 Trumpets), then Nurse Shark enters and
// the battle suspends on its choice.
if g.PendingBattle == nil {
t.Fatal("expected the battle to suspend on Nurse Shark's choice")
}
if g.PendingBattle.Seat != 0 || g.PendingBattle.Max != 2 || g.PendingBattle.Kind != "nurseShark" {
t.Fatalf("unexpected pending decision: %+v", g.PendingBattle)
}
if res.WinnerSeat != -1 {
t.Fatalf("a suspended battle has no winner yet, got %d", res.WinnerSeat)
}
// Spend both Trumpets: 4 rocks (2+2+2+2 = 8) kill the 6-power Tank.
if err := g.BattleChoose(g.Players[0].ID, 2); err != nil {
t.Fatal(err)
}
if g.PendingBattle != nil {
t.Fatalf("battle should have completed: %+v", g.PendingBattle)
}
spends := 0
for _, ev := range eventsOfType(g.Battle, "trumpet") {
if ev.Count < 0 {
spends++
}
}
if spends != 1 {
t.Fatalf("expected one trumpet spend, got %d", spends)
}
rocks := eventsOfType(g.Battle, "rock")
if len(rocks) != 1 || len(rocks[0].Dice) != 4 || !rocks[0].TargetDied {
t.Fatalf("Nurse Shark should throw 4 rocks (2 per Trumpet) and kill Tank: %+v", rocks)
}
if g.Battle.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", g.Battle.WinnerSeat)
}
}
// Choosing to spend zero Trumpets throws no rocks.
func TestNurseSharkSpendZero(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 2 }
forceBattle(t, g,
[]Card{g.goldenPet(t, "Nyala"), g.goldenPet(t, "Nurse Shark")},
[]Card{g.pet("Big", 4), g.pet("Tank", 6)},
)
if g.PendingBattle == nil {
t.Fatal("expected a pending decision")
}
if err := g.BattleChoose(g.Players[0].ID, 0); err != nil {
t.Fatal(err)
}
if len(eventsOfType(g.Battle, "rock")) != 0 {
t.Fatal("spending zero Trumpets should throw no rocks")
}
}
// With no Trumpets, Nurse Shark's choice is trivial (max 0) and the rollout
// path auto-resolves without suspending.
func TestNurseSharkAutoResolvesInRollout(t *testing.T) {
res := SimulateBattle(1, 0,
[]Card{cardWithName("Nurse Shark", 3, []Effect{{Trigger: TriggerPlay, Action: ActionSpendRocks, Count: 3}})},
[]Card{{ID: "x", Kind: KindPet, Name: "Foe", Power: 2}},
func() int { return 2 },
)
if res == nil || res.WinnerSeat < -1 {
t.Fatalf("rollout should complete without suspending: %+v", res)
}
}
func cardWithName(name string, power int, eff []Effect) Card {
return Card{ID: "t-" + name, Kind: KindPet, Name: name, Power: power, Effects: eff}
}
// --- Other tier 4-5 battle mechanics ---
// Vaquita doubles the side's Trumpets, capped at 4 gained.
func TestVaquitaDoublesTrumpets(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Nyala"), g.goldenPet(t, "Vaquita")}, // Nyala faint: +2
[]Card{g.pet("Big", 4), g.pet("Wall", 20)},
)
// Nyala trades with Big (+2 Trumpets), Vaquita enters and doubles to 4.
var gain *BattleEvent
for i, ev := range res.Events {
if ev.Type == "trumpet" && ev.Seat == 0 && ev.Count == 2 {
// two events: Nyala's +2 gain and Vaquita's +2 double.
gain = &res.Events[i]
}
}
if gain == nil {
t.Fatalf("Vaquita should double 2 Trumpets into 2 more: %+v", eventsOfType(res, "trumpet"))
}
}
// Manatee throws rocks at itself.
func TestManateeSelfRock(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 1 } // each rock deals 1
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Manatee")},
[]Card{g.pet("Wall", 20)},
)
var self *BattleEvent
for i, ev := range res.Events {
if ev.Type == "rock" && ev.Seat == 0 && ev.Target == 0 {
self = &res.Events[i]
}
}
if self == nil || self.Roll != 2 {
t.Fatalf("Manatee should pelt itself for 2: %+v", eventsOfType(res, "rock"))
}
// It also stacks 4 apples on top of the deck.
apples := 0
for _, ev := range eventsOfType(res, "summon") {
if ev.Card != nil && ev.Card.Name == "Apple" {
apples++
}
}
if apples != 4 {
t.Fatalf("Manatee should add 4 apples, got %d", apples)
}
}
// Poison Dart Frog, once set aside, throws rocks each time a Bee is played.
func TestPoisonDartFrogBeeRocks(t *testing.T) {
g, _, _ := testGame(t)
g.RollDie = func() int { return 2 }
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Poison Dart Frog"), g.newBee()},
[]Card{g.pet("Killer", 2), g.pet("Tank", 1)},
)
// Frog trades with Killer (set aside); the Bee enters and triggers 2 rocks
// (roll 4) that kill the 1-power Tank.
rocks := eventsOfType(res, "rock")
if len(rocks) != 1 || rocks[0].Roll != 4 || !rocks[0].TargetDied {
t.Fatalf("playing a Bee should throw Poison Dart Frog's 2 rocks: %+v", rocks)
}
if res.WinnerSeat != 0 {
t.Fatalf("seat 0 should win, got %d", res.WinnerSeat)
}
}
// Giant Isopod, once set aside, spends a Trumpet to feed each played pet.
func TestGiantIsopodFeedsOnPlay(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Nyala"), g.goldenPet(t, "Giant Isopod"), g.pet("Ally", 1)},
[]Card{g.pet("K1", 4), g.pet("K2", 4), g.pet("Wall", 1)},
)
// Nyala banks 2 Trumpets, Isopod faints (set aside), then Ally enters and is
// fed 2 apples (spending 1 Trumpet), reaching power 3 to beat the Wall.
fed := false
for _, ev := range eventsOfType(res, "eat") {
if ev.Seat == 0 && ev.Bonus == 2 {
fed = true
}
}
if !fed {
t.Fatalf("Giant Isopod should feed Ally 2 apples: %+v", eventsOfType(res, "eat"))
}
if res.WinnerSeat != 0 {
t.Fatalf("fed Ally should win, got %d", res.WinnerSeat)
}
}
// Raccoon steals the enemy perk onto the top of its own deck.
func TestRaccoonStealsPerk(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenPet(t, "Raccoon")},
[]Card{g.realFood(t, "Melon"), g.pet("Foe", 2)},
)
stole := false
for _, ev := range eventsOfType(res, "summon") {
if ev.Card != nil && ev.Card.Name == "Melon" && ev.Seat == 0 {
stole = true
}
}
if !stole {
t.Fatalf("Raccoon should summon the stolen Melon onto its own deck: %+v", eventsOfType(res, "summon"))
}
}
// Macaque recycles its perk and apples onto the top of the deck.
func TestMacaqueRecyclesPerkAndApples(t *testing.T) {
g, _, _ := testGame(t)
// Macaque wears Honey (a perk) and carries an apple, then faints.
res := forceBattle(t, g,
[]Card{g.realFood(t, "Honey"), g.newApple(), g.goldenPet(t, "Macaque")},
[]Card{g.pet("Big", 9)},
)
// On faint it puts the apple and the Honey perk back on top of the deck.
var sawApple, sawHoney bool
for _, ev := range eventsOfType(res, "summon") {
if ev.Card == nil || ev.Seat != 0 {
continue
}
switch ev.Card.Name {
case "Apple":
sawApple = true
case "Honey":
sawHoney = true
}
}
if !sawApple || !sawHoney {
t.Fatalf("Macaque should recycle its apple and Honey perk: %+v", eventsOfType(res, "summon"))
}
}
// Potato prevents 2 damage on each of the first two hits.
func TestPotatoPreventsTwice(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenFood(t, "Potato"), g.pet("Hero", 5)},
[]Card{g.pet("A", 3), g.pet("B", 3)},
)
prevents := eventsOfType(res, "prevent")
if len(prevents) != 2 {
t.Fatalf("Potato should prevent damage twice, got %d: %+v", len(prevents), prevents)
}
for _, p := range prevents {
if p.Count != 2 || p.Seat != 0 {
t.Fatalf("each prevention should shave 2 for seat 0: %+v", p)
}
}
if res.WinnerSeat != 0 {
t.Fatalf("Hero should survive both hits and win, got %d", res.WinnerSeat)
}
}
// Durian discards the enemy's apples in play.
func TestDurianStripsEnemyApples(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.goldenFood(t, "Durian"), g.pet("Hero", 3)},
[]Card{g.newApple(), g.newApple(), g.pet("Foe", 1)}, // Foe buffed to power 3
)
// Without the strip, a power-3 Foe would trade evenly; stripped back to
// power 1, it just dies.
if res.WinnerSeat != 0 {
t.Fatalf("Durian should strip Foe's apples so Hero wins, got %d", res.WinnerSeat)
}
if len(eventsOfType(res, "strip")) == 0 {
t.Fatal("expected a strip event")
}
}
// Saiga Antelope banks a Trumpet per friendly fainted pet on entry.
func TestSaigaTrumpetsPerFainted(t *testing.T) {
g, _, _ := testGame(t)
res := forceBattle(t, g,
[]Card{g.pet("F1", 1), g.pet("F2", 1), g.goldenPet(t, "Saiga Antelope")},
[]Card{g.pet("K1", 1), g.pet("K2", 1), g.pet("Last", 1)},
)
// F1/F2 trade with K1/K2 (2 friendly faints), then Saiga banks 2 Trumpets.
var gain *BattleEvent
for i, ev := range res.Events {
if ev.Type == "trumpet" && ev.Seat == 0 && ev.Count == 2 {
gain = &res.Events[i]
}
}
if gain == nil {
t.Fatalf("Saiga should bank 2 Trumpets for 2 friendly faints: %+v", eventsOfType(res, "trumpet"))
}
}
// --- Tier 4-5 shop mechanics ---
// Manta Ray waives the cost of the first buy each round (with few pets).
func TestMantaRayFirstBuyFree(t *testing.T) {
g, p1, _ := goldenGame(t)
p1.Deck = append(p1.Deck, g.goldenPet(t, "Manta Ray"))
g.startShopRound() // re-open the shop with Manta Ray in the deck
if !p1.FirstBuyFree {
t.Fatal("Manta Ray should ready a free first buy at 4 or fewer pets")
}
g.Turn = 0
coins := p1.Coins
g.ShopRow[0] = g.goldenPet(t, "Nyala")
if err := g.Buy(p1.ID, 0); err != nil {
t.Fatal(err)
}
if p1.Coins != coins {
t.Fatalf("the first buy should be free: coins %d -> %d", coins, p1.Coins)
}
if p1.FirstBuyFree {
t.Fatal("the free-buy flag should clear after use")
}
// The next buy costs a coin.
g.Turn = 0
if err := g.Buy(p1.ID, 1); err != nil {
t.Fatal(err)
}
if p1.Coins != coins-1 {
t.Fatalf("the second buy should cost a coin: %d", p1.Coins)
}
}
// Blue-Ringed Octopus grants an apple per buy made this round.
func TestOctopusApplesPerBuy(t *testing.T) {
g, p1, _ := goldenGame(t)
g.Turn = 0
g.ShopRow[0] = g.goldenPet(t, "Nyala") // a plain first buy
if err := g.Buy(p1.ID, 0); err != nil {
t.Fatal(err)
}
g.Turn = 0
g.ShopRow[1] = g.goldenPet(t, "Blue-Ringed Octopus") // the second buy
if err := g.Buy(p1.ID, 1); err != nil {
t.Fatal(err)
}
// Second buy: buysThisRound == 2 -> 2 apples.
if countApplesIn(p1.Deck) != 2 {
t.Fatalf("Octopus bought as the 2nd buy should add 2 apples, got %d", countApplesIn(p1.Deck))
}
}
// Cockatoo makes the buyer reveal a pet for apples equal to its power.
func TestCockatooReveal(t *testing.T) {
g, p1, _ := goldenGame(t)
beefy := g.pet("Beefy", 5)
p1.Deck = append(p1.Deck, beefy)
g.Turn = 0
g.ShopRow[0] = g.goldenPet(t, "Cockatoo")
if err := g.Buy(p1.ID, 0); err != nil {
t.Fatal(err)
}
if g.PendingReveal == nil || g.PendingReveal.PlayerID != p1.ID {
t.Fatalf("buying Cockatoo should open a reveal: %+v", g.PendingReveal)
}
if g.Turn != 0 {
t.Fatalf("the turn should stay on the buyer during the reveal, got %d", g.Turn)
}
if err := g.RevealChoose(p1.ID, beefy.ID); err != nil {
t.Fatal(err)
}
if g.PendingReveal != nil {
t.Fatal("the reveal should be resolved")
}
if countApplesIn(p1.Deck) != 5 {
t.Fatalf("revealing a power-5 pet should add 5 apples, got %d", countApplesIn(p1.Deck))
}
}