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:
parent
0755c57439
commit
075a691849
4 changed files with 83 additions and 9 deletions
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue