415 lines
11 KiB
TypeScript
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());
|
|
}
|
|
}
|