/
docNemo
/
tactic-hack-core
Обзор
Документация
Войти
/
docNemo
/
tactic-hack-core
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/match.test.ts
152 строки
5 KB
neo
Implement rules engine: geometry, placement, attack reducer, replay, projection, deduction, protocol types
19 июл 2026, 00:03
19 июл 2026, 00:03
d1403fb
Код
Авторство
О чём код?
import { describe, expect, it } from "vitest"; import { applyAttack, createMatchState, replayMatch } from "./match.js"; import { Cell, MatchSetup } from "./types.js"; const SHIPS_P1: Cell[] = [ { row: 0, col: 0 }, { row: 1, col: 2 }, { row: 2, col: 4 }, { row: 3, col: 6 }, { row: 4, col: 8 }, ]; const SHIPS_P2: Cell[] = [ { row: 0, col: 1 }, { row: 1, col: 3 }, { row: 2, col: 5 }, { row: 3, col: 7 }, { row: 4, col: 9 }, ]; const SETUP: MatchSetup = { placements: { p1: SHIPS_P1, p2: SHIPS_P2 }, firstTurn: "p1", }; describe("createMatchState", () => { it("отклоняет невалидную расстановку", () => { expect(() => createMatchState({ placements: { p1: SHIPS_P1, p2: SHIPS_P1.slice(0, 4) }, firstTurn: "p1", }), ).toThrow(/wrongShipCount/); }); }); describe("applyAttack: промах", () => { it("открывает цифру и передаёт ход", () => { const state = createMatchState(SETUP); // (9,0): столбец 0 пуст у p2; строка 9 пуста; диагонали: r+c=9 — нет // кораблей p2 (1,4,7,10,13), r-c=9 — нет (-1..-5). Ждём 0... проверим (9,9): // столбец 9 содержит (4,9) → 1 линия. const result = applyAttack(state, "p1", { row: 9, col: 9 }); if (!result.ok) throw new Error(result.code); expect(result.outcome).toEqual({ kind: "miss", digit: 1 }); expect(result.state.turn).toBe("p2"); expect(result.state.opened.p2).toEqual([ { cell: { row: 9, col: 9 }, digit: 1 }, ]); }); }); describe("applyAttack: попадание", () => { it("уничтожает корабль, ход остаётся у атакующего", () => { const state = createMatchState(SETUP); const result = applyAttack(state, "p1", { row: 0, col: 1 }); if (!result.ok) throw new Error(result.code); expect(result.outcome).toEqual({ kind: "hit", victory: false }); expect(result.state.turn).toBe("p1"); expect(result.state.destroyed.p2).toEqual([{ row: 0, col: 1 }]); expect(result.state.winner).toBeNull(); }); it("серия попаданий до победы", () => { let state = createMatchState(SETUP); for (const [i, ship] of SHIPS_P2.entries()) { const result = applyAttack(state, "p1", ship); if (!result.ok) throw new Error(result.code); state = result.state; expect(state.turn).toBe("p1"); if (i < SHIPS_P2.length - 1) { expect(result.outcome).toEqual({ kind: "hit", victory: false }); } else { expect(result.outcome).toEqual({ kind: "hit", victory: true }); } } expect(state.winner).toBe("p1"); }); }); describe("applyAttack: невалидные ходы", () => { it("отклоняет ход не в свою очередь", () => { const state = createMatchState(SETUP); expect(applyAttack(state, "p2", { row: 5, col: 5 })).toEqual({ ok: false, code: "notYourTurn", }); }); it("отклоняет повторную атаку без изменения состояния и хода", () => { let state = createMatchState(SETUP); const miss = applyAttack(state, "p1", { row: 9, col: 9 }); if (!miss.ok) throw new Error(miss.code); state = miss.state; const back = applyAttack(state, "p2", { row: 9, col: 8 }); if (!back.ok) throw new Error(back.code); state = back.state; const repeat = applyAttack(state, "p1", { row: 9, col: 9 }); expect(repeat).toEqual({ ok: false, code: "cellAlreadyOpened" }); expect(state.turn).toBe("p1"); }); it("отклоняет атаку по уничтоженному кораблю", () => { let state = createMatchState(SETUP); const hit = applyAttack(state, "p1", { row: 0, col: 1 }); if (!hit.ok) throw new Error(hit.code); state = hit.state; expect(applyAttack(state, "p1", { row: 0, col: 1 })).toEqual({ ok: false, code: "cellAlreadyOpened", }); }); it("отклоняет ходы после конца партии", () => { let state = createMatchState(SETUP); for (const ship of SHIPS_P2) { const result = applyAttack(state, "p1", ship); if (!result.ok) throw new Error(result.code); state = result.state; } expect(applyAttack(state, "p1", { row: 9, col: 0 })).toEqual({ ok: false, code: "gameFinished", }); }); }); describe("replayMatch", () => { it("реплей журнала даёт идентичное состояние", () => { let state = createMatchState(SETUP); const moves: { attacker: "p1" | "p2"; cell: Cell }[] = [ { attacker: "p1", cell: { row: 9, col: 9 } }, { attacker: "p2", cell: { row: 0, col: 0 } }, { attacker: "p2", cell: { row: 5, col: 5 } }, { attacker: "p1", cell: { row: 1, col: 3 } }, { attacker: "p1", cell: { row: 7, col: 0 } }, ]; for (const move of moves) { const result = applyAttack(state, move.attacker, move.cell); if (!result.ok) throw new Error(result.code); state = result.state; } expect(replayMatch(SETUP, state.attacks)).toEqual(state); }); it("бросает на невалидном журнале", () => { expect(() => replayMatch(SETUP, [{ attacker: "p2", cell: { row: 0, col: 0 } }]), ).toThrow(/notYourTurn/); }); });