- 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
75 lines
2.1 KiB
TypeScript
75 lines
2.1 KiB
TypeScript
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;
|
|
};
|