Files
ForbiddenStarsApp/frontend/src/domain/finishWarnings.ts
T
NotBigGhostandClaude Opus 5 cc64bb4644 Рейтинг: ввод деталей партии и предупреждения в формах
Форма завершения и правка результатов (MatchDetailPage): у каждого игрока
в PlaceEditor поля «цели» и «миры» на конец партии (у выбывшего миры
заблокированы нулём), общий блок «Итог партии» — причина победы и раунд
окончания из max_rounds партии. Всё новое уходит в общий черновик.
Причина «последний выживший» ставится сама при одном невыбывшем, выбор
заблокирован; вернули второго — причина сбрасывается и её нужно выбрать.
То же в админской правке партии.

Предупреждения (domain/finishWarnings.ts, отправку не блокируют): тип победы
и цели/миры лидеров не согласуются, досрочный конец без N целей у победителя,
у соперника целей больше, чем у победителя. Настройки группы: переключатель
«9 раундов при 5–6 игроках» через PATCH /groups/{id}. schema.d.ts
перегенерирован. #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-14 22:45:25 +03:00

102 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { LAST_STANDING, type WinReason, winReasonLabel } from "./winReasons";
export interface OutcomeSeat {
userId: number;
nickname: string;
/** Место невыбывшего; у выбывшего не учитывается. */
place: number;
eliminated: boolean;
objectives: number | null;
worlds: number | null;
}
/**
* Подсказки о несогласованных итогах партии. Это предупреждения, а не отказ: сервер
* проверяет только диапазоны и явные противоречия, а здесь — сочетания, которые по
* правилам почти наверняка опечатка. Проверка идёт только по заполненным полям.
*/
export function finishWarnings({
seats,
winReason,
endRound,
maxRounds,
}: {
seats: OutcomeSeat[];
winReason: WinReason | null;
endRound: number | null;
maxRounds: number;
}): string[] {
const out: string[] = [];
const n = seats.length;
const survivors = seats.filter((s) => !s.eliminated).sort((a, b) => a.place - b.place);
const winners = survivors.filter((s) => s.place === 1);
if (winners.length === 0 || winReason === LAST_STANDING) return out;
// Досрочно партия заканчивается, только когда кто-то набрал столько целей, сколько
// игроков за столом.
const winnerObjectives = winners.map((w) => w.objectives).filter((v): v is number => v != null);
if (
endRound != null &&
endRound < maxRounds &&
winnerObjectives.length > 0 &&
Math.max(...winnerObjectives) < n
) {
out.push(
`Партия закончилась в ${endRound}-м раунде из ${maxRounds}, но у победителя меньше ${n} целей — ` +
`досрочно побеждает тот, кто набрал ${n}.`,
);
}
// Остальное сравнивает победителя с соперниками — при ничьей за 1-е место не с кем.
if (winners.length > 1) return out;
const winner = winners[0];
const secondPlace = survivors.find((s) => s.place > 1)?.place;
const runnersUp = survivors.filter((s) => s.place === secondPlace);
const label = winReasonLabel(winReason).toLowerCase();
for (const r of runnersUp) {
const objKnown = winner.objectives != null && r.objectives != null;
const worldsKnown = winner.worlds != null && r.worlds != null;
if (winReason === "objectives" && objKnown && winner.objectives === r.objectives) {
out.push(
`У ${winner.nickname} и ${r.nickname} поровну целей (${winner.objectives}) — ` +
"при равенстве победа определяется по мирам, пластику или ресурсам.",
);
}
if (winReason && winReason !== "objectives" && objKnown && winner.objectives !== r.objectives) {
out.push(
`Победа ${label} предполагает равные цели у лидеров: у ${winner.nickname} — ` +
`${winner.objectives}, у ${r.nickname} — ${r.objectives}.`,
);
}
if (winReason === "worlds" && worldsKnown && winner.worlds! <= r.worlds!) {
out.push(
`Победа по мирам, но у ${winner.nickname} миров не больше, чем у ${r.nickname} ` +
`(${winner.worlds} и ${r.worlds}).`,
);
}
if (
(winReason === "plastic" || winReason === "resources") &&
worldsKnown &&
winner.worlds !== r.worlds
) {
out.push(
`Победа ${label} предполагает равные миры у лидеров: у ${winner.nickname} — ` +
`${winner.worlds}, у ${r.nickname} — ${r.worlds}.`,
);
}
}
if (winner.objectives != null) {
for (const s of seats) {
if (s.userId !== winner.userId && s.objectives != null && s.objectives > winner.objectives) {
out.push(
`У ${s.nickname} целей больше, чем у победителя ${winner.nickname} ` +
`(${s.objectives} против ${winner.objectives}).`,
);
}
}
}
return out;
}