import { randomUUID } from "crypto"; import { getGameTerms, getDistractors } from "@lila/db"; import type { GameRequest, GameSession, GameQuestion, AnswerOption, AnswerSubmission, AnswerResult, } from "@lila/shared"; import type { GameSessionStore } from "../gameSessionStore/index.js"; import { NotFoundError, ConflictError, UnprocessableEntityError, } from "../errors/AppError.js"; import { shuffleArray } from "../lib/utils.js"; export const createGameSession = async ( request: GameRequest, store: GameSessionStore, userId: string | null, ): Promise => { const terms = await getGameTerms( request.source_language, request.target_language, request.pos, request.difficulty, request.rounds, ); if (terms.length === 0) { throw new UnprocessableEntityError("No terms found for the given filters"); } const answerKey = new Map(); const questions: GameQuestion[] = await Promise.all( terms.map(async (term) => { const distractorTexts = await getDistractors( term.entryId, term.targetText, request.source_language, request.target_language, request.pos, request.difficulty, 6, ); const uniqueDistractors = [ ...new Set(distractorTexts.filter((t) => t !== term.targetText)), ]; if (uniqueDistractors.length < 3) { throw new Error( `Not enough unique distractors for term: ${term.targetText}`, ); } const optionTexts = [term.targetText, ...uniqueDistractors.slice(0, 3)]; const shuffledTexts = shuffleArray(optionTexts); const correctOptionId = shuffledTexts.indexOf(term.targetText); const options: AnswerOption[] = shuffledTexts.map((text, index) => ({ optionId: index, text, })); const questionId = randomUUID(); answerKey.set(questionId, { correctOptionId }); return { questionId, prompt: term.sourceText, gloss: term.sourceGloss, options, }; }), ); const sessionId = randomUUID(); await store.create(sessionId, { answers: answerKey, userId }, 30 * 60 * 1000); return { sessionId, questions }; }; export const evaluateAnswer = async ( submission: AnswerSubmission, store: GameSessionStore, userId: string | null, ): Promise => { const session = await store.get(submission.sessionId); if (!session) { throw new NotFoundError(`Game session not found: ${submission.sessionId}`); } if (session.userId !== userId) { throw new NotFoundError(`Game session not found: ${submission.sessionId}`); } const answer = session.answers.get(submission.questionId); if (answer === undefined) { throw new ConflictError( `Question already answered: ${submission.questionId}`, ); } const updatedAnswers = new Map(session.answers); updatedAnswers.delete(submission.questionId); if (updatedAnswers.size === 0) { await store.delete(submission.sessionId); } else { await store.update(submission.sessionId, { answers: updatedAnswers, userId: session.userId, }); } return { questionId: submission.questionId, isCorrect: submission.selectedOptionId === answer.correctOptionId, correctOptionId: answer.correctOptionId, selectedOptionId: submission.selectedOptionId, }; };