import { Game, type GamePlayer, type IGameConfig, type IGameState, } from "@/game-engien"; interface QuizQuestion { id: string; text: string; options: string[]; correctOptionIndex: number; imageUrl?: string; timeLimit?: number; points: number; } export interface QuizData { version: string; id: string; title: string; description: string; author: string; questions: Array; category: string; difficulty: "easy" | "medium" | "hard"; tags: Array; } interface QuizGameConfig extends IGameConfig { randomizeQuestions?: boolean; revealAnswersImmediately?: boolean; pointsPerQuestion?: number; } interface QuizGameState extends IGameState { currentQuestion?: QuizQuestion; answeredQuestions: Set; playerAnswers: Record>; leaderboard: Array; } type PlayerQuizAction = | { type: "answer"; questionId: string; selectedOptionIndex: number; } | { type: "ready"; } | { type: "requestHint"; questionId: string; }; export class QuizGame extends Game { private quizData: QuizData; private questions: QuizQuestion[]; private quizState: QuizGameState; private timers: Map = new Map(); constructor(quizData: QuizData, config: QuizGameConfig = {}) { super( { name: "quiz", id: quizData.id, title: quizData.title, description: quizData.description, version: quizData.version, }, { timeLimit: 15, // Default 15 seconds per question scoreMultiplier: 1, allowHints: false, ...config, }, ); this.quizData = quizData; this.questions = [...quizData.questions]; this.quizState = { ...this.state, answeredQuestions: new Set(), playerAnswers: {}, leaderboard: [], totalRounds: this.questions.length, }; this.state = this.quizState; } public initialize(config?: QuizGameConfig): void { // Override the default config with any provided config if (config) { this.config = { ...this.config, ...config, }; } // Initialize player answers tracking this.players.forEach((player) => { this.quizState.playerAnswers[player.id] = {}; }); // Randomize questions if configured if ((this.config as QuizGameConfig).randomizeQuestions) { this.questions = this.shuffleArray([...this.questions]); } this.state.totalRounds = this.questions.length; this.state.status = "waiting"; } public start(): void { if (this.state.status !== "waiting") { throw new Error("Game must be in waiting state to start"); } this.state.status = "active"; this.nextQuestion(); } public end(): void { this.clearTimers(); this.state.status = "completed"; this.quizState.leaderboard = [...this.players].sort( (a, b) => b.score - a.score, ); // Notify players of game end this.broadcastGameEnd(); } public handlePlayerAction(playerId: string, action: PlayerQuizAction): void { const player = this.players.find((p) => p.id === playerId); if (!player) { throw new Error(`Player ${playerId} not found`); } switch (action.type) { case "answer": this.handlePlayerAnswer( player, action.questionId, action.selectedOptionIndex, ); break; case "ready": this.handlePlayerReady(player); break; case "requestHint": this.handleRequestHint(player, action.questionId); break; default: throw new Error(`Unknown action type: ${(action as any).type}`); } } private handlePlayerAnswer( player: GamePlayer, questionId: string, selectedOptionIndex: number, ): void { const currentQuestion = this.quizState.currentQuestion; if (!currentQuestion || currentQuestion.id !== questionId) { throw new Error("Invalid question ID"); } // Record the answer this.quizState.playerAnswers[player.id][questionId] = selectedOptionIndex; // Check if answer is correct and update score if (selectedOptionIndex === currentQuestion.correctOptionIndex) { const points = currentQuestion.points * (this.config.scoreMultiplier || 1); player.score += points; // Broadcast correct answer event this.broadcastPlayerCorrectAnswer(player, questionId, points); } // Check if all players have answered const allAnswered = this.players.every( (p) => questionId in (this.quizState.playerAnswers[p.id] || {}), ); if (allAnswered) { this.clearTimers(); // If configured to reveal answers immediately, wait briefly before next question if ((this.config as QuizGameConfig).revealAnswersImmediately) { this.broadcastQuestionResults(currentQuestion); setTimeout(() => this.nextQuestion(), 3000); } else { this.nextQuestion(); } } } private handlePlayerReady(player: GamePlayer): void { // Could implement a ready-up system before starting each question // For now, just acknowledging the ready state } private handleRequestHint(player: GamePlayer, questionId: string): void { if (!this.config.allowHints) { return; } const currentQuestion = this.quizState.currentQuestion; if (!currentQuestion || currentQuestion.id !== questionId) { throw new Error("Invalid question ID"); } // Implementation of hint system // For example, eliminate one wrong option this.provideHint(player, currentQuestion); } private nextQuestion(): void { if (this.state.status !== "active") { return; } this.clearTimers(); // Check if we've gone through all questions if (this.state.currentRound >= this.questions.length) { this.end(); return; } const nextQuestion = this.questions[this.state.currentRound]; this.quizState.currentQuestion = nextQuestion; this.state.currentRound++; this.quizState.answeredQuestions.add(nextQuestion.id); // Set timer for question const timeLimit = nextQuestion.timeLimit || this.config.timeLimit || 15; this.state.timeRemaining = timeLimit; // Broadcast the new question to all players this.broadcastQuestion(nextQuestion); // Start the timer const timer = setInterval(() => { if (this.state.timeRemaining && this.state.timeRemaining > 0) { this.state.timeRemaining--; } else { this.clearTimers(); this.timeExpired(nextQuestion); } }, 1000); this.timers.set("questionTimer", timer); } private timeExpired(question: QuizQuestion): void { // Handle when time expires for a question // Auto-submit blank answers for players who didn't answer this.players.forEach((player) => { if (!(question.id in (this.quizState.playerAnswers[player.id] || {}))) { this.quizState.playerAnswers[player.id][question.id] = -1; // -1 means no answer } }); if ((this.config as QuizGameConfig).revealAnswersImmediately) { this.broadcastQuestionResults(question); setTimeout(() => this.nextQuestion(), 3000); } else { this.nextQuestion(); } } private clearTimers(): void { this.timers.forEach((timer) => clearInterval(timer)); this.timers.clear(); } private shuffleArray(array: T[]): T[] { const newArray = [...array]; for (let i = newArray.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; } return newArray; } private provideHint(player: GamePlayer, question: QuizQuestion): void { // Implementation of hint system // This is a placeholder for the hint logic // Example: Find a wrong answer to eliminate const wrongOptions = question.options .map((option, index) => ({ option, index })) .filter((item) => item.index !== question.correctOptionIndex); if (wrongOptions.length > 0) { const randomWrongOption = wrongOptions[Math.floor(Math.random() * wrongOptions.length)]; this.broadcastHint(player, question.id, randomWrongOption.index); } } // Broadcasting methods (these would connect to your websocket/realtime system) private broadcastQuestion(question: QuizQuestion): void { // In a real implementation, this would send the question to all players console.log("Broadcasting question:", question.text); // Example of what to broadcast (would be sent via websocket) const broadcastData = { type: "question", questionId: question.id, text: question.text, options: question.options, imageUrl: question.imageUrl, timeLimit: question.timeLimit || this.config.timeLimit, points: question.points, }; // Send to all players // this.broadcastToPlayers(broadcastData); } private broadcastQuestionResults(question: QuizQuestion): void { // In a real implementation, this would send the results to all players console.log("Broadcasting question results for:", question.text); // Example of what to broadcast const broadcastData = { type: "questionResults", questionId: question.id, correctOptionIndex: question.correctOptionIndex, playerAnswers: this.quizState.playerAnswers, }; // Send to all players // this.broadcastToPlayers(broadcastData); } private broadcastPlayerCorrectAnswer( player: GamePlayer, questionId: string, points: number, ): void { // Example of what to broadcast when a player answers correctly const broadcastData = { type: "correctAnswer", playerId: player.id, playerName: player.displayName, questionId: questionId, points: points, }; // Send to all players // this.broadcastToPlayers(broadcastData); } private broadcastGameEnd(): void { // Example of what to broadcast when the game ends const broadcastData = { type: "gameEnd", leaderboard: this.quizState.leaderboard, }; // Send to all players // this.broadcastToPlayers(broadcastData); } private broadcastHint( player: GamePlayer, questionId: string, eliminatedOptionIndex: number, ): void { // Example of what to broadcast when providing a hint const broadcastData = { type: "hint", playerId: player.id, questionId: questionId, eliminatedOptionIndex: eliminatedOptionIndex, }; // Send to specific player // this.sendToPlayer(player.id, broadcastData); } } // Game Registry to manage different games class GameRegistry { private games: Map = new Map(); registerGame(gameClass: any): void { const tempInstance = new gameClass({} as any); this.games.set(tempInstance.getGameId(), gameClass); } createGameInstance(gameId: string, ...args: any[]): Game { const GameClass = this.games.get(gameId); if (!GameClass) { throw new Error(`Game ${gameId} not found in registry`); } return new GameClass(...args); } listAvailableGames(): string[] { return Array.from(this.games.keys()); } }