Форма завершения и правка результатов (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
102 lines
4.2 KiB
TypeScript
102 lines
4.2 KiB
TypeScript
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;
|
||
}
|