diff --git a/packages/server/src/games/game-manager.ts b/packages/server/src/games/game-manager.ts index 73dc9d7..3bdcd84 100644 --- a/packages/server/src/games/game-manager.ts +++ b/packages/server/src/games/game-manager.ts @@ -1,27 +1,11 @@ -import { randomUUID } from "node:crypto" -import type { Prediction, Dish, DishGuess, GameState, Lineup } from "@celebrate-esc/shared" -import lineupData from "../../data/esc-2026.json" +import type { Prediction, GameState, Lineup } from "@celebrate-esc/shared" +import lineupData from "../../data/esc-2025.json" const lineup: Lineup = lineupData as Lineup -const countryCodes = new Set(lineup.countries.map((c) => c.code)) - -interface InternalDish { - id: string - name: string - correctCountry: string - revealed: boolean -} - -interface InternalDishGuess { - dishId: string - playerId: string - guessedCountry: string -} +const countryCodes = new Set(lineup.entries.map((e) => e.country.code)) export class GameManager { private predictions = new Map() // playerId → prediction - private dishes: InternalDish[] = [] - private dishGuesses: InternalDishGuess[] = [] private locked = false getLineup(): Lineup { @@ -44,29 +28,23 @@ export class GameManager { submitPrediction( playerId: string, - predictedWinner: string, - top3: string[], - nulPointsPick: string, + first: string, + second: string, + third: string, + last: string, ): { success: true } | { error: string } { if (this.locked) return { error: "Predictions are locked" } - // Validate all countries exist - const allPicks = [predictedWinner, ...top3, nulPointsPick] + const allPicks = [first, second, third, last] for (const code of allPicks) { if (!this.isValidCountry(code)) return { error: `Invalid country: ${code}` } } - // Winner must not be in top3 - if (top3.includes(predictedWinner)) { - return { error: "Winner cannot also be in top 3" } + if (new Set(allPicks).size !== 4) { + return { error: "All 4 picks must be different countries" } } - // No duplicates in top3 - if (new Set(top3).size !== 3) { - return { error: "Top 3 must be unique countries" } - } - - this.predictions.set(playerId, { playerId, predictedWinner, top3, nulPointsPick }) + this.predictions.set(playerId, { playerId, first, second, third, last }) return { success: true } } @@ -78,112 +56,35 @@ export class GameManager { return this.predictions } - // ─── Dishes ───────────────────────────────────────────────────── - - addDish(name: string, correctCountry: string): { dish: InternalDish } | { error: string } { - if (!this.isValidCountry(correctCountry)) { - return { error: `Invalid country: ${correctCountry}` } - } - - const dish: InternalDish = { - id: randomUUID(), - name, - correctCountry, - revealed: false, - } - this.dishes.push(dish) - return { dish } - } - - getDishes(): InternalDish[] { - return this.dishes - } - - submitDishGuess( - playerId: string, - dishId: string, - guessedCountry: string, - ): { success: true } | { error: string } { - if (!this.isValidCountry(guessedCountry)) { - return { error: `Invalid country: ${guessedCountry}` } - } - - const dish = this.dishes.find((d) => d.id === dishId) - if (!dish) return { error: "Dish not found" } - if (dish.revealed) return { error: "Dish already revealed" } - - // Replace existing guess for same player+dish - this.dishGuesses = this.dishGuesses.filter( - (g) => !(g.playerId === playerId && g.dishId === dishId), - ) - this.dishGuesses.push({ dishId, playerId, guessedCountry }) - return { success: true } - } - - getDishGuesses(playerId: string): DishGuess[] { - return this.dishGuesses - .filter((g) => g.playerId === playerId) - .map((g) => ({ dishId: g.dishId, playerId: g.playerId, guessedCountry: g.guessedCountry })) - } - - revealDishes(): InternalDish[] { - for (const dish of this.dishes) { - dish.revealed = true - } - return this.dishes - } - - getDishResults( - playerLookup: Map, - ): { dish: Dish; guesses: { playerId: string; displayName: string; guessedCountry: string; correct: boolean }[] }[] { - return this.dishes.map((dish) => ({ - dish: { id: dish.id, name: dish.name, correctCountry: dish.correctCountry, revealed: dish.revealed }, - guesses: this.dishGuesses - .filter((g) => g.dishId === dish.id) - .map((g) => ({ - playerId: g.playerId, - displayName: playerLookup.get(g.playerId) ?? "Unknown", - guessedCountry: g.guessedCountry, - correct: g.guessedCountry === dish.correctCountry, - })), - })) - } - - areAllDishesRevealed(): boolean { - return this.dishes.length > 0 && this.dishes.every((d) => d.revealed) + hasPrediction(playerId: string): boolean { + return this.predictions.has(playerId) } // ─── State for client ─────────────────────────────────────────── - getGameStateForPlayer(playerId: string, playerLookup: Map, isHost: boolean): GameState { + private buildPredictionSubmitted(playerIds: string[]): Record { + const result: Record = {} + for (const id of playerIds) { + result[id] = this.predictions.has(id) + } + return result + } + + getGameStateForPlayer(playerId: string, allPlayerIds: string[]): GameState { return { lineup, myPrediction: this.getPrediction(playerId), predictionsLocked: this.locked, - dishes: this.dishes.map((d) => ({ - id: d.id, - name: d.name, - correctCountry: d.revealed || isHost ? d.correctCountry : "", - revealed: d.revealed, - })), - myDishGuesses: this.getDishGuesses(playerId), - dishResults: this.areAllDishesRevealed() ? this.getDishResults(playerLookup) : null, + predictionSubmitted: this.buildPredictionSubmitted(allPlayerIds), } } - getGameStateForDisplay(playerLookup: Map): GameState { + getGameStateForDisplay(allPlayerIds: string[]): GameState { return { lineup, myPrediction: null, predictionsLocked: this.locked, - dishes: this.dishes.map((d) => ({ - id: d.id, - name: d.name, - correctCountry: d.revealed ? d.correctCountry : "", - revealed: d.revealed, - })), - myDishGuesses: [], - dishResults: this.areAllDishesRevealed() ? this.getDishResults(playerLookup) : null, + predictionSubmitted: this.buildPredictionSubmitted(allPlayerIds), } } }