package game import "testing" // --- Nurse Shark --- // Nurse Shark automatically spends every available Trumpet (up to 3), throwing // two rocks per Trumpet spent. func TestNurseSharkSpendsAllTrumpets(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 // spends both: 4 rocks (2+2+2+2 = 8) kill the 6-power Tank. spends := 0 for _, ev := range eventsOfType(res, "trumpet") { if ev.Count < 0 { spends++ } } if spends != 1 { t.Fatalf("expected one trumpet spend, got %d", spends) } rocks := eventsOfType(res, "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 res.WinnerSeat != 0 { t.Fatalf("seat 0 should win, got %d", res.WinnerSeat) } } // The spend is capped at 3 Trumpets even when more are banked. func TestNurseSharkCapsAtThree(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 1 } // each rock deals 1 g.Players[0].PendingTrumpets = 5 // seed a pool larger than the cap res := forceBattle(t, g, []Card{g.goldenPet(t, "Nurse Shark")}, []Card{g.pet("Wall", 20)}, ) var spend *BattleEvent for i, ev := range res.Events { if ev.Type == "trumpet" && ev.Count < 0 { spend = &res.Events[i] } } if spend == nil || spend.Count != -3 { t.Fatalf("Nurse Shark should spend exactly 3 Trumpets: %+v", eventsOfType(res, "trumpet")) } rocks := eventsOfType(res, "rock") if len(rocks) != 1 || len(rocks[0].Dice) != 6 { t.Fatalf("spending 3 Trumpets should throw 6 rocks: %+v", rocks) } } // With no Trumpets, Nurse Shark throws no rocks and the battle still resolves. func TestNurseSharkNoTrumpets(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("battle should resolve: %+v", res) } if len(eventsOfType(res, "rock")) != 0 { t.Fatal("with no Trumpets, Nurse Shark should throw no rocks") } } 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) } } // Even when its own rocks are lethal, the Manatee still stacks all 4 apples — // the summon is posthumous, so it fires after the Manatee faints. func TestManateeSelfRockFatalStillAddsApples(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 2 } // 2 rocks = 4 damage, past its 3 power 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.Target == 0 { self = &res.Events[i] } } if self == nil || !self.TargetDied { t.Fatalf("Manatee's own rocks should faint it: %+v", eventsOfType(res, "rock")) } apples := 0 for _, ev := range eventsOfType(res, "summon") { if ev.Card != nil && ev.Card.Name == "Apple" { apples++ } } if apples != 4 { t.Fatalf("Manatee should still add 4 apples after fainting, got %d", apples) } } // A player whose only in-play pet faints to its own play effect (Manatee's // self-rock) still wins if it has pets left in its deck and the opponent has // none. The battle loop breaks the instant the opponent runs out — before the // survivor's next pet is revealed — so the winner must be decided by who can // still field a pet, not by who happens to have one in play at that instant. func TestSelfFaintWithReserveStillWins(t *testing.T) { g, _, _ := testGame(t) g.RollDie = func() int { return 2 } // 2 rocks = 4 damage, fatal to the 3-power Manatee res := forceBattle(t, g, []Card{g.goldenPet(t, "Manatee"), g.pet("Reserve", 5)}, []Card{g.newApple()}, // opponent has only food — no pet, ever ) if res.WinnerSeat != 0 { t.Fatalf("seat 0 should win with a reserve pet vs an all-food deck, got winner %d", res.WinnerSeat) } } // 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)) } }