Рейтинг: ввод деталей партии и предупреждения в формах
Форма завершения и правка результатов (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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user