feat: 在线双人五子棋(Socket.IO + Vue3 + Canvas)

- 服务端权威状态机:落子校验、四方向五连判胜、悔棋请求、认输、离线判负、交换黑白再来一局
- 断线重连:resumeToken 凭证 + localStorage,刷新/锁屏自动回到原座位
- 移动端 Canvas 棋盘:DPR 适配、触摸容差吸附、最后一手标记、胜利连子高亮
- Docker 多阶段构建,非 root + 只读根文件系统
- k8s 清单:单副本 Recreate(内存态房态)、Ingress WebSocket 超时与粘性会话注释
This commit is contained in:
root
2026-09-10 18:17:10 +08:00
commit 59bfc98afa
22 changed files with 5995 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
/**
* 纯规则模块:坐标换算、落子合法性、胜负判定。
* 不依赖任何 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
}