package game import "testing" // testGame builds a started 2-player game without going through the lobby. func testGame(t *testing.T) (*Game, *Player, *Player) { t.Helper() g := New() p1, err := g.AddPlayer("Alice") if err != nil { t.Fatal(err) } p2, err := g.AddPlayer("Bob") if err != nil { t.Fatal(err) } if g.Phase != PhaseShop { t.Fatalf("expected shop phase after both players join, got %s", g.Phase) } return g, p1, p2 } func (g *Game) pet(name string, power int) Card { return Card{ID: g.newCardID(), Kind: KindPet, Name: name, Tier: 1, Power: power, Suit: SuitSun} } // forceBattle sets both decks, arranges them in current order, and resolves. func forceBattle(t *testing.T, g *Game, d1, d2 []Card) *BattleResult { t.Helper() g.Players[0].Deck = d1 g.Players[1].Deck = d2 g.Phase = PhaseArrange g.Players[0].Ready = false g.Players[1].Ready = false for _, p := range g.Players { ids := make([]string, len(p.Deck)) for i, c := range p.Deck { ids[i] = c.ID } if err := g.SubmitOrder(p.ID, ids); err != nil { t.Fatal(err) } } if g.Phase != PhaseBattle { t.Fatalf("expected battle phase, got %s", g.Phase) } return g.Battle } // The worked example from the rules: a 3-power pet fights a 5-power pet. The // 3 dies, the 5 survives with 3 damage markers (2 health left, 5 attack). // Then a 2-power pet trades with it: both die. func TestBattleDamageMarkers(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.pet("Three", 3), g.pet("Two", 2)}, []Card{g.pet("Five", 5)}, ) if len(res.Events) != 2 { t.Fatalf("expected 2 clashes, got %d", len(res.Events)) } first := res.Events[0] if !first.Died[0] || first.Died[1] { t.Fatalf("first clash: 3-power should die, 5-power should survive: %+v", first) } if first.Damage[1] != 3 { t.Fatalf("5-power pet should carry 3 damage, has %d", first.Damage[1]) } second := res.Events[1] if !second.Died[0] || !second.Died[1] { t.Fatalf("second clash: both should die (5 attack kills the 2; 3+2 damage kills the 5): %+v", second) } if res.WinnerSeat != -1 { t.Fatalf("battle should be a draw, winner=%d", res.WinnerSeat) } if g.Players[0].Trophies != 0 || g.Players[1].Trophies != 0 { t.Fatal("no trophies on a draw") } } func TestBattleEqualPowerBothDie(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.pet("A", 4)}, []Card{g.pet("B", 4)}, ) ev := res.Events[0] if !ev.Died[0] || !ev.Died[1] { t.Fatalf("equal power pets should both die: %+v", ev) } if res.WinnerSeat != -1 { t.Fatal("expected a draw") } } func TestBattleWinnerGetsTrophy(t *testing.T) { g, _, p2 := testGame(t) res := forceBattle(t, g, []Card{g.pet("Small", 1)}, []Card{g.pet("Big", 5)}, ) if res.WinnerSeat != 1 { t.Fatalf("seat 1 should win, got %d", res.WinnerSeat) } if res.Trophies != 1 || p2.Trophies != 1 { t.Fatalf("round 1 win should award 1 trophy, got %d/%d", res.Trophies, p2.Trophies) } } func TestBattleFinalRoundWorthTwoTrophies(t *testing.T) { g, _, p2 := testGame(t) g.Round = MaxRounds res := forceBattle(t, g, []Card{g.pet("Small", 1)}, []Card{g.pet("Big", 5)}, ) if res.Trophies != 2 || p2.Trophies != 2 { t.Fatalf("final round win should award 2 trophies, got %d/%d", res.Trophies, p2.Trophies) } } // Foods stack onto the next pet beneath them; each apple adds 1 power. // Trailing foods with no pet under them are wasted. func TestBattleApplesBuffNextPet(t *testing.T) { g, _, _ := testGame(t) apple1, apple2, apple3 := g.newApple(), g.newApple(), g.newApple() res := forceBattle(t, g, []Card{apple1, apple2, g.pet("Buffed", 3), apple3}, // 3+2=5 power; apple3 wasted []Card{g.pet("Enemy", 5)}, ) u := res.Lineups[0][0] if u.Bonus != 2 || u.Power() != 5 { t.Fatalf("expected 2 apples for 5 total power, got bonus=%d power=%d", u.Bonus, u.Power()) } if len(res.WastedFood[0]) != 1 || res.WastedFood[0][0].ID != apple3.ID { t.Fatalf("trailing apple should be wasted: %+v", res.WastedFood[0]) } if res.WinnerSeat != -1 { t.Fatal("5 vs 5 should draw") } } // A player with an empty lineup loses immediately with zero clashes. func TestBattleEmptyLineupLoses(t *testing.T) { g, _, _ := testGame(t) res := forceBattle(t, g, []Card{g.pet("Solo", 1)}, []Card{g.newApple()}, // food only, no pets ) if len(res.Events) != 0 { t.Fatalf("expected no clashes, got %d", len(res.Events)) } if res.WinnerSeat != 0 { t.Fatalf("seat 0 should win by default, got %d", res.WinnerSeat) } }