diff --git a/packages/server/tests/game-manager.test.ts b/packages/server/tests/game-manager.test.ts new file mode 100644 index 0000000..f37307e --- /dev/null +++ b/packages/server/tests/game-manager.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, beforeEach } from "vitest" +import { GameManager } from "../src/games/game-manager" + +describe("GameManager", () => { + let gm: GameManager + + beforeEach(() => { + gm = new GameManager() + }) + + describe("lineup", () => { + it("returns the ESC 2026 lineup", () => { + const lineup = gm.getLineup() + expect(lineup.year).toBe(2026) + expect(lineup.countries.length).toBeGreaterThan(20) + expect(lineup.countries[0]).toHaveProperty("code") + expect(lineup.countries[0]).toHaveProperty("name") + }) + + it("validates country codes", () => { + expect(gm.isValidCountry("DE")).toBe(true) + expect(gm.isValidCountry("XX")).toBe(false) + }) + }) + + describe("predictions", () => { + it("accepts a valid prediction", () => { + const result = gm.submitPrediction("p1", "SE", ["DE", "IT", "FR"], "GB") + expect(result).toEqual({ success: true }) + expect(gm.getPrediction("p1")).toEqual({ + playerId: "p1", + predictedWinner: "SE", + top3: ["DE", "IT", "FR"], + nulPointsPick: "GB", + }) + }) + + it("rejects prediction with invalid country", () => { + const result = gm.submitPrediction("p1", "XX", ["DE", "IT", "FR"], "GB") + expect(result).toEqual({ error: "Invalid country: XX" }) + }) + + it("rejects winner in top 3", () => { + const result = gm.submitPrediction("p1", "SE", ["SE", "IT", "FR"], "GB") + expect(result).toEqual({ error: "Winner cannot also be in top 3" }) + }) + + it("rejects duplicate top 3", () => { + const result = gm.submitPrediction("p1", "SE", ["DE", "DE", "FR"], "GB") + expect(result).toEqual({ error: "Top 3 must be unique countries" }) + }) + + it("allows overwriting a prediction", () => { + gm.submitPrediction("p1", "SE", ["DE", "IT", "FR"], "GB") + gm.submitPrediction("p1", "NO", ["DE", "IT", "FR"], "GB") + expect(gm.getPrediction("p1")?.predictedWinner).toBe("NO") + }) + + it("rejects prediction when locked", () => { + gm.lockPredictions() + const result = gm.submitPrediction("p1", "SE", ["DE", "IT", "FR"], "GB") + expect(result).toEqual({ error: "Predictions are locked" }) + }) + }) + + describe("dishes", () => { + it("adds a dish", () => { + const result = gm.addDish("Köttbullar", "SE") + expect("dish" in result).toBe(true) + if ("dish" in result) { + expect(result.dish.name).toBe("Köttbullar") + expect(result.dish.correctCountry).toBe("SE") + expect(result.dish.revealed).toBe(false) + } + }) + + it("rejects dish with invalid country", () => { + const result = gm.addDish("Mystery", "XX") + expect(result).toEqual({ error: "Invalid country: XX" }) + }) + + it("accepts a dish guess", () => { + const addResult = gm.addDish("Köttbullar", "SE") + if (!("dish" in addResult)) throw new Error("unexpected") + const result = gm.submitDishGuess("p1", addResult.dish.id, "SE") + expect(result).toEqual({ success: true }) + }) + + it("rejects guess for nonexistent dish", () => { + const result = gm.submitDishGuess("p1", "fake-id", "SE") + expect(result).toEqual({ error: "Dish not found" }) + }) + + it("rejects guess after reveal", () => { + const addResult = gm.addDish("Köttbullar", "SE") + if (!("dish" in addResult)) throw new Error("unexpected") + gm.revealDishes() + const result = gm.submitDishGuess("p1", addResult.dish.id, "SE") + expect(result).toEqual({ error: "Dish already revealed" }) + }) + + it("replaces an existing guess for the same dish", () => { + const addResult = gm.addDish("Köttbullar", "SE") + if (!("dish" in addResult)) throw new Error("unexpected") + gm.submitDishGuess("p1", addResult.dish.id, "DE") + gm.submitDishGuess("p1", addResult.dish.id, "SE") + const guesses = gm.getDishGuesses("p1") + expect(guesses).toHaveLength(1) + expect(guesses[0]?.guessedCountry).toBe("SE") + }) + + it("reveals dishes and produces results", () => { + const addResult = gm.addDish("Köttbullar", "SE") + if (!("dish" in addResult)) throw new Error("unexpected") + gm.submitDishGuess("p1", addResult.dish.id, "SE") + gm.submitDishGuess("p2", addResult.dish.id, "DE") + gm.revealDishes() + + const lookup = new Map([ + ["p1", "Alice"], + ["p2", "Bob"], + ]) + const results = gm.getDishResults(lookup) + expect(results).toHaveLength(1) + expect(results[0]?.guesses).toHaveLength(2) + + const aliceGuess = results[0]?.guesses.find((g) => g.playerId === "p1") + expect(aliceGuess?.correct).toBe(true) + + const bobGuess = results[0]?.guesses.find((g) => g.playerId === "p2") + expect(bobGuess?.correct).toBe(false) + }) + }) + + describe("getGameStateForPlayer", () => { + it("hides correct country for unrevealed dishes", () => { + gm.addDish("Köttbullar", "SE") + const lookup = new Map() + const state = gm.getGameStateForPlayer("p1", lookup) + expect(state.dishes[0]?.correctCountry).toBe("") + }) + + it("shows correct country after reveal", () => { + gm.addDish("Köttbullar", "SE") + gm.revealDishes() + const lookup = new Map() + const state = gm.getGameStateForPlayer("p1", lookup) + expect(state.dishes[0]?.correctCountry).toBe("SE") + }) + + it("includes only the requesting player's prediction", () => { + gm.submitPrediction("p1", "SE", ["DE", "IT", "FR"], "GB") + gm.submitPrediction("p2", "NO", ["DE", "IT", "FR"], "GB") + const lookup = new Map() + const state = gm.getGameStateForPlayer("p1", lookup) + expect(state.myPrediction?.predictedWinner).toBe("SE") + }) + }) +})