From 075a69184901a2dbd9ac1dd998a9fd0dee806e6d Mon Sep 17 00:00:00 2001 From: lila Date: Sat, 11 Apr 2026 12:12:54 +0200 Subject: [PATCH] feat(api): add answer evaluation endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/api/src/controllers/gameController.ts | 19 +++++++++- apps/api/src/routes/gameRouter.ts | 3 +- apps/api/src/services/gameService.ts | 43 ++++++++++++++++++++-- scripts/gametest/test-game.ts | 27 ++++++++++++-- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/apps/api/src/controllers/gameController.ts b/apps/api/src/controllers/gameController.ts index 0266067..101763a 100644 --- a/apps/api/src/controllers/gameController.ts +++ b/apps/api/src/controllers/gameController.ts @@ -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 }); + } +}; diff --git a/apps/api/src/routes/gameRouter.ts b/apps/api/src/routes/gameRouter.ts index d1e4b33..664a640 100644 --- a/apps/api/src/routes/gameRouter.ts +++ b/apps/api/src/routes/gameRouter.ts @@ -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); diff --git a/apps/api/src/services/gameService.ts b/apps/api/src/services/gameService.ts index 542fba8..7f56cdd 100644 --- a/apps/api/src/services/gameService.ts +++ b/apps/api/src/services/gameService.ts @@ -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 => { @@ -18,6 +24,8 @@ export const createGameSession = async ( Number(request.rounds), ); + const answerKey = new Map(); + 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 = (array: T[]): T[] => { const result = [...array]; for (let i = result.length - 1; i > 0; i--) { @@ -59,3 +74,25 @@ const shuffle = (array: T[]): T[] => { } return result; }; + +export const evaluateAnswer = async ( + submission: AnswerSubmission, +): Promise => { + 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, + }; +}; diff --git a/scripts/gametest/test-game.ts b/scripts/gametest/test-game.ts index bca0e14..230bf0c 100644 --- a/scripts/gametest/test-game.ts +++ b/scripts/gametest/test-game.ts @@ -1,5 +1,6 @@ async function main() { - const response = await fetch("http://localhost:3000/api/v1/game/start", { + // Step 1: start a game + const startResponse = await fetch("http://localhost:3000/api/v1/game/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -10,9 +11,29 @@ async function main() { rounds: "3", }), }); + const game = await startResponse.json(); + console.log("Game started:", JSON.stringify(game, null, 2)); - const data = await response.json(); - console.log(JSON.stringify(data, null, 2)); + // Step 2: answer each question (always pick option 0) + for (const question of game.data.questions) { + const answerResponse = await fetch( + "http://localhost:3000/api/v1/game/answer", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + sessionId: game.data.sessionId, + questionId: question.questionId, + selectedOptionId: 0, + }), + }, + ); + const result = await answerResponse.json(); + console.log("Raw result:", JSON.stringify(result, null, 2)); + console.log( + `${question.prompt}: ${result.data.isCorrect ? "✓" : "✗"} (picked ${0}, correct was ${result.data.correctOptionId})`, + ); + } } main();