Skip to content

Commit 606dd5f

Browse files
committed
помолимся чтобы работало - СУПЕР ИГРА ЧЕК!!!
1 parent 43b6e09 commit 606dd5f

29 files changed

Lines changed: 1187 additions & 79 deletions

artifacts/api-server/build.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ globalThis.__dirname = __bannerPath.dirname(globalThis.__filename);
119119
},
120120
});
121121

122-
for (const id of [1, 2, 3]) {
122+
for (const id of [1, 2, 3, 4]) {
123123
await cp(
124124
path.resolve(artifactDir, `src/lib/default-adepts-quiz-board-${id}.json`),
125125
path.resolve(distDir, `default-adepts-quiz-board-${id}.json`),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"themes": ["СУПЕР ИГРА!"],
3+
"questions": [
4+
[
5+
{
6+
"text": "",
7+
"questionUrl": "/super-game-card-1.png",
8+
"answerText": "",
9+
"answerUrl": ""
10+
},
11+
{
12+
"text": "",
13+
"questionUrl": "/super-game-card-2.png",
14+
"answerText": "",
15+
"answerUrl": ""
16+
},
17+
{
18+
"text": "",
19+
"questionUrl": "/super-game-card-3.png",
20+
"answerText": "",
21+
"answerUrl": ""
22+
},
23+
{
24+
"text": "",
25+
"questionUrl": "/super-game-card-4.png",
26+
"answerText": "",
27+
"answerUrl": ""
28+
}
29+
]
30+
]
31+
}

artifacts/api-server/src/adepts-quiz-board-routes.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ function boardIdFromReq(
2626
const raw = req.params["boardId"];
2727
const id = parsePersistedBoardId(raw);
2828
if (!id) {
29-
res.status(400).json({ ok: false, error: "boardId must be 1, 2, or 3" });
29+
res.status(400).json({ ok: false, error: "boardId must be 1, 2, 3, or 4" });
3030
return null;
3131
}
3232
return id;
3333
}
3434

3535
function parsePutBody(
36-
body: unknown
36+
body: unknown,
37+
boardId: AdeptsPersistedBoardId
3738
): { ok: true; board: AdeptsQuizBoardPersisted } | { ok: false; error: string } {
3839
if (!body || typeof body !== "object") {
3940
return { ok: false, error: "JSON body required" };
@@ -83,13 +84,26 @@ function parsePutBody(
8384
if (themes.length === 0) {
8485
return { ok: false, error: "At least one theme required" };
8586
}
86-
for (const row of questions) {
87-
if (row.length !== 5) {
88-
return { ok: false, error: "Each theme must have exactly 5 questions" };
87+
if (boardId === 4) {
88+
if (themes.length !== 1) {
89+
return { ok: false, error: "Board 4 must have exactly 1 theme" };
90+
}
91+
const r0 = questions[0];
92+
if (!Array.isArray(r0) || r0.length !== 4) {
93+
return { ok: false, error: "Board 4 must have exactly 4 questions in the single row" };
94+
}
95+
if (questions.length !== 1) {
96+
return { ok: false, error: "Board 4 must have exactly one questions row" };
97+
}
98+
} else {
99+
for (const row of questions) {
100+
if (row.length !== 5) {
101+
return { ok: false, error: "Each theme must have exactly 5 questions" };
102+
}
103+
}
104+
if (questions.length !== themes.length) {
105+
return { ok: false, error: "themes length must match questions rows" };
89106
}
90-
}
91-
if (questions.length !== themes.length) {
92-
return { ok: false, error: "themes length must match questions rows" };
93107
}
94108
const board: AdeptsQuizBoardPersisted = {
95109
themes,
@@ -138,7 +152,7 @@ export function attachAdeptsQuizBoardRoutes(app: Express): void {
138152
} else {
139153
id = 1;
140154
}
141-
const parsed = parsePutBody(req.body);
155+
const parsed = parsePutBody(req.body, id);
142156
if (!parsed.ok) {
143157
res.status(400).json({ ok: false, error: parsed.error });
144158
return;

artifacts/api-server/src/adepts.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
applyPickCell,
1313
applyPlayerDonation,
1414
applyHostMounts400DedDonationDoubleReward,
15+
applySuperTttPick,
16+
applySuperTttResetBoard,
17+
applyBoard4LeaveReset,
18+
applyCloseSuperGameCard,
1519
clearHideDonationsTableOnBoard3,
1620
cloneQuizRelay,
1721
getQuizRelayOrDefault,
@@ -213,6 +217,66 @@ export function setupAdepts(io: Server): void {
213217
run();
214218
return;
215219
}
220+
case "superTttPick": {
221+
const seatResolved = resolvePickSeat(socket, cmd, host);
222+
if (!host && seatResolved === null) {
223+
logger.warn({ sessionId, socketId: socket.id }, "superTttPick rejected: no seat");
224+
return;
225+
}
226+
const seatForPick = host ? (seatResolved ?? seat ?? 0) : seatResolved!;
227+
const cellRaw = cmd["cellIndex"];
228+
const cellIndex = typeof cellRaw === "number" ? cellRaw : Number(cellRaw);
229+
const r = applySuperTttPick(sessionId, seatForPick, cellIndex, { hostBypass: host });
230+
if (!r.ok) {
231+
logger.warn(
232+
{ sessionId, socketId: socket.id, err: r.error, cellIndex, seat: seatForPick },
233+
"superTttPick rejected",
234+
);
235+
return;
236+
}
237+
run();
238+
return;
239+
}
240+
case "superTttResetBoard": {
241+
if (!host) return;
242+
const r = applySuperTttResetBoard(sessionId);
243+
if (!r.ok) {
244+
logger.warn({ sessionId, socketId: socket.id, err: r.error }, "superTttResetBoard rejected");
245+
return;
246+
}
247+
run();
248+
return;
249+
}
250+
case "board4LeaveReset": {
251+
if (!host) return;
252+
const r = applyBoard4LeaveReset(sessionId);
253+
if (!r.ok) {
254+
logger.warn({ sessionId, socketId: socket.id, err: r.error }, "board4LeaveReset rejected");
255+
return;
256+
}
257+
run();
258+
return;
259+
}
260+
case "closeSuperGameCard": {
261+
const seatResolved = resolvePickSeat(socket, cmd, host);
262+
if (!host && seatResolved === null) {
263+
logger.warn({ sessionId, socketId: socket.id }, "closeSuperGameCard rejected: no seat");
264+
return;
265+
}
266+
const seatForCmd = host ? (seatResolved ?? seat ?? 0) : seatResolved!;
267+
const t = Number(cmd["themeIndex"]);
268+
const q = Number(cmd["questionIndex"]);
269+
const r = applyCloseSuperGameCard(sessionId, seatForCmd, t, q, { hostBypass: host });
270+
if (!r.ok) {
271+
logger.warn(
272+
{ sessionId, socketId: socket.id, err: r.error, themeIndex: t, questionIndex: q },
273+
"closeSuperGameCard rejected",
274+
);
275+
return;
276+
}
277+
run();
278+
return;
279+
}
216280
case "playerDonation": {
217281
if (host) return;
218282
if (!socketClaimsPlayerRole(socket)) return;

artifacts/api-server/src/lib/adepts-quiz-board-store.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import type {
1313

1414
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1515

16-
/** Persisted quiz boards: round 1–3 match `AdeptsBoardId` in the game client. */
17-
export type AdeptsPersistedBoardId = 1 | 2 | 3;
16+
/** Persisted quiz boards: ids match `AdeptsBoardId` in the game client. */
17+
export type AdeptsPersistedBoardId = 1 | 2 | 3 | 4;
1818

1919
const QUESTION_KEYS: (keyof AdeptsQuizQuestionPersisted)[] = [
2020
"text",
@@ -31,7 +31,7 @@ const QUESTION_KEYS: (keyof AdeptsQuizQuestionPersisted)[] = [
3131

3232
export function parsePersistedBoardId(raw: unknown): AdeptsPersistedBoardId | null {
3333
const n = typeof raw === "string" ? Number(raw) : typeof raw === "number" ? raw : NaN;
34-
if (n === 1 || n === 2 || n === 3) return n;
34+
if (n === 1 || n === 2 || n === 3 || n === 4) return n;
3535
return null;
3636
}
3737

@@ -51,7 +51,7 @@ export function persistedDataFilePath(boardId: AdeptsPersistedBoardId): string {
5151
return path.resolve(process.cwd(), "data", `adepts-quiz-board-${boardId}.json`);
5252
}
5353

54-
function parseBoardJson(raw: string): AdeptsQuizBoardPersisted {
54+
function parseBoardJson(raw: string, boardId: AdeptsPersistedBoardId): AdeptsQuizBoardPersisted {
5555
const data = JSON.parse(raw) as unknown;
5656
if (!data || typeof data !== "object") throw new Error("Invalid board JSON");
5757
const o = data as Record<string, unknown>;
@@ -64,11 +64,18 @@ function parseBoardJson(raw: string): AdeptsQuizBoardPersisted {
6464
return row.map((cell) => normalizeQuestion(cell));
6565
});
6666
if (themes.length === 0) throw new Error("At least one theme required");
67-
for (const row of questions) {
68-
if (row.length !== 5) throw new Error("Each theme must have exactly 5 questions");
69-
}
70-
if (questions.length !== themes.length) {
71-
throw new Error("themes and questions row counts must match");
67+
if (boardId === 4) {
68+
if (themes.length !== 1) throw new Error("Board 4 must have exactly 1 theme row");
69+
const row = questions[0];
70+
if (!row || row.length !== 4) throw new Error("Board 4 must have exactly 4 question cells");
71+
if (questions.length !== 1) throw new Error("themes and questions row counts must match");
72+
} else {
73+
for (const row of questions) {
74+
if (row.length !== 5) throw new Error("Each theme must have exactly 5 questions");
75+
}
76+
if (questions.length !== themes.length) {
77+
throw new Error("themes and questions row counts must match");
78+
}
7279
}
7380
return { themes, questions };
7481
}
@@ -144,7 +151,7 @@ function ensurePersistedFileFromSeed(boardId: AdeptsPersistedBoardId): void {
144151
const dir = path.dirname(target);
145152
mkdirSync(dir, { recursive: true });
146153
const seed = readFileSync(seedFilePath(boardId), "utf8");
147-
parseBoardJson(seed);
154+
parseBoardJson(seed, boardId);
148155
writeFileSync(target, seed, "utf8");
149156
}
150157

@@ -163,7 +170,7 @@ export function readAdeptsQuizBoard(boardId: AdeptsPersistedBoardId): AdeptsQuiz
163170
ensurePersistedFileFromSeed(boardId);
164171
const target = persistedDataFilePath(boardId);
165172
const raw = readFileSync(target, "utf8");
166-
return parseBoardJson(raw);
173+
return parseBoardJson(raw, boardId);
167174
}
168175

169176
export function writeAdeptsQuizBoard(
@@ -212,7 +219,8 @@ export function patchQuestionCell(
212219
) {
213220
throw new Error("Invalid theme index");
214221
}
215-
if (!Number.isInteger(questionIndex) || questionIndex < 0 || questionIndex >= 5) {
222+
const rowLen = board.questions[themeIndex]!.length;
223+
if (!Number.isInteger(questionIndex) || questionIndex < 0 || questionIndex >= rowLen) {
216224
throw new Error("Invalid question index");
217225
}
218226
const row = [...board.questions[themeIndex]!];

artifacts/api-server/src/lib/adepts-quiz-relay-types.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,24 @@ export type AdeptsDonationLogEntry = {
2222
seatIndex?: number;
2323
};
2424

25+
export type AdeptsSuperTttMark = "X" | "O";
26+
27+
/** Крестики-нолики 5×5: победа — линия из 4 символов (после 4 карточек на доске 4). */
28+
export type AdeptsSuperTttState = {
29+
cells: (AdeptsSuperTttMark | null)[];
30+
nextIsX: boolean;
31+
seatX: number;
32+
seatO: number;
33+
};
34+
35+
export type AdeptsSuperTttWinner = {
36+
nick: string;
37+
atMs: number;
38+
};
39+
2540
export type AdeptsQuizRelayPayload = {
2641
boardRoom: string;
27-
/** Board id (1 | 2 | 3). Used to detect cross-board relay contamination. */
42+
/** Board id (1–4). Used to detect cross-board relay contamination. */
2843
boardId?: number;
2944
players: AdeptsRelayPlayer[];
3045
activeQuizCard: AdeptsRelayActiveCard | null;
@@ -39,9 +54,15 @@ export type AdeptsQuizRelayPayload = {
3954
donationLog?: AdeptsDonationLogEntry[];
4055
/** После бонуса ×2 с «деда» на 400 — скрыть таблицу на квиз-доске 3; сброс при входе на похороны. */
4156
hideDonationsTableOnBoard3?: boolean;
42-
/** Титры (квиз-доска 3); обновляется только если поле присутствует в relay от доски 3. */
57+
/** Титры (квиз-доски 3 и 4); в relay от досок 1/2 не приходят — сервер сбрасывает. */
4358
creditsRollActive?: boolean;
4459
creditsRollStartedAt?: number;
60+
/** Доска «СУПЕР ИГРА!» (id 4): поле 5 клеток после открытия всех карточек. */
61+
superTtt?: AdeptsSuperTttState | null;
62+
/** Победитель крестиков-ноликов (оверлей ~5 с). */
63+
superTttWinner?: AdeptsSuperTttWinner | null;
64+
/** Доска 4: место игрока, открывшего верхнюю правую карточку (тема 0, вопрос 1 в сетке 2×2) — ○ против лидера (✕). */
65+
superBoardFourKeyOpenerSeat?: number | null;
4566
};
4667

4768
const THEME_COUNT = 8;

0 commit comments

Comments
 (0)