Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
import { GameCard } from '../../Domain/Game/GameAggregate';
|
||||
import { CardType } from '../../Domain/Deck/DeckAggregate';
|
||||
|
||||
// Type-specific answer structures
|
||||
export interface QuizOption {
|
||||
answer: string; // A, B, C, D
|
||||
text: string;
|
||||
correct: boolean;
|
||||
}
|
||||
|
||||
export interface CloserAnswer {
|
||||
correct: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
export interface CardClientData {
|
||||
cardid: string;
|
||||
question: string;
|
||||
type: CardType;
|
||||
// Type-specific client data
|
||||
options?: QuizOption[]; // For QUIZ
|
||||
words?: string[]; // For SENTENCE_PAIRING (scrambled)
|
||||
acceptableAnswers?: string[]; // For OWN_ANSWER (not sent to client)
|
||||
// CLOSER and TRUE_FALSE send only question
|
||||
}
|
||||
|
||||
export interface CardValidationResult {
|
||||
isCorrect: boolean;
|
||||
submittedAnswer: any;
|
||||
correctAnswer?: any;
|
||||
explanation?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service responsible for handling type-specific card processing
|
||||
* Prepares cards for clients and validates answers based on CardType
|
||||
*/
|
||||
export class CardProcessingService {
|
||||
|
||||
/**
|
||||
* Prepare card data for client based on card type
|
||||
* @param card The game card to prepare
|
||||
* @returns Client-safe card data with type-specific information
|
||||
*/
|
||||
prepareCardForClient(card: GameCard): CardClientData {
|
||||
if (!card.question || card.type === undefined) {
|
||||
throw new Error('Card must have question and type defined');
|
||||
}
|
||||
|
||||
const baseData: CardClientData = {
|
||||
cardid: card.cardid,
|
||||
question: card.question,
|
||||
type: card.type
|
||||
};
|
||||
|
||||
switch (card.type) {
|
||||
case CardType.QUIZ:
|
||||
return this.prepareQuizCard(card, baseData);
|
||||
|
||||
case CardType.SENTENCE_PAIRING:
|
||||
return this.prepareSentencePairingCard(card, baseData);
|
||||
|
||||
case CardType.OWN_ANSWER:
|
||||
return this.prepareOwnAnswerCard(card, baseData);
|
||||
|
||||
case CardType.TRUE_FALSE:
|
||||
return this.prepareTrueFalseCard(card, baseData);
|
||||
|
||||
case CardType.CLOSER:
|
||||
return this.prepareCloserCard(card, baseData);
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported card type: ${card.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate player's answer based on card type
|
||||
* @param card The game card
|
||||
* @param playerAnswer Player's submitted answer
|
||||
* @returns Validation result with correctness and explanation
|
||||
*/
|
||||
validateAnswer(card: GameCard, playerAnswer: any): CardValidationResult {
|
||||
if (card.type === undefined) {
|
||||
throw new Error('Card type is required for validation');
|
||||
}
|
||||
|
||||
switch (card.type) {
|
||||
case CardType.QUIZ:
|
||||
return this.validateQuizAnswer(card, playerAnswer);
|
||||
|
||||
case CardType.SENTENCE_PAIRING:
|
||||
return this.validateSentencePairingAnswer(card, playerAnswer);
|
||||
|
||||
case CardType.OWN_ANSWER:
|
||||
return this.validateOwnAnswerAnswer(card, playerAnswer);
|
||||
|
||||
case CardType.TRUE_FALSE:
|
||||
return this.validateTrueFalseAnswer(card, playerAnswer);
|
||||
|
||||
case CardType.CLOSER:
|
||||
return this.validateCloserAnswer(card, playerAnswer);
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported card type for validation: ${card.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare QUIZ card with multiple choice options
|
||||
*/
|
||||
private prepareQuizCard(card: GameCard, baseData: CardClientData): CardClientData {
|
||||
if (!Array.isArray(card.answer)) {
|
||||
throw new Error('Quiz card answer must be an array of options');
|
||||
}
|
||||
|
||||
return {
|
||||
...baseData,
|
||||
options: card.answer as QuizOption[]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare SENTENCE_PAIRING card with scrambled words
|
||||
*/
|
||||
private prepareSentencePairingCard(card: GameCard, baseData: CardClientData): CardClientData {
|
||||
if (typeof card.answer !== 'string') {
|
||||
throw new Error('Sentence pairing card answer must be a string');
|
||||
}
|
||||
|
||||
const words = card.answer.split(' ').filter(word => word.trim() !== '');
|
||||
const scrambledWords = this.scrambleArray([...words]);
|
||||
|
||||
return {
|
||||
...baseData,
|
||||
words: scrambledWords
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare OWN_ANSWER card (only question, acceptable answers hidden)
|
||||
*/
|
||||
private prepareOwnAnswerCard(card: GameCard, baseData: CardClientData): CardClientData {
|
||||
// Don't send acceptable answers to client
|
||||
return baseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare TRUE_FALSE card (only question)
|
||||
*/
|
||||
private prepareTrueFalseCard(card: GameCard, baseData: CardClientData): CardClientData {
|
||||
return baseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare CLOSER card (only question)
|
||||
*/
|
||||
private prepareCloserCard(card: GameCard, baseData: CardClientData): CardClientData {
|
||||
return baseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate QUIZ answer (A, B, C, D)
|
||||
*/
|
||||
private validateQuizAnswer(card: GameCard, playerAnswer: string): CardValidationResult {
|
||||
if (!Array.isArray(card.answer)) {
|
||||
throw new Error('Quiz card answer must be an array');
|
||||
}
|
||||
|
||||
const options = card.answer as QuizOption[];
|
||||
const correctOption = options.find(opt => opt.correct);
|
||||
|
||||
if (!correctOption) {
|
||||
throw new Error('Quiz card must have one correct answer');
|
||||
}
|
||||
|
||||
const isCorrect = playerAnswer.toUpperCase() === correctOption.answer.toUpperCase();
|
||||
|
||||
return {
|
||||
isCorrect,
|
||||
submittedAnswer: playerAnswer,
|
||||
correctAnswer: correctOption.answer,
|
||||
explanation: isCorrect
|
||||
? `✅ Correct! ${correctOption.text}`
|
||||
: `❌ Wrong! Correct answer was ${correctOption.answer}: ${correctOption.text}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SENTENCE_PAIRING answer (reconstructed sentence)
|
||||
*/
|
||||
private validateSentencePairingAnswer(card: GameCard, playerAnswer: string[] | string): CardValidationResult {
|
||||
if (typeof card.answer !== 'string') {
|
||||
throw new Error('Sentence pairing card answer must be a string');
|
||||
}
|
||||
|
||||
// Handle both array of words and joined string
|
||||
const reconstructed = Array.isArray(playerAnswer)
|
||||
? playerAnswer.join(' ').toLowerCase().trim()
|
||||
: playerAnswer.toLowerCase().trim();
|
||||
|
||||
const correctSentence = card.answer.toLowerCase().trim();
|
||||
const isCorrect = reconstructed === correctSentence;
|
||||
|
||||
return {
|
||||
isCorrect,
|
||||
submittedAnswer: reconstructed,
|
||||
correctAnswer: card.answer,
|
||||
explanation: isCorrect
|
||||
? '✅ Perfect! You arranged the sentence correctly!'
|
||||
: `❌ Wrong order! Correct sentence: "${card.answer}"`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate OWN_ANSWER (check against acceptable answers array)
|
||||
*/
|
||||
private validateOwnAnswerAnswer(card: GameCard, playerAnswer: string): CardValidationResult {
|
||||
if (!Array.isArray(card.answer)) {
|
||||
throw new Error('Own answer card must have array of acceptable answers');
|
||||
}
|
||||
|
||||
const acceptableAnswers = card.answer as string[];
|
||||
const cleanPlayerAnswer = playerAnswer.toLowerCase().trim();
|
||||
|
||||
const isCorrect = acceptableAnswers.some(acceptable =>
|
||||
acceptable.toLowerCase().trim() === cleanPlayerAnswer
|
||||
);
|
||||
|
||||
return {
|
||||
isCorrect,
|
||||
submittedAnswer: playerAnswer,
|
||||
correctAnswer: acceptableAnswers,
|
||||
explanation: isCorrect
|
||||
? '✅ Correct! Your answer is acceptable.'
|
||||
: `❌ Your answer doesn't match any acceptable responses.`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate TRUE_FALSE answer
|
||||
*/
|
||||
private validateTrueFalseAnswer(card: GameCard, playerAnswer: string): CardValidationResult {
|
||||
if (typeof card.answer !== 'boolean' && typeof card.answer !== 'string') {
|
||||
throw new Error('True/false card answer must be boolean or string');
|
||||
}
|
||||
|
||||
// Convert player answer to boolean
|
||||
const playerBool = this.convertToBoolean(playerAnswer);
|
||||
const correctBool = typeof card.answer === 'boolean'
|
||||
? card.answer
|
||||
: this.convertToBoolean(card.answer);
|
||||
|
||||
const isCorrect = playerBool === correctBool;
|
||||
|
||||
return {
|
||||
isCorrect,
|
||||
submittedAnswer: playerAnswer,
|
||||
correctAnswer: correctBool ? 'True' : 'False',
|
||||
explanation: isCorrect
|
||||
? '✅ Correct!'
|
||||
: `❌ Wrong! The correct answer is ${correctBool ? 'True' : 'False'}.`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CLOSER answer (numerical proximity)
|
||||
*/
|
||||
private validateCloserAnswer(card: GameCard, playerAnswer: string | number): CardValidationResult {
|
||||
if (typeof card.answer !== 'object' || !card.answer.correct || !card.answer.percent) {
|
||||
throw new Error('Closer card answer must have correct and percent fields');
|
||||
}
|
||||
|
||||
const closerAnswer = card.answer as CloserAnswer;
|
||||
const playerNumber = typeof playerAnswer === 'number'
|
||||
? playerAnswer
|
||||
: parseFloat(playerAnswer.toString());
|
||||
|
||||
if (isNaN(playerNumber)) {
|
||||
return {
|
||||
isCorrect: false,
|
||||
submittedAnswer: playerAnswer,
|
||||
correctAnswer: closerAnswer.correct,
|
||||
explanation: '❌ Invalid number! Please enter a valid numeric answer.'
|
||||
};
|
||||
}
|
||||
|
||||
const tolerance = closerAnswer.correct * (closerAnswer.percent / 100);
|
||||
const minValue = closerAnswer.correct - tolerance;
|
||||
const maxValue = closerAnswer.correct + tolerance;
|
||||
|
||||
const isCorrect = playerNumber >= minValue && playerNumber <= maxValue;
|
||||
|
||||
return {
|
||||
isCorrect,
|
||||
submittedAnswer: playerNumber,
|
||||
correctAnswer: closerAnswer.correct,
|
||||
explanation: isCorrect
|
||||
? `✅ Close enough! Correct answer: ${closerAnswer.correct}`
|
||||
: `❌ Not close enough! Correct answer: ${closerAnswer.correct} (±${closerAnswer.percent}%)`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to boolean for TRUE_FALSE validation
|
||||
*/
|
||||
private convertToBoolean(value: string): boolean {
|
||||
const lowerValue = value.toLowerCase().trim();
|
||||
return ['true', 'yes', '1', 'correct', 'right'].includes(lowerValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scramble array elements randomly
|
||||
*/
|
||||
private scrambleArray<T>(array: T[]): T[] {
|
||||
const scrambled = [...array];
|
||||
for (let i = scrambled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[scrambled[i], scrambled[j]] = [scrambled[j], scrambled[i]];
|
||||
}
|
||||
return scrambled;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user