feat(api): add answer evaluation endpoint

Complete the game answer flow:

- Add evaluateAnswer service function: looks up the session in the
  GameSessionStore, compares the submitted optionId against the stored
  correct answer, returns an AnswerResult.
- Add submitAnswer controller with safeParse validation and error
  handling (session/question not found → 404).
- Add POST /api/v1/game/answer route.
- Fix createGameSession: was missing the answerKey tracking and the
  gameSessionStore.create() call, so sessions were never persisted.

The full singleplayer game loop now works end-to-end:
POST /game/start → GameSession, POST /game/answer → AnswerResult.
This commit is contained in:
lila 2026-04-11 12:12:54 +02:00
parent 0755c57439
commit 075a691849
4 changed files with 83 additions and 9 deletions

View file

@ -1,6 +1,6 @@
import type { Request, Response } from "express";
import { GameRequestSchema } from "@glossa/shared";
import { createGameSession } from "../services/gameService.js";
import { GameRequestSchema, AnswerSubmissionSchema } from "@glossa/shared";
import { createGameSession, evaluateAnswer } from "../services/gameService.js";
export const createGame = async (req: Request, res: Response) => {
const gameSettings = GameRequestSchema.safeParse(req.body);
@ -15,3 +15,18 @@ export const createGame = async (req: Request, res: Response) => {
res.json({ success: true, data: gameQuestions });
};
export const submitAnswer = async (req: Request, res: Response) => {
const submission = AnswerSubmissionSchema.safeParse(req.body);
// TODO: remove when global error handler is implemented
if (!submission.success) {
res.status(400).json({ success: false });
return;
}
try {
const result = await evaluateAnswer(submission.data);
res.json({ success: true, data: result });
} catch (error) {
res.status(404).json({ success: false });
}
};

View file

@ -1,7 +1,8 @@
import express from "express";
import type { Router } from "express";
import { createGame } from "../controllers/gameController.js";
import { createGame, submitAnswer } from "../controllers/gameController.js";
export const gameRouter: Router = express.Router();
gameRouter.post("/start", createGame);
gameRouter.post("/answer", submitAnswer);

View file

@ -5,8 +5,14 @@ import type {
GameSession,
GameQuestion,
AnswerOption,
AnswerSubmission,
AnswerResult,
} from "@glossa/shared";
import { InMemoryGameSessionStore } from "../gameSessionStore/index.js";
const gameSessionStore = new InMemoryGameSessionStore();
export const createGameSession = async (
request: GameRequest,
): Promise<GameSession> => {
@ -18,6 +24,8 @@ export const createGameSession = async (
Number(request.rounds),
);
const answerKey = new Map<string, number>();
const questions: GameQuestion[] = await Promise.all(
correctAnswers.map(async (correctAnswer) => {
const distractorTexts = await getDistractors(
@ -32,13 +40,18 @@ export const createGameSession = async (
const optionTexts = [correctAnswer.targetText, ...distractorTexts];
const shuffledTexts = shuffle(optionTexts);
const correctOptionId = shuffledTexts.indexOf(correctAnswer.targetText);
const options: AnswerOption[] = shuffledTexts.map((text, index) => ({
optionId: index,
text,
}));
const questionId = randomUUID();
answerKey.set(questionId, correctOptionId);
return {
questionId: randomUUID(),
questionId,
prompt: correctAnswer.sourceText,
gloss: correctAnswer.sourceGloss,
options,
@ -46,9 +59,11 @@ export const createGameSession = async (
}),
);
return { sessionId: randomUUID(), questions };
};
const sessionId = randomUUID();
await gameSessionStore.create(sessionId, { answers: answerKey });
return { sessionId, questions };
};
const shuffle = <T>(array: T[]): T[] => {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
@ -59,3 +74,25 @@ const shuffle = <T>(array: T[]): T[] => {
}
return result;
};
export const evaluateAnswer = async (
submission: AnswerSubmission,
): Promise<AnswerResult> => {
const session = await gameSessionStore.get(submission.sessionId);
if (!session) {
throw new Error(`Game session not found: ${submission.sessionId}`);
}
const correctOptionId = session.answers.get(submission.questionId);
if (correctOptionId === undefined) {
throw new Error(`Question not found: ${submission.questionId}`);
}
return {
questionId: submission.questionId,
isCorrect: submission.selectedOptionId === correctOptionId,
correctOptionId,
};
};