415 lines
11 KiB
TypeScript

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<QuizQuestion>;
category: string;
difficulty: "easy" | "medium" | "hard";
tags: Array<string>;
}
interface QuizGameConfig extends IGameConfig {
randomizeQuestions?: boolean;
revealAnswersImmediately?: boolean;
pointsPerQuestion?: number;
}
interface QuizGameState extends IGameState {
currentQuestion?: QuizQuestion;
answeredQuestions: Set<string>;
playerAnswers: Record<string, Record<string, number>>;
leaderboard: Array<GamePlayer>;
}
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<string, NodeJS.Timeout> = 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<string>(),
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<T>(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<string, any> = 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());
}
}