Рейтинг: ввод деталей партии и предупреждения в формах

Форма завершения и правка результатов (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
This commit is contained in:
2026-09-14 22:45:25 +03:00
co-authored by Claude Opus 5
parent 6ea4a67672
commit cc64bb4644
10 changed files with 683 additions and 130 deletions
+101
View File
@@ -0,0 +1,101 @@
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;
}