969 lines
29 KiB
Go
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.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)
|
|
}
|
|
}
|