add prediction scoring to GameManager with tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ const countryCodes = new Set(lineup.entries.map((e) => e.country.code))
|
||||
export class GameManager {
|
||||
private predictions = new Map<string, Prediction>() // playerId → prediction
|
||||
private locked = false
|
||||
private actualResults: { winner: string; second: string; third: string; last: string } | null = null
|
||||
|
||||
getLineup(): Lineup {
|
||||
return lineup
|
||||
@@ -222,6 +223,29 @@ export class GameManager {
|
||||
return score
|
||||
}
|
||||
|
||||
// ─── Prediction Scoring ─────────────────────────────────────────
|
||||
|
||||
setActualResults(winner: string, second: string, third: string, last: string): void {
|
||||
this.actualResults = { winner, second, third, last }
|
||||
}
|
||||
|
||||
getActualResults(): { winner: string; second: string; third: string; last: string } | null {
|
||||
return this.actualResults
|
||||
}
|
||||
|
||||
getPredictionScore(playerId: string): number {
|
||||
if (!this.actualResults) return 0
|
||||
const prediction = this.predictions.get(playerId)
|
||||
if (!prediction) return 0
|
||||
|
||||
let score = 0
|
||||
if (prediction.first === this.actualResults.winner) score += scoringConfig.prediction_winner
|
||||
if (prediction.second === this.actualResults.second) score += scoringConfig.prediction_top3
|
||||
if (prediction.third === this.actualResults.third) score += scoringConfig.prediction_top3
|
||||
if (prediction.last === this.actualResults.last) score += scoringConfig.prediction_nul_points
|
||||
return score
|
||||
}
|
||||
|
||||
// ─── State for client ───────────────────────────────────────────
|
||||
|
||||
private buildPredictionSubmitted(playerIds: string[]): Record<string, boolean> {
|
||||
@@ -247,6 +271,7 @@ export class GameManager {
|
||||
myJuryVote: this.getPlayerJuryVote(playerId),
|
||||
myBingoCard: this.getBingoCard(playerId),
|
||||
bingoAnnouncements: this.bingoAnnouncements,
|
||||
actualResults: this.actualResults,
|
||||
leaderboard: this.buildLeaderboard(allPlayerIds, displayNames ?? {}),
|
||||
}
|
||||
}
|
||||
@@ -265,6 +290,7 @@ export class GameManager {
|
||||
myJuryVote: null,
|
||||
myBingoCard: null,
|
||||
bingoAnnouncements: this.bingoAnnouncements,
|
||||
actualResults: this.actualResults,
|
||||
leaderboard: this.buildLeaderboard(allPlayerIds, displayNames ?? {}),
|
||||
}
|
||||
}
|
||||
@@ -272,17 +298,19 @@ export class GameManager {
|
||||
private buildLeaderboard(
|
||||
playerIds: string[],
|
||||
displayNames: Record<string, string>,
|
||||
): { playerId: string; displayName: string; juryPoints: number; bingoPoints: number; totalPoints: number }[] {
|
||||
): { playerId: string; displayName: string; juryPoints: number; bingoPoints: number; predictionPoints: number; totalPoints: number }[] {
|
||||
return playerIds
|
||||
.map((id) => {
|
||||
const juryPoints = this.getJuryScore(id)
|
||||
const bingoPoints = this.getBingoScore(id)
|
||||
const predictionPoints = this.getPredictionScore(id)
|
||||
return {
|
||||
playerId: id,
|
||||
displayName: displayNames[id] ?? "Unknown",
|
||||
juryPoints,
|
||||
bingoPoints,
|
||||
totalPoints: juryPoints + bingoPoints,
|
||||
predictionPoints,
|
||||
totalPoints: juryPoints + bingoPoints + predictionPoints,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.totalPoints - a.totalPoints)
|
||||
|
||||
@@ -339,6 +339,104 @@ describe("GameManager", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("prediction scoring", () => {
|
||||
it("returns 0 for all when no actual results set", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "SE", "IT", "FR", "GB")
|
||||
expect(gm.getPredictionScore("p1")).toBe(0)
|
||||
})
|
||||
|
||||
it("scores correct winner", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "SE", "IT", "FR", "GB")
|
||||
gm.setActualResults("SE", "CH", "DE", "AL")
|
||||
expect(gm.getPredictionScore("p1")).toBe(25)
|
||||
})
|
||||
|
||||
it("scores correct second place", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "NO", "IT", "FR", "GB")
|
||||
gm.setActualResults("SE", "IT", "DE", "AL")
|
||||
expect(gm.getPredictionScore("p1")).toBe(10)
|
||||
})
|
||||
|
||||
it("scores correct third place", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "NO", "DK", "FR", "GB")
|
||||
gm.setActualResults("SE", "IT", "FR", "AL")
|
||||
expect(gm.getPredictionScore("p1")).toBe(10)
|
||||
})
|
||||
|
||||
it("scores correct last place", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "NO", "DK", "FI", "GB")
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
expect(gm.getPredictionScore("p1")).toBe(15)
|
||||
})
|
||||
|
||||
it("scores perfect prediction (all correct)", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "SE", "IT", "FR", "GB")
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
expect(gm.getPredictionScore("p1")).toBe(60)
|
||||
})
|
||||
|
||||
it("scores 0 for all wrong", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "NO", "DK", "FI", "EE")
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
expect(gm.getPredictionScore("p1")).toBe(0)
|
||||
})
|
||||
|
||||
it("returns 0 for player with no prediction", () => {
|
||||
const gm = new GameManager()
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
expect(gm.getPredictionScore("p1")).toBe(0)
|
||||
})
|
||||
|
||||
it("getActualResults returns null before setting", () => {
|
||||
const gm = new GameManager()
|
||||
expect(gm.getActualResults()).toBeNull()
|
||||
})
|
||||
|
||||
it("getActualResults returns results after setting", () => {
|
||||
const gm = new GameManager()
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
expect(gm.getActualResults()).toEqual({ winner: "SE", second: "IT", third: "FR", last: "GB" })
|
||||
})
|
||||
|
||||
it("setActualResults overwrites previous results", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "SE", "IT", "FR", "GB")
|
||||
gm.setActualResults("NO", "DK", "FI", "EE")
|
||||
expect(gm.getPredictionScore("p1")).toBe(0)
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
expect(gm.getPredictionScore("p1")).toBe(60)
|
||||
})
|
||||
|
||||
it("prediction points appear in leaderboard", () => {
|
||||
const gm = new GameManager()
|
||||
gm.submitPrediction("p1", "SE", "IT", "FR", "GB")
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
const state = gm.getGameStateForPlayer("p1", ["p1"], { p1: "Alice" })
|
||||
expect(state.leaderboard[0]!.predictionPoints).toBe(60)
|
||||
expect(state.leaderboard[0]!.totalPoints).toBe(60)
|
||||
})
|
||||
|
||||
it("actualResults included in game state", () => {
|
||||
const gm = new GameManager()
|
||||
gm.setActualResults("SE", "IT", "FR", "GB")
|
||||
const state = gm.getGameStateForPlayer("p1", ["p1"], { p1: "Alice" })
|
||||
expect(state.actualResults).toEqual({ winner: "SE", second: "IT", third: "FR", last: "GB" })
|
||||
})
|
||||
|
||||
it("actualResults null in game state when not set", () => {
|
||||
const gm = new GameManager()
|
||||
const state = gm.getGameStateForPlayer("p1", ["p1"], { p1: "Alice" })
|
||||
expect(state.actualResults).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getGameStateForDisplay", () => {
|
||||
it("returns null myPrediction", () => {
|
||||
gm.submitPrediction("p1", "SE", "DE", "IT", "GB")
|
||||
|
||||
Reference in New Issue
Block a user