- 四套主题(雅致木纹/心动粉/暗夜紫/水墨江南):CSS 变量 + Canvas 棋盘配色联动 - 落子弹跳、最后一手呼吸光晕、五连扫光动画、终局爱心跳动卡片 - Web Audio 合成音效 + 手机震动反馈(落子/胜利/悔棋/表情/爱心) - 自定义昵称(服务端座位持久化)+ 房间战绩统计(局数/胜负/和棋/决胜手数) - 长按棋盘随机发送甜蜜表情,实时漂浮同步给对手(3 秒冷却防刷) - 爱心连珠彩蛋:服务端模板检测棋盘爱心形状(含旋转镜像),触发全场爱心雨 - 双击棋盘点燃小爱心;随机甜蜜结算文案;大厅双人剪影插画与浪漫渐变 - 首页暴露局域网地址(vite host: true)
144 lines
4.1 KiB
TypeScript
144 lines
4.1 KiB
TypeScript
/**
|
|
* 纯规则模块:坐标换算、落子合法性、胜负判定。
|
|
* 不依赖任何 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>(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<Array<Array<0 | 1>>> = [
|
|
// 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<0 | 1>>): Array<Array<0 | 1>> {
|
|
return t.map((row) => [...row].reverse())
|
|
}
|
|
|
|
/** 顺时针旋转 90° */
|
|
function rotate(t: Array<Array<0 | 1>>): Array<Array<0 | 1>> {
|
|
const h = t.length
|
|
const w = t[0]!.length
|
|
const out: Array<Array<0 | 1>> = []
|
|
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<Array<Array<0 | 1>>> = [base]
|
|
const seen = new Set<string>()
|
|
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
|
|
}
|