rewrite game manager: ordered predictions, predictionSubmitted, remove dishes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, Prediction>() // 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<string, string>,
|
||||
): { 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<string, string>, isHost: boolean): GameState {
|
||||
private buildPredictionSubmitted(playerIds: string[]): Record<string, boolean> {
|
||||
const result: Record<string, boolean> = {}
|
||||
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<string, string>): 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user