/** * 纯规则模块:坐标换算、落子合法性、胜负判定。 * 不依赖任何 I/O,便于单独推演和测试。 */ import { BOARD_SIZE, CELL_COUNT, WIN_COUNT, type Cell, type Player } from '../shared/protocol.js' export function idx(x: number, y: number): number { return y * BOARD_SIZE + x } export function inBounds(x: number, y: number): boolean { return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE } export function createBoard(): Cell[] { return new Array(CELL_COUNT).fill(0) } /** * 以最后落子点为中心,向横、竖、两条斜线四个方向统计同色连子。 * 返回该方向上的完整连子下标数组(长度 >= 5 即为获胜),否则返回 null。 */ export function findWinLine(board: Cell[], x: number, y: number, player: Player): number[] | null { const dirs: Array<[number, number]> = [ [1, 0], [0, 1], [1, 1], [1, -1], ] for (const [dx, dy] of dirs) { const line: number[] = [idx(x, y)] // 正方向延伸 for (let cx = x + dx, cy = y + dy; inBounds(cx, cy) && board[idx(cx, cy)] === player; cx += dx, cy += dy) { line.push(idx(cx, cy)) } // 反方向延伸,插到数组头部以保持连线有序 for (let cx = x - dx, cy = y - dy; inBounds(cx, cy) && board[idx(cx, cy)] === player; cx -= dx, cy -= dy) { line.unshift(idx(cx, cy)) } if (line.length >= WIN_COUNT) return line } return null } export function isEmptyCell(board: Cell[], x: number, y: number): boolean { return inBounds(x, y) && board[idx(x, y)] === 0 } export function opponent(p: Player): Player { return p === 1 ? 2 : 1 } /* ------------------------------------------------------------------ */ /* 爱心连珠彩蛋:预设爱心形状模板,任一方棋子完整覆盖即触发 */ /* 模板用 1 图案 / 0 空格表示,扫描全盘(含 4 向旋转),支持 any 尺寸 */ /* ------------------------------------------------------------------ */ const HEART_TEMPLATES: Array>> = [ // 4x4 小爱心(12 格) [ [0, 1, 1, 0], [1, 1, 1, 1], [1, 1, 1, 1], [0, 1, 1, 0], ], // 5x5 大爱心(12 格,尖角形) [ [0, 1, 0, 1, 0], [1, 1, 1, 1, 1], [0, 1, 1, 1, 0], [0, 0, 1, 0, 0], ], ] /** 镜像模板(左右翻转) */ function mirror(t: Array>): Array> { return t.map((row) => [...row].reverse()) } /** 顺时针旋转 90° */ function rotate(t: Array>): Array> { const h = t.length const w = t[0]!.length const out: Array> = [] for (let x = 0; x < w; x++) { const row: Array<0 | 1> = [] for (let y = h - 1; y >= 0; y--) row.push(t[y]![x]!) out.push(row) } return out } /** * 检测 player 是否在棋盘上完整摆出了某个爱心模板(含旋转与镜像)。 * 命中返回模板格子的下标数组(用于前端撒花定位),否则 null。 */ export function findHeartLine(board: Cell[], player: Player): number[] | null { const compact: Array<{ i: number; j: number }> = [] for (const base of HEART_TEMPLATES) { // 变体集合:原形、旋转 90/180/270、每种的镜像 const variants: Array>> = [base] const seen = new Set() let cur = base for (let r = 0; r < 4; r++) { cur = rotate(cur) const key = JSON.stringify(cur) if (!seen.has(key)) { seen.add(key) variants.push(cur) variants.push(mirror(cur)) } } for (const tmpl of variants) { const th = tmpl.length const tw = tmpl[0]!.length compact.length = 0 for (let i = 0; i < th; i++) { for (let j = 0; j < tw; j++) { if (tmpl[i]![j]) compact.push({ i, j }) } } // 全盘扫描偏移 for (let oy = 0; oy + th <= BOARD_SIZE; oy++) { for (let ox = 0; ox + tw <= BOARD_SIZE; ox++) { const all = compact.every( ({ i, j }) => board[idx(ox + j, oy + i)] === player, ) if (all) { return compact.map(({ i, j }) => idx(ox + j, oy + i)) } } } } } return null }