/
docNemo
/
tactic-hack-core
Обзор
Документация
Войти
/
docNemo
/
tactic-hack-core
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/match.ts
124 строки
4 KB
neo
Implement rules engine: geometry, placement, attack reducer, replay, projection, deduction, protocol types
19 июл 2026, 00:03
19 июл 2026, 00:03
d1403fb
Код
Авторство
О чём код?
import { countSolidLines } from "./geometry.js"; import { validatePlacement } from "./placement.js"; import { AttackRecord, Cell, inBounds, MatchSetup, OpenedCell, opponentOf, PlayerId, sameCell, SHIPS_PER_PLAYER, } from "./types.js"; /** Полное (серверное) состояние боя. Клиентам целиком не отправляется. */ export interface MatchState { setup: MatchSetup; attacks: AttackRecord[]; turn: PlayerId; /** Открытые пустые клетки на поле каждого игрока (ключ — владелец поля). */ opened: Record<PlayerId, OpenedCell[]>; /** Уничтоженные корабли на поле каждого игрока (ключ — владелец поля). */ destroyed: Record<PlayerId, Cell[]>; winner: PlayerId | null; } export type AttackError = | "gameFinished" | "notYourTurn" | "outOfBounds" | "cellAlreadyOpened"; export type AttackOutcome = | { kind: "miss"; digit: number } | { kind: "hit"; victory: boolean }; export type AttackResult = | { ok: true; state: MatchState; outcome: AttackOutcome } | { ok: false; code: AttackError }; export function createMatchState(setup: MatchSetup): MatchState { for (const player of ["p1", "p2"] as const) { const valid = validatePlacement(setup.placements[player]); if (!valid.ok) { throw new Error(`Invalid placement for ${player}: ${valid.code}`); } } return { setup, attacks: [], turn: setup.firstTurn, opened: { p1: [], p2: [] }, destroyed: { p1: [], p2: [] }, winner: null, }; } /** Применить атаку. Невалидный ход не меняет состояние и не передаёт ход. */ export function applyAttack( state: MatchState, attacker: PlayerId, cell: Cell, ): AttackResult { if (state.winner !== null) return { ok: false, code: "gameFinished" }; if (attacker !== state.turn) return { ok: false, code: "notYourTurn" }; if (!inBounds(cell)) return { ok: false, code: "outOfBounds" }; const defender = opponentOf(attacker); const alreadyOpened = state.opened[defender].some((o) => sameCell(o.cell, cell)) || state.destroyed[defender].some((s) => sameCell(s, cell)); if (alreadyOpened) return { ok: false, code: "cellAlreadyOpened" }; const defenderShips = state.setup.placements[defender]; const attacks = [...state.attacks, { attacker, cell }]; if (defenderShips.some((ship) => sameCell(ship, cell))) { const destroyed = [...state.destroyed[defender], cell]; const victory = destroyed.length === SHIPS_PER_PLAYER; return { ok: true, state: { ...state, attacks, destroyed: { ...state.destroyed, [defender]: destroyed }, winner: victory ? attacker : null, // попадание: ход остаётся у атакующего, серия не ограничена turn: attacker, }, outcome: { kind: "hit", victory }, }; } const digit = countSolidLines(defenderShips, cell); return { ok: true, state: { ...state, attacks, opened: { ...state.opened, [defender]: [...state.opened[defender], { cell, digit }], }, turn: defender, }, outcome: { kind: "miss", digit }, }; } /** Детерминированное восстановление состояния свёрткой журнала атак. */ export function replayMatch( setup: MatchSetup, attacks: readonly AttackRecord[], ): MatchState { let state = createMatchState(setup); for (const attack of attacks) { const result = applyAttack(state, attack.attacker, attack.cell); if (!result.ok) { throw new Error(`Invalid attack in journal: ${result.code}`); } state = result.state; } return state; }