package game import "testing" // --- Unicorn pack test helpers --- // unicornGame builds a started 2-player game on the Unicorn pack, bypassing the // lobby and the Playable gate (tiers 4-6 aren't printed yet). func unicornGame(t *testing.T) (*Game, *Player, *Player) { t.Helper() g := New() g.Packs = []string{"unicorn"} 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 } // unicornPet mints a copy of a Unicorn pack pet (with effects) by name. func (g *Game) unicornPet(t *testing.T, name string) Card { t.Helper() for tierIdx, tier := range unicornPetTiers { 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 unicorn pet named %s", name) return Card{} } // unicornFood mints a Unicorn pack food by name. func (g *Game) unicornFood(t *testing.T, name string) Card { t.Helper() for tierIdx, tier := range unicornFoodTiers { 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 unicorn food named %s", name) return Card{} } func manaEvents(res *BattleResult) []BattleEvent { return eventsOfType(res, "mana") } // --- Mana --- // Cuddle Toad's Buy grants persistent Mana that survives into later rounds. func TestCuddleToadShopMana(t *testing.T) { g, p1, _ := unicornGame(t) g.ShopRow[0] = g.unicornPet(t, "Cuddle Toad") if err := g.Buy(p1.ID, 0); err != nil { t.Fatal(err) } if p1.Mana != 1 { t.Fatalf("Cuddle Toad should grant 1 Mana on buy, got %d", p1.Mana) } } // Thunderbird grants 2 Mana on buy. func TestThunderbirdShopMana(t *testing.T) { g, p1, _ := unicornGame(t) g.ShopRow[0] = g.unicornPet(t, "Thunderbird") if err := g.Buy(p1.ID, 0); err != nil { t.Fatal(err) } if p1.Mana != 2 { t.Fatalf("Thunderbird should grant 2 Mana, got %d", p1.Mana) } } // Alchemedes gains Mana on play, and it persists on the player after the battle. func TestAlchemedesBattleManaPersists(t *testing.T) { g, p1, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Alchemedes"), g.pet("Body", 3)}, []Card{g.pet("Weak", 1)}, ) gains := 0 for _, ev := range manaEvents(res) { if ev.Seat == 0 && ev.Count > 0 { gains += ev.Count } } if gains != 1 { t.Fatalf("Alchemedes should gain 1 Mana in battle, got %d", gains) } if p1.Mana != 1 { t.Fatalf("Mana should persist on the player after battle, got %d", p1.Mana) } } // Pengobble spends banked Mana to throw 2 Rocks; with no Mana it does nothing. func TestPengobbleSpendsMana(t *testing.T) { g, p1, _ := unicornGame(t) g.RollDie = func() int { return 2 } // each rock deals 2 p1.Mana = 1 res := forceBattle(t, g, []Card{g.unicornPet(t, "Pengobble")}, []Card{g.pet("Tank", 3)}, ) rocks := eventsOfType(res, "rock") if len(rocks) != 1 || rocks[0].Roll != 4 || !rocks[0].TargetDied { t.Fatalf("Pengobble should spend Mana to throw 2 rocks (4 dmg, kills 3-tank): %+v", rocks) } if p1.Mana != 0 { t.Fatalf("Pengobble should have spent the Mana, left %d", p1.Mana) } } func TestPengobbleNoManaNoRocks(t *testing.T) { g, _, _ := unicornGame(t) g.RollDie = func() int { return 2 } res := forceBattle(t, g, []Card{g.unicornPet(t, "Pengobble")}, []Card{g.pet("Tank", 3)}, ) if rocks := eventsOfType(res, "rock"); len(rocks) != 0 { t.Fatalf("Pengobble with no Mana should throw no rocks, got %+v", rocks) } } // Tatzelwurm gains 1 Mana per blank rolled while still dealing rock damage. func TestTatzelwurmManaOnBlanks(t *testing.T) { g, p1, _ := unicornGame(t) g.RollDie = func() int { return 0 } // both rocks blank res := forceBattle(t, g, []Card{g.unicornPet(t, "Tatzelwurm"), g.pet("Body", 3)}, []Card{g.pet("Tank", 3)}, ) gains := 0 for _, ev := range manaEvents(res) { if ev.Seat == 0 && ev.Count > 0 { gains += ev.Count } } if gains != 2 { t.Fatalf("Tatzelwurm should gain 2 Mana from 2 blanks, got %d", gains) } if p1.Mana != 2 { t.Fatalf("Tatzelwurm Mana should persist, got %d", p1.Mana) } } // Gargoyle spends Mana on faint to bank 2 apples on top of its deck. func TestGargoyleFaintSpendsMana(t *testing.T) { g, _, _ := unicornGame(t) g.Players[0].Mana = 1 res := forceBattle(t, g, []Card{g.unicornPet(t, "Gargoyle")}, []Card{g.pet("Killer", 5)}, ) apples := 0 for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 0 && ev.Card != nil && ev.Card.Food == FoodApple { apples++ } } if apples != 2 { t.Fatalf("Gargoyle should summon 2 apples when it can pay Mana, got %d", apples) } } // Fairy Dust (perk) grants its pet 1 Mana when the pet is played in battle. func TestFairyDustPerkMana(t *testing.T) { g, p1, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornFood(t, "Fairy Dust"), g.pet("Body", 4)}, []Card{g.pet("Weak", 1)}, ) gains := 0 for _, ev := range manaEvents(res) { if ev.Seat == 0 && ev.Count > 0 { gains += ev.Count } } if gains != 1 { t.Fatalf("Fairy Dust should grant 1 Mana on play, got %d", gains) } if p1.Mana != 1 { t.Fatalf("Fairy Dust Mana should persist, got %d", p1.Mana) } } // --- Ailments --- // Basilisk's Exposed makes the enemy pet take extra damage on each hit — a // 1-power Basilisk trades into a 2-power pet it could never otherwise kill. func TestBasiliskExposedKills(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Basilisk"), g.pet("Cleanup", 2)}, []Card{g.pet("Two", 2)}, ) if len(eventsOfType(res, "ailment")) == 0 { t.Fatal("expected an ailment event from Basilisk") } // Basilisk (1) + 1 Exposed deals 2 to Two (2) — it faints in the clash; the // cleanup pet then wins for seat 0. if res.WinnerSeat != 0 { t.Fatalf("Exposed should let Basilisk's side win, got winner %d", res.WinnerSeat) } } // Nightcrawler's 2 Spooked cut the enemy pet's clash attack. func TestNightcrawlerSpookedReducesAttack(t *testing.T) { g, _, _ := unicornGame(t) // Seat 0: Nightcrawler(1) spooks the enemy by 2, then a 3-power body. // Seat 1: a 3-power attacker. Spooked drops its attack to 1, so the body // survives and seat 0 wins. res := forceBattle(t, g, []Card{g.unicornPet(t, "Nightcrawler"), g.pet("Body", 3)}, []Card{g.pet("Bruiser", 3)}, ) if res.WinnerSeat != 0 { t.Fatalf("Spooked should weaken the Bruiser enough for seat 0 to win, got %d", res.WinnerSeat) } } // Baku shrugs off the first ailment it would receive. func TestBakuDiscardsFirstAilment(t *testing.T) { g, _, _ := unicornGame(t) // Seat 1 leads with Barghest (spooks Baku by 1) — Baku's guard eats it, so // Baku attacks at full power. res := forceBattle(t, g, []Card{g.unicornPet(t, "Baku")}, []Card{g.unicornPet(t, "Barghest"), g.pet("Body", 2)}, ) // Barghest (1) spooks Baku, but the guard cancels it. Baku(2) vs Barghest(1): // Baku deals 2, Barghest dies; Baku takes 1, survives. Then Baku(2, 1 dmg) // vs Body(2): Baku deals 2 → Body dies; Body deals 2 → Baku total 3 ≥ 2 dies. // Both empty ⇒ draw. Without the guard Baku would deal only 1 and lose. So a // non-loss for seat 0 proves the guard worked. if res.WinnerSeat == 1 { t.Fatal("Baku should not lose — its guard cancels Barghest's Spooked") } ail := eventsOfType(res, "ailment") if len(ail) != 1 || ail[0].Count != 0 { t.Fatalf("Baku's guard should record a shrugged-off ailment (count 0), got %+v", ail) } } // Frost Wolf's faint exposes the enemy pet and drops another Exposed on top of // the enemy deck for the next pet. func TestFrostWolfExposes(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Frost Wolf")}, []Card{g.pet("A", 2), g.pet("B", 2)}, ) // One Exposed on the current enemy pet (an ailment event) and one on top of // the enemy deck (a summon of an ailment card onto seat 1). if len(eventsOfType(res, "ailment")) == 0 { t.Fatal("Frost Wolf should afflict the enemy pet in play") } deckAilment := false for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 1 && ev.Card != nil && ev.Card.IsAilment() { deckAilment = true } } if !deckAilment { t.Fatal("Frost Wolf should drop an Exposed on top of the enemy deck") } } // Mothman eats 2 apples only when the enemy pet already carries an ailment. func TestMothmanEatsOnEnemyAilment(t *testing.T) { g, _, _ := unicornGame(t) // Seat 1 leads with Basilisk, which exposes seat 0's first pet (Mothman). // Wait — Mothman checks the ENEMY's ailment. Put the ailment on the enemy: // seat 0 = [Barghest (spooks enemy), Mothman]; when Mothman enters, the enemy // pet is spooked, so Mothman eats. res := forceBattle(t, g, []Card{g.unicornPet(t, "Barghest"), g.unicornPet(t, "Mothman"), g.pet("Body", 5)}, []Card{g.pet("Tank", 8)}, ) ate := false for _, ev := range eventsOfType(res, "eat") { if ev.Seat == 0 { ate = true } } if !ate { t.Fatal("Mothman should eat when the enemy pet is afflicted") } } func TestMothmanNoEatWithoutAilment(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Mothman"), g.pet("Body", 5)}, []Card{g.pet("Tank", 8)}, ) for _, ev := range eventsOfType(res, "eat") { if ev.Seat == 0 { t.Fatal("Mothman should not eat when the enemy pet has no ailment") } } } // Calygreyhound spends Mana to eat an apple and spook the enemy. func TestCalygreyhoundSpendsMana(t *testing.T) { g, _, _ := unicornGame(t) g.Players[0].Mana = 1 res := forceBattle(t, g, []Card{g.unicornPet(t, "Calygreyhound"), g.pet("Body", 3)}, []Card{g.pet("Tank", 6)}, ) if len(eventsOfType(res, "ailment")) == 0 { t.Fatal("Calygreyhound should spook the enemy when it can pay Mana") } ate := false for _, ev := range eventsOfType(res, "eat") { if ev.Seat == 0 { ate = true } } if !ate { t.Fatal("Calygreyhound should also eat an apple") } if g.Players[0].Mana != 0 { t.Fatalf("Calygreyhound should have spent the Mana, left %d", g.Players[0].Mana) } } // --- Faint mechanics --- // Fur-Bearing Trout gains Mana and banks 2 apples when it faints. func TestFurBearingTroutFaint(t *testing.T) { g, p1, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Fur-Bearing Trout")}, []Card{g.pet("Killer", 5)}, ) if p1.Mana != 1 { t.Fatalf("Fur-Bearing Trout should bank 1 Mana on faint, got %d", p1.Mana) } apples := 0 for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 0 && ev.Card != nil && ev.Card.Food == FoodApple { apples++ } } if apples != 2 { t.Fatalf("Fur-Bearing Trout should summon 2 apples on faint, got %d", apples) } } // Skeleton Dog banks an apple for the next round's hand. func TestSkeletonDogBanksNextRoundApple(t *testing.T) { g, p1, _ := unicornGame(t) forceBattle(t, g, []Card{g.unicornPet(t, "Skeleton Dog")}, []Card{g.pet("Killer", 5)}, ) if p1.NextRoundApples != 1 { t.Fatalf("Skeleton Dog should bank 1 next-round apple, got %d", p1.NextRoundApples) } // Acknowledge the battle and enter the next round; the apple should land. before := countFood(p1.Deck, FoodApple) g.AcknowledgeBattle(p1.ID) g.AcknowledgeBattle(g.Players[1].ID) if g.Phase != PhaseShop { t.Fatalf("expected a new shop round, got %s", g.Phase) } after := countFood(p1.Deck, FoodApple) if after != before+1 { t.Fatalf("banked apple should arrive next round: before=%d after=%d", before, after) } if p1.NextRoundApples != 0 { t.Fatalf("the bank should reset after paying out, got %d", p1.NextRoundApples) } } func countFood(deck []Card, food string) int { n := 0 for _, c := range deck { if c.Food == food { n++ } } return n } // Mandrake cancels the next enemy Faint ability. Mandrake must faint first to // arm the negation, so seat 0's Heavy kills it, then seat 0's Ant later faints // against seat 1's Big — and its apple is cancelled. func TestMandrakeNegatesEnemyFaint(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.pet("Heavy", 3), g.tier1(t, "Ant")}, []Card{g.unicornPet(t, "Mandrake"), g.pet("Big", 6)}, ) for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 0 && ev.Card != nil && ev.Card.Food == FoodApple { t.Fatal("Ant's faint ability should have been cancelled by Mandrake") } } // Sanity: without the negation the Ant's apple would appear — confirm the // Mandrake actually got set aside and released as a cancel. sawCancel := false for _, ev := range eventsOfType(res, "release") { if ev.Card != nil && ev.Card.Name == "Mandrake" { sawCancel = true } } if !sawCancel { t.Fatal("expected the Mandrake to spend its negation on Ant's faint") } } // Slime revives itself once and drops an Exposed on the enemy deck. func TestSlimeReviveOnce(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Slime")}, []Card{g.pet("A", 2), g.pet("B", 2), g.pet("C", 2)}, ) revives := 0 for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 0 && ev.Card != nil && ev.Card.Name == "Slime" { revives++ } } if revives != 1 { t.Fatalf("Slime should revive exactly once, got %d", revives) } deckAilment := false for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 1 && ev.Card != nil && ev.Card.IsAilment() { deckAilment = true } } if !deckAilment { t.Fatal("Slime should drop an Exposed on the enemy deck") } } // --- Shop foods & abilities --- // Lucky Cat's Triple adds 3 apples to the hand. func TestLuckyCatTriple(t *testing.T) { g, p1, _ := unicornGame(t) // Three same-suit pets including Lucky Cat, so the Triple is legal. lc := g.unicornPet(t, "Lucky Cat") // red a := g.suitedPet("A", 1, SuitRed) b := g.suitedPet("B", 1, SuitRed) p1.Deck = []Card{lc, a, b} before := countFood(p1.Deck, FoodApple) if err := g.TradeStart(p1.ID, []string{lc.ID, a.ID, b.ID}); err != nil { t.Fatal(err) } after := countFood(p1.Deck, FoodApple) if after != before+3 { t.Fatalf("Lucky Cat Triple should add 3 apples: before=%d after=%d", before, after) } } // Water of Youth: buying it opens a sacrifice choice; resolving it discards the // chosen pet and the food, and grants a next-tier pet for free. func TestWaterOfYouthUpgrade(t *testing.T) { g, p1, _ := unicornGame(t) junk := g.suitedPet("Junk", 1, SuitRed) p1.Deck = []Card{junk} // Pin the top of the next tier's deck to a known pet with a Buy effect. g.ShopDecks[1] = append([]Card{g.unicornPet(t, "Thunderbird")}, g.ShopDecks[1]...) g.ShopRow[0] = g.unicornFood(t, "Water of Youth") if err := g.Buy(p1.ID, 0); err != nil { t.Fatal(err) } if g.PendingSacrifice == nil || g.PendingSacrifice.PlayerID != p1.ID { t.Fatal("buying Water of Youth should open a sacrifice choice") } if err := g.SacrificeChoose(p1.ID, junk.ID); err != nil { t.Fatal(err) } if p1.cardIndex(junk.ID) >= 0 { t.Fatal("the sacrificed pet should be gone") } for _, c := range p1.Deck { if c.Food == FoodWaterOfYouth { t.Fatal("Water of Youth food should be consumed") } } gotUpgrade := false for _, c := range p1.Deck { if c.IsPet() && c.Name == "Thunderbird" { gotUpgrade = true } } if !gotUpgrade { t.Fatalf("expected the Thunderbird upgrade from the next tier, deck=%+v", p1.Deck) } // The granted pet's Buy effect fires: Thunderbird grants 2 Mana. if p1.Mana != 2 { t.Fatalf("upgrade's Buy effect should fire (Thunderbird → 2 Mana), got %d", p1.Mana) } } // Bigfoot lets its owner peek at the top of the shop deck, once per round. func TestBigfootPeek(t *testing.T) { g, p1, _ := unicornGame(t) p1.Deck = []Card{g.unicornPet(t, "Bigfoot")} if err := g.PeekShopDeck(p1.ID); err != nil { t.Fatal(err) } if p1.ShopPeek == nil { t.Fatal("peeking should reveal the top of the shop deck") } top := g.ShopDecks[g.Round-1][0] if p1.ShopPeek.ID != top.ID { t.Fatalf("peek should show the deck's top card %s, got %s", top.Name, p1.ShopPeek.Name) } if err := g.PeekShopDeck(p1.ID); err == nil { t.Fatal("Bigfoot should only peek once per round") } } func TestPeekRequiresBigfoot(t *testing.T) { g, p1, _ := unicornGame(t) p1.Deck = []Card{g.pet("Plain", 2)} if err := g.PeekShopDeck(p1.ID); err == nil { t.Fatal("peeking without a Bigfoot should be rejected") } } // --- Tier 4-6 --- func seat0ManaGains(res *BattleResult) int { n := 0 for _, ev := range manaEvents(res) { if ev.Seat == 0 && ev.Count > 0 { n += ev.Count } } return n } func seat0Summons(res *BattleResult, name string) int { n := 0 for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 0 && ev.Card != nil && ev.Card.Name == name { n++ } } return n } // Roc grants Mana on both Play (battle) and Sell (shop). func TestRocMana(t *testing.T) { g, p1, _ := unicornGame(t) // Sell grants shop-time mana. roc := g.unicornPet(t, "Roc") p1.Deck = []Card{roc, g.pet("Keep", 2)} if err := g.Sell(p1.ID, []string{roc.ID}); err != nil { t.Fatal(err) } if p1.Mana != 1 { t.Fatalf("Roc Sell should grant 1 Mana, got %d", p1.Mana) } // Play grants battle-time mana too. res := forceBattle(t, g, []Card{g.unicornPet(t, "Roc")}, []Card{g.pet("Weak", 1)}, ) if seat0ManaGains(res) != 1 { t.Fatalf("Roc Play should grant 1 Mana, got %d", seat0ManaGains(res)) } } // Kraken spooks the enemy pet and drops a Spooked on the enemy deck. func TestKrakenSpooks(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Kraken")}, []Card{g.pet("A", 3), g.pet("B", 3)}, ) if len(eventsOfType(res, "ailment")) == 0 { t.Fatal("Kraken should spook the enemy pet") } deckAilment := false for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 1 && ev.Card != nil && ev.Card.IsAilment() { deckAilment = true } } if !deckAilment { t.Fatal("Kraken should drop a Spooked on the enemy deck") } } // Unicorn's guard converts the next incoming friendly ailment into 2 apples. func TestUnicornAilmentGuard(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Unicorn"), g.pet("Tank", 10)}, []Card{g.pet("Sword", 4), g.unicornPet(t, "Nightcrawler")}, ) // Unicorn(4) trades with Sword(4) and faints, arming the guard. Nightcrawler // then tries to add 2 Spooked to the Tank; the guard turns the first into 2 // apples on our deck (a release + apple summons), the second lands. released := false for _, ev := range eventsOfType(res, "release") { if ev.Card != nil && ev.Card.Name == "Unicorn" { released = true } } if !released { t.Fatal("Unicorn's guard should fire against the incoming Spooked") } if seat0Summons(res, "Apple") < 2 { t.Fatalf("the guard should add 2 apples on our deck, got %d", seat0Summons(res, "Apple")) } } // Rootlin buffs friendly pets with base Power 2 or less. func TestRootlinSmallPetAura(t *testing.T) { g, _, _ := unicornGame(t) // Rootlin faints to a 7-power Killer, arming +1 for small pets. A 2-power // Small then hits at 3, exactly killing the wounded Killer — a mutual KO // (draw). Without the aura the Small would deal 2 and lose outright. res := forceBattle(t, g, []Card{g.unicornPet(t, "Rootlin"), g.pet("Small", 2)}, []Card{g.pet("Killer", 7)}, ) if res.WinnerSeat == 1 { t.Fatal("Rootlin's aura should let the Small pet trade with the Killer (draw, not a loss)") } } // Fairy recycles the next friendly faint to the bottom of the deck. func TestFairyRecyclesNextFaint(t *testing.T) { g, p1, _ := unicornGame(t) p1.Mana = 3 res := forceBattle(t, g, []Card{g.unicornPet(t, "Fairy"), g.tier1(t, "Ant")}, []Card{g.pet("Killer", 5)}, ) // Fairy(1) dies to Killer, spends 3 Mana, arms the guard. Ant then dies and // is sent to the bottom of our deck as a fresh copy. recycled := false for _, ev := range eventsOfType(res, "summon") { if ev.Seat == 0 && ev.Card != nil && ev.Card.Name == "Ant" { recycled = true } } if !recycled { t.Fatal("Fairy should recycle the Ant to the bottom of the deck") } if p1.Mana != 0 { t.Fatalf("Fairy should have spent 3 Mana, left %d", p1.Mana) } } // Health Potion heals damage when its pet survives a hit. func TestHealthPotionHeals(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornFood(t, "Health Potion"), g.pet("Big", 10)}, []Card{g.pet("Chip", 3)}, ) if len(eventsOfType(res, "heal")) == 0 { t.Fatal("Health Potion should heal the pet after it is hurt") } if res.WinnerSeat != 0 { t.Fatalf("the healed 10-power pet should win, got %d", res.WinnerSeat) } } // Kitsune gains Mana per friendly fainted pet at play time. func TestKitsuneManaPerFaint(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.pet("Weak", 1), g.unicornPet(t, "Kitsune"), g.pet("Big", 5)}, []Card{g.pet("Mid", 3)}, ) // Weak faints (1 friendly fainted), Kitsune enters and gains 1 Mana. if seat0ManaGains(res) != 1 { t.Fatalf("Kitsune should gain 1 Mana for the 1 fainted pet, got %d", seat0ManaGains(res)) } } // Werewolf eats 6 apples only on even rounds. func TestWerewolfEvenRound(t *testing.T) { g, _, _ := unicornGame(t) g.Round = 2 res := forceBattle(t, g, []Card{g.unicornPet(t, "Werewolf"), g.pet("Body", 3)}, []Card{g.pet("Tank", 12)}, ) fed := false for _, ev := range eventsOfType(res, "eat") { if ev.Seat == 0 && ev.Bonus >= 6 { fed = true } } if !fed { t.Fatal("Werewolf should eat 6 apples on an even round") } g2, _, _ := unicornGame(t) g2.Round = 1 res2 := forceBattle(t, g2, []Card{g2.unicornPet(t, "Werewolf"), g2.pet("Body", 3)}, []Card{g2.pet("Tank", 12)}, ) for _, ev := range eventsOfType(res2, "eat") { if ev.Seat == 0 { t.Fatal("Werewolf should not eat on an odd round") } } } // Loveland Frogman bounces the enemy pet to the bottom of the enemy deck. func TestLovelandBounce(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Loveland Frogman"), g.pet("Body", 5)}, []Card{g.pet("Enemy", 3), g.pet("Behind", 3)}, ) if len(eventsOfType(res, "bounce")) == 0 { t.Fatal("Loveland Frogman should bounce the enemy pet") } } // Sleipnir's base Power equals the owner's Mana (capped). func TestSleipnirManaPower(t *testing.T) { g, p1, _ := unicornGame(t) p1.Mana = 5 res := forceBattle(t, g, []Card{g.unicornPet(t, "Sleipnir")}, []Card{g.pet("Four", 4)}, ) var reveal *BattleEvent for i, ev := range res.Events { if ev.Type == "reveal" && ev.Card != nil && ev.Card.Name == "Sleipnir" { reveal = &res.Events[i] } } if reveal == nil || reveal.Card.Power != 5 { t.Fatalf("Sleipnir should enter with Power 5 from 5 Mana, got %+v", reveal) } if res.WinnerSeat != 0 { t.Fatalf("the 5-power Sleipnir should beat the 4-power pet, got %d", res.WinnerSeat) } } // Sea Serpent spends all Mana to throw that many Rocks. func TestSeaSerpentSpendManaRocks(t *testing.T) { g, p1, _ := unicornGame(t) p1.Mana = 3 g.RollDie = func() int { return 2 } res := forceBattle(t, g, []Card{g.unicornPet(t, "Sea Serpent")}, []Card{g.pet("Tank", 6)}, ) rocks := eventsOfType(res, "rock") if len(rocks) != 1 || rocks[0].Roll != 6 || !rocks[0].TargetDied { t.Fatalf("Sea Serpent should spend 3 Mana for 3 rocks (6 dmg): %+v", rocks) } if p1.Mana != 0 { t.Fatalf("Sea Serpent should spend all Mana, left %d", p1.Mana) } } // Bakunawa spends all Mana to spook the enemy pet. func TestBakunawaSpendManaSpook(t *testing.T) { g, p1, _ := unicornGame(t) p1.Mana = 3 res := forceBattle(t, g, []Card{g.unicornPet(t, "Bakunawa"), g.pet("Body", 3)}, []Card{g.pet("Bruiser", 4)}, ) spook := 0 for _, ev := range eventsOfType(res, "ailment") { if ev.Seat == 1 && ev.Card != nil && ev.Card.Ailment == AilmentSpooked { spook += ev.Count } } if spook != 3 { t.Fatalf("Bakunawa should add 3 Spooked, got %d", spook) } if p1.Mana != 0 { t.Fatalf("Bakunawa should spend all Mana, left %d", p1.Mana) } } // Manticore boosts the value of Ailments on enemy pets by 1. func TestManticoreBoostsEnemyAilments(t *testing.T) { g, _, _ := unicornGame(t) // Manticore faints to a 12-power BigKiller, arming +1 to enemy ailments. // Basilisk then exposes the BigKiller: its hit lands for 1 + 1(Exposed) + // 1(Manticore) = 3, so the wounded BigKiller reaches 6+3 = 9 damage. res := forceBattle(t, g, []Card{g.unicornPet(t, "Manticore"), g.unicornPet(t, "Basilisk")}, []Card{g.pet("BigKiller", 12)}, ) got := 0 for _, ev := range eventsOfType(res, "clash") { if len(ev.Damage) == 2 { got = ev.Damage[1] } } if got != 9 { t.Fatalf("Manticore-boosted Exposed should push BigKiller to 9 damage, got %d", got) } } // Behemoth eats 2 apples after every clash it survives (not once per battle). func TestBehemothEatsEveryClash(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornPet(t, "Behemoth")}, []Card{g.pet("Chip1", 1), g.pet("Chip2", 1), g.pet("Chip3", 1)}, ) eats := 0 for _, ev := range eventsOfType(res, "eat") { if ev.Seat == 0 { eats++ } } if eats < 2 { t.Fatalf("Behemoth should eat after each of several clashes, got %d eats", eats) } } // Chimera spends Mana to summon random cards from the tier 1 discard pile. func TestChimeraSummonFromDiscard(t *testing.T) { g, _, _ := unicornGame(t) g.Players[0].Mana = 4 g.Discards = map[int][]Card{1: {g.pet("Discarded", 2)}} res := forceBattle(t, g, []Card{g.unicornPet(t, "Chimera")}, []Card{g.pet("Killer", 5)}, ) if seat0Summons(res, "Discarded") != 2 { t.Fatalf("Chimera should summon 2 cards from the discard pile, got %d", seat0Summons(res, "Discarded")) } if g.Players[0].Mana != 0 { t.Fatalf("Chimera should spend 4 Mana, left %d", g.Players[0].Mana) } } // Abomination mimics a discard pet's ability, firing its Play effect at once. func TestAbominationMimicsPlayAbility(t *testing.T) { g, _, _ := unicornGame(t) // The tier 1 discard pile holds a Barghest (Play → Spook the enemy pet). g.Discards = map[int][]Card{1: {g.unicornPet(t, "Barghest")}} res := forceBattle(t, g, []Card{g.unicornPet(t, "Abomination"), g.pet("Body", 4)}, []Card{g.pet("Tank", 10)}, ) if len(eventsOfType(res, "ailment")) == 0 { t.Fatal("Abomination should immediately fire the mimicked Barghest Play ability") } } // Abomination's mimicked non-Play ability fires on its natural trigger later. func TestAbominationMimicsFaintAbility(t *testing.T) { g, _, _ := unicornGame(t) // Discard holds an Ant (Faint → apple on deck); Abomination gains it and the // apple appears when Abomination faints. g.Discards = map[int][]Card{1: {g.tier1(t, "Ant")}} res := forceBattle(t, g, []Card{g.unicornPet(t, "Abomination")}, []Card{g.pet("Killer", 12)}, ) if seat0Summons(res, "Apple") == 0 { t.Fatal("Abomination should gain the Ant's Faint ability and drop an apple on faint") } } // Pixiu spends Mana to summon the top of the tier 6 shop deck. func TestPixiuSummonFromTierDeck(t *testing.T) { g, _, _ := unicornGame(t) g.Players[0].Mana = 4 g.ShopDecks[5] = append([]Card{g.pet("SixTop", 6)}, g.ShopDecks[5]...) res := forceBattle(t, g, []Card{g.unicornPet(t, "Pixiu")}, []Card{g.pet("Killer", 5)}, ) if seat0Summons(res, "SixTop") != 1 { t.Fatalf("Pixiu should summon the top tier 6 card, got %d", seat0Summons(res, "SixTop")) } } // Vampire Bat throws rocks and eats apples equal to the damage dealt, when the // enemy pet is ailing. func TestVampireBatRockThenEat(t *testing.T) { g, _, _ := unicornGame(t) g.RollDie = func() int { return 2 } // each rock deals 2 res := forceBattle(t, g, []Card{g.unicornPet(t, "Barghest"), g.unicornPet(t, "Vampire Bat"), g.pet("Body", 3)}, []Card{g.pet("Tank", 20)}, ) // Barghest spooks the Tank (an ailment); Vampire Bat then throws 2 rocks (4 // damage) and eats 4 apples. fed := false for _, ev := range eventsOfType(res, "eat") { if ev.Seat == 0 && ev.Bonus >= 4 { fed = true } } if !fed { t.Fatal("Vampire Bat should eat apples equal to the rock damage dealt") } } // Cornucopia (perk) feeds its pet 2 apples and adds 2 on top of the deck. func TestCornucopia(t *testing.T) { g, _, _ := unicornGame(t) res := forceBattle(t, g, []Card{g.unicornFood(t, "Cornucopia"), g.pet("Body", 4)}, []Card{g.pet("Weak", 1)}, ) fed := false for _, ev := range eventsOfType(res, "eat") { if ev.Seat == 0 && ev.Bonus >= 2 { fed = true } } if !fed { t.Fatal("Cornucopia should feed its pet 2 apples") } if seat0Summons(res, "Apple") < 2 { t.Fatalf("Cornucopia should add 2 apples on the deck, got %d", seat0Summons(res, "Apple")) } } // Quetzalcoatl reveals a tier 3 or lower pet on buy for 3 apples. func TestQuetzalcoatlReveal(t *testing.T) { g, p1, _ := unicornGame(t) low := g.pet("Low", 2) // tier 1 p1.Deck = []Card{low} g.ShopRow[0] = g.unicornPet(t, "Quetzalcoatl") if err := g.Buy(p1.ID, 0); err != nil { t.Fatal(err) } if g.PendingReveal == nil || g.PendingReveal.PlayerID != p1.ID { t.Fatal("buying Quetzalcoatl should open a reveal") } before := countFood(p1.Deck, FoodApple) if err := g.RevealChoose(p1.ID, low.ID); err != nil { t.Fatal(err) } if got := countFood(p1.Deck, FoodApple) - before; got != 3 { t.Fatalf("Quetzalcoatl should grant a fixed 3 apples, got %d", got) } }