feat(api): add WebSocket foundation and multiplayer game store
- Add ws/ directory: server setup, auth, router, connections map - WebSocket auth rejects upgrade with 401 if no Better Auth session - Router parses WsClientMessageSchema, dispatches to handlers, two-layer error handling (AppError -> WsErrorSchema, unknown -> 500) - connections.ts: in-memory Map<lobbyId, Map<userId, WebSocket>> with addConnection, removeConnection, broadcastToLobby - LobbyGameStore interface + InMemoryLobbyGameStore implementation following existing GameSessionStore pattern - multiplayerGameService: generateMultiplayerQuestions() decoupled from single-player flow, hardcoded defaults en->it nouns easy 3 rounds - handleLobbyJoin and handleLobbyLeave implemented - WsErrorSchema added to shared schemas - server.ts switched to createServer + setupWebSocket
This commit is contained in:
parent
b0aef8cc16
commit
745c5c4e3a
14 changed files with 443 additions and 1 deletions
75
apps/api/src/services/multiplayerGameService.ts
Normal file
75
apps/api/src/services/multiplayerGameService.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { randomUUID } from "crypto";
|
||||
import { getGameTerms, getDistractors } from "@lila/db";
|
||||
import type {
|
||||
GameQuestion,
|
||||
AnswerOption,
|
||||
SupportedLanguageCode,
|
||||
SupportedPos,
|
||||
DifficultyLevel,
|
||||
} from "@lila/shared";
|
||||
|
||||
// TODO(game-mode-slice): replace with lobby settings when mode selection lands
|
||||
const MULTIPLAYER_DEFAULTS = {
|
||||
sourceLanguage: "en" as SupportedLanguageCode,
|
||||
targetLanguage: "it" as SupportedLanguageCode,
|
||||
pos: "noun" as SupportedPos,
|
||||
difficulty: "easy" as DifficultyLevel,
|
||||
rounds: 3,
|
||||
};
|
||||
|
||||
const shuffle = <T>(array: T[]): T[] => {
|
||||
const result = [...array];
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const temp = result[i]!;
|
||||
result[i] = result[j]!;
|
||||
result[j] = temp;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export type MultiplayerQuestion = GameQuestion & { correctOptionId: number };
|
||||
|
||||
export const generateMultiplayerQuestions = async (): Promise<
|
||||
MultiplayerQuestion[]
|
||||
> => {
|
||||
const correctAnswers = await getGameTerms(
|
||||
MULTIPLAYER_DEFAULTS.sourceLanguage,
|
||||
MULTIPLAYER_DEFAULTS.targetLanguage,
|
||||
MULTIPLAYER_DEFAULTS.pos,
|
||||
MULTIPLAYER_DEFAULTS.difficulty,
|
||||
MULTIPLAYER_DEFAULTS.rounds,
|
||||
);
|
||||
|
||||
const questions: MultiplayerQuestion[] = await Promise.all(
|
||||
correctAnswers.map(async (correctAnswer) => {
|
||||
const distractorTexts = await getDistractors(
|
||||
correctAnswer.termId,
|
||||
correctAnswer.targetText,
|
||||
MULTIPLAYER_DEFAULTS.targetLanguage,
|
||||
MULTIPLAYER_DEFAULTS.pos,
|
||||
MULTIPLAYER_DEFAULTS.difficulty,
|
||||
3,
|
||||
);
|
||||
|
||||
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,
|
||||
}));
|
||||
|
||||
return {
|
||||
questionId: randomUUID(),
|
||||
prompt: correctAnswer.sourceText,
|
||||
gloss: correctAnswer.sourceGloss,
|
||||
options,
|
||||
correctOptionId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return questions;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue