mafia / frontend /src /systems /QuizManager.ts
Alfaxad's picture
Migrate Mafia game to ZeroGPU Gradio Space
c670567 verified
Raw
History Blame Contribute Delete
5.14 kB
/**
* ============================================================================
* QUIZ MANAGER - Question bank & validation system
* ============================================================================
*
* Manages quiz/question functionality for educational games:
* - Load questions from JSON or inline data
* - Random question selection (with optional subject/difficulty filters)
* - Answer validation
* - Usage tracking (avoid repeats)
* - Statistics (correct/wrong counts, accuracy)
*
* USAGE:
* const qm = new QuizManager();
* qm.loadQuestions(questionsArray);
* // or: qm.loadFromJSON(scene, 'questions'); // from cache
*
* const q = qm.getRandomQuestion({ subject: 'math', difficulty: 2 });
* const correct = qm.checkAnswer(q, selectedIndex);
* const stats = qm.getStats();
*/
import Phaser from 'phaser';
import { type QuizQuestion } from '../scenes/BaseBattleScene';
export interface QuizFilter {
/** Filter by subject */
subject?: string;
/** Filter by difficulty (exact) */
difficulty?: number;
/** Filter by max difficulty */
maxDifficulty?: number;
/** Exclude already-used questions */
excludeUsed?: boolean;
}
export interface QuizStats {
totalAsked: number;
totalCorrect: number;
totalWrong: number;
accuracy: number;
streakCurrent: number;
streakBest: number;
}
export class QuizManager {
private questions: QuizQuestion[] = [];
private usedIndices: Set<number> = new Set();
private stats: QuizStats = {
totalAsked: 0,
totalCorrect: 0,
totalWrong: 0,
accuracy: 0,
streakCurrent: 0,
streakBest: 0,
};
// -- Loading --
/** Load questions from an array. */
loadQuestions(questions: QuizQuestion[]): void {
this.questions = questions;
}
/** Load questions from a cached JSON key. */
loadFromJSON(scene: Phaser.Scene, cacheKey: string): void {
const data = scene.cache.json.get(cacheKey);
if (data?.questions) {
this.questions = data.questions;
}
}
// -- Selection --
/** Get a random question matching optional filters. */
getRandomQuestion(filter?: QuizFilter): QuizQuestion | undefined {
if (this.questions.length === 0) return undefined;
// Build candidate list applying filters
let candidates = this.questions.map((q, i) => ({ q, i }));
if (filter) {
if (filter.subject) {
candidates = candidates.filter((c) => c.q.subject === filter.subject);
}
if (filter.difficulty !== undefined) {
candidates = candidates.filter(
(c) => c.q.difficulty === filter.difficulty,
);
}
if (filter.maxDifficulty !== undefined) {
candidates = candidates.filter(
(c) => (c.q.difficulty ?? 1) <= filter.maxDifficulty!,
);
}
if (filter.excludeUsed) {
candidates = candidates.filter((c) => !this.usedIndices.has(c.i));
}
}
if (candidates.length === 0) {
// If all questions have been used, reset and try again without the used filter
if (filter?.excludeUsed && this.usedIndices.size > 0) {
this.usedIndices.clear();
return this.getRandomQuestion({ ...filter, excludeUsed: false });
}
return undefined;
}
const pick = candidates[Math.floor(Math.random() * candidates.length)];
this.usedIndices.add(pick.i);
return pick.q;
}
/** Get the total number of available questions matching optional filters. */
getQuestionCount(filter?: QuizFilter): number {
if (!filter) return this.questions.length;
return this.questions.filter((q, i) => {
if (filter.subject && q.subject !== filter.subject) return false;
if (filter.difficulty !== undefined && q.difficulty !== filter.difficulty)
return false;
if (
filter.maxDifficulty !== undefined &&
(q.difficulty ?? 1) > filter.maxDifficulty
)
return false;
if (filter.excludeUsed && this.usedIndices.has(i)) return false;
return true;
}).length;
}
// -- Validation --
/** Check if the selected answer is correct. Updates stats. */
checkAnswer(question: QuizQuestion, selectedIndex: number): boolean {
const correct = selectedIndex === question.correctIndex;
this.stats.totalAsked++;
if (correct) {
this.stats.totalCorrect++;
this.stats.streakCurrent++;
this.stats.streakBest = Math.max(
this.stats.streakBest,
this.stats.streakCurrent,
);
} else {
this.stats.totalWrong++;
this.stats.streakCurrent = 0;
}
this.stats.accuracy =
this.stats.totalAsked > 0
? this.stats.totalCorrect / this.stats.totalAsked
: 0;
return correct;
}
// -- Stats --
/** Get quiz statistics. */
getStats(): QuizStats {
return { ...this.stats };
}
/** Reset usage tracking (allow questions to repeat). */
resetUsed(): void {
this.usedIndices.clear();
}
/** Reset all stats. */
resetStats(): void {
this.stats = {
totalAsked: 0,
totalCorrect: 0,
totalWrong: 0,
accuracy: 0,
streakCurrent: 0,
streakBest: 0,
};
}
}