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

969 lines
29 KiB
Go

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.Pack = "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)
}
}