import type { GameMode, GamePhase, ShuffledQuestion } from "./types"; export type ChallengePhase = "easy" | "medium" | "hard"; const CHALLENGE_PROGRESSION: ChallengePhase[] = ["easy", "medium", "hard"]; /** * The complete state of an in-progress game session. */ export interface GameState { mode: GameMode; phase: GamePhase; questions: ShuffledQuestion[]; currentQuestionIndex: number; score: number; streak: number; streakMax: number; questionsCorrect: number; startedAt: number | null; finishedAt: number | null; challengePhase: ChallengePhase; lastAnswerCorrect: boolean | null; } /** * All actions that can be dispatched to the game reducer. */ export type GameAction = | { type: "START" } | { type: "ANSWER_CORRECT"; points: number } | { type: "ANSWER_WRONG" } | { type: "TIME_UP" } | { type: "NEXT_PHASE" } | { type: "ADD_QUESTIONS"; questions: ShuffledQuestion[] } | { type: "SET_QUESTIONS"; questions: ShuffledQuestion[] }; /** * Creates the initial idle GameState for the given mode and question list. */ export function createInitialState(mode: GameMode, questions: ShuffledQuestion[]): GameState { return { mode, phase: "idle", questions, currentQuestionIndex: 0, score: 0, streak: 0, streakMax: 0, questionsCorrect: 0, startedAt: null, finishedAt: null, challengePhase: "easy", lastAnswerCorrect: null, }; } /** * Pure reducer for the game state machine. Returns a new GameState given the * current state and an action — never mutates the input state. */ export function gameReducer(state: GameState, action: GameAction): GameState { switch (action.type) { case "START": return { ...state, phase: "playing", startedAt: Date.now() }; case "ANSWER_CORRECT": { const newStreak = state.streak + 1; const newIndex = state.currentQuestionIndex + 1; const isFinished = newIndex >= state.questions.length; // For challenge-me, only truly finish on the last phase (hard) or if they hit 30 questions const isChallengeComplete = state.mode === "challenge-me" && (state.challengePhase === "hard" || newIndex >= 30); const shouldFinish = isFinished && !(state.mode === "challenge-me" && !isChallengeComplete); return { ...state, score: state.score + action.points, streak: newStreak, streakMax: Math.max(state.streakMax, newStreak), questionsCorrect: state.questionsCorrect + 1, currentQuestionIndex: newIndex, lastAnswerCorrect: true, phase: shouldFinish ? "finished" : state.phase, finishedAt: shouldFinish ? Date.now() : null, }; } case "ANSWER_WRONG": { if (state.mode === "streak") { return { ...state, streak: 0, currentQuestionIndex: state.currentQuestionIndex + 1, lastAnswerCorrect: false, phase: "finished", finishedAt: Date.now(), }; } const newIndex = state.currentQuestionIndex + 1; const isFinished = newIndex >= state.questions.length; // For challenge-me, only truly finish on the last phase (hard) or if they hit 30 questions const isChallengeComplete = state.mode === "challenge-me" && (state.challengePhase === "hard" || newIndex >= 30); const shouldFinish = isFinished && !(state.mode === "challenge-me" && !isChallengeComplete); return { ...state, streak: 0, currentQuestionIndex: newIndex, lastAnswerCorrect: false, phase: shouldFinish ? "finished" : state.phase, finishedAt: shouldFinish ? Date.now() : null, }; } case "TIME_UP": return { ...state, phase: "finished", finishedAt: Date.now() }; case "NEXT_PHASE": { const idx = CHALLENGE_PROGRESSION.indexOf(state.challengePhase); if (idx >= CHALLENGE_PROGRESSION.length - 1) { return { ...state, phase: "finished", finishedAt: Date.now() }; } return { ...state, challengePhase: CHALLENGE_PROGRESSION[idx + 1]! }; } case "ADD_QUESTIONS": return { ...state, questions: [...state.questions, ...action.questions] }; case "SET_QUESTIONS": return createInitialState(state.mode, action.questions); default: return state; } }