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 type { Request, Response } from "express";
|
||||||
import { GameRequestSchema } from "@glossa/shared";
|
import { GameRequestSchema, AnswerSubmissionSchema } from "@glossa/shared";
|
||||||
import { createGameSession } from "../services/gameService.js";
|
import { createGameSession, evaluateAnswer } from "../services/gameService.js";
|
||||||
|
|
||||||
export const createGame = async (req: Request, res: Response) => {
|
export const createGame = async (req: Request, res: Response) => {
|
||||||
const gameSettings = GameRequestSchema.safeParse(req.body);
|
const gameSettings = GameRequestSchema.safeParse(req.body);
|
||||||
|
|
@ -15,3 +15,18 @@ export const createGame = async (req: Request, res: Response) => {
|
||||||
|
|
||||||
res.json({ success: true, data: gameQuestions });
|
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 express from "express";
|
||||||
import type { Router } 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();
|
export const gameRouter: Router = express.Router();
|
||||||
|
|
||||||
gameRouter.post("/start", createGame);
|
gameRouter.post("/start", createGame);
|
||||||
|
gameRouter.post("/answer", submitAnswer);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,14 @@ import type {
|
||||||
GameSession,
|
GameSession,
|
||||||
GameQuestion,
|
GameQuestion,
|
||||||
AnswerOption,
|
AnswerOption,
|
||||||
|
AnswerSubmission,
|
||||||
|
AnswerResult,
|
||||||
} from "@glossa/shared";
|
} from "@glossa/shared";
|
||||||
|
|
||||||
|
import { InMemoryGameSessionStore } from "../gameSessionStore/index.js";
|
||||||
|
|
||||||
|
const gameSessionStore = new InMemoryGameSessionStore();
|
||||||
|
|
||||||
export const createGameSession = async (
|
export const createGameSession = async (
|
||||||
request: GameRequest,
|
request: GameRequest,
|
||||||
): Promise<GameSession> => {
|
): Promise<GameSession> => {
|
||||||
|
|
@ -18,6 +24,8 @@ export const createGameSession = async (
|
||||||
Number(request.rounds),
|
Number(request.rounds),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const answerKey = new Map<string, number>();
|
||||||
|
|
||||||
const questions: GameQuestion[] = await Promise.all(
|
const questions: GameQuestion[] = await Promise.all(
|
||||||
correctAnswers.map(async (correctAnswer) => {
|
correctAnswers.map(async (correctAnswer) => {
|
||||||
const distractorTexts = await getDistractors(
|
const distractorTexts = await getDistractors(
|
||||||
|
|
@ -32,13 +40,18 @@ export const createGameSession = async (
|
||||||
const optionTexts = [correctAnswer.targetText, ...distractorTexts];
|
const optionTexts = [correctAnswer.targetText, ...distractorTexts];
|
||||||
const shuffledTexts = shuffle(optionTexts);
|
const shuffledTexts = shuffle(optionTexts);
|
||||||
|
|
||||||
|
const correctOptionId = shuffledTexts.indexOf(correctAnswer.targetText);
|
||||||
|
|
||||||
const options: AnswerOption[] = shuffledTexts.map((text, index) => ({
|
const options: AnswerOption[] = shuffledTexts.map((text, index) => ({
|
||||||
optionId: index,
|
optionId: index,
|
||||||
text,
|
text,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const questionId = randomUUID();
|
||||||
|
answerKey.set(questionId, correctOptionId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
questionId: randomUUID(),
|
questionId,
|
||||||
prompt: correctAnswer.sourceText,
|
prompt: correctAnswer.sourceText,
|
||||||
gloss: correctAnswer.sourceGloss,
|
gloss: correctAnswer.sourceGloss,
|
||||||
options,
|
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 shuffle = <T>(array: T[]): T[] => {
|
||||||
const result = [...array];
|
const result = [...array];
|
||||||
for (let i = result.length - 1; i > 0; i--) {
|
for (let i = result.length - 1; i > 0; i--) {
|
||||||
|
|
@ -59,3 +74,25 @@ const shuffle = <T>(array: T[]): T[] => {
|
||||||
}
|
}
|
||||||
return result;
|
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() {
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -10,9 +11,29 @@ async function main() {
|
||||||
rounds: "3",
|
rounds: "3",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
const game = await startResponse.json();
|
||||||
|
console.log("Game started:", JSON.stringify(game, null, 2));
|
||||||
|
|
||||||
const data = await response.json();
|
// Step 2: answer each question (always pick option 0)
|
||||||
console.log(JSON.stringify(data, null, 2));
|
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();
|
main();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue