Spaces:
Running
Running
| 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; | |
| } | |
| } | |