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; }