feat: 在线双人五子棋(Socket.IO + Vue3 + Canvas)
- 服务端权威状态机:落子校验、四方向五连判胜、悔棋请求、认输、离线判负、交换黑白再来一局 - 断线重连:resumeToken 凭证 + localStorage,刷新/锁屏自动回到原座位 - 移动端 Canvas 棋盘:DPR 适配、触摸容差吸附、最后一手标记、胜利连子高亮 - Docker 多阶段构建,非 root + 只读根文件系统 - k8s 清单:单副本 Recreate(内存态房态)、Ingress WebSocket 超时与粘性会话注释
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 前后端共享协议:棋盘常量、事件名、状态与载荷类型。
|
||||
* 仅此一处定义,避免两端漂移。
|
||||
*/
|
||||
|
||||
export const BOARD_SIZE = 15
|
||||
export const CELL_COUNT = BOARD_SIZE * BOARD_SIZE
|
||||
export const WIN_COUNT = 5
|
||||
|
||||
/** 落子方:1 = 黑(先手),2 = 白 */
|
||||
export type Player = 1 | 2
|
||||
|
||||
/** 单格状态:0 = 空 */
|
||||
export type Cell = 0 | Player
|
||||
|
||||
export type GameStatus = 'waiting' | 'playing' | 'over'
|
||||
|
||||
/** 终局原因:五连 / 和棋 / 认输 / 对手离线判负 */
|
||||
export type EndReason = 'five' | 'draw' | 'resign' | 'offline'
|
||||
|
||||
/** 座位是否有人、是否在线 */
|
||||
export interface SeatInfo {
|
||||
joined: boolean
|
||||
online: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整对局状态(服务端权威,每次变更后全量下发)。
|
||||
* 225 个格子的全量同步只有几百字节,用全量换掉增量同步的一致性风险。
|
||||
*/
|
||||
export interface RoomState {
|
||||
roomId: string
|
||||
/** 扁平棋盘,下标 = y * BOARD_SIZE + x */
|
||||
board: Cell[]
|
||||
turn: Player
|
||||
status: GameStatus
|
||||
/** null = 未结束;0 = 和棋 */
|
||||
winner: Player | 0 | null
|
||||
endReason: EndReason | null
|
||||
/** 获胜的连子下标,用于前端高亮 */
|
||||
winLine: number[] | null
|
||||
/** 最后一手下标,用于前端标记 */
|
||||
lastMove: number | null
|
||||
moveCount: number
|
||||
/** 谁发起了悔棋请求(未决时非 null) */
|
||||
undoRequestedBy: Player | null
|
||||
black: SeatInfo
|
||||
white: SeatInfo
|
||||
/** 接收者自己执子颜色,未入座为 null */
|
||||
seat: Player | null
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Socket.IO 事件名 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const EV = {
|
||||
/** 创建房间,ack: CreateAck */
|
||||
create: 'room:create',
|
||||
/** 加入 / 重连房间,ack: JoinAck */
|
||||
join: 'room:join',
|
||||
/** 落子,ack: ActionAck */
|
||||
move: 'game:move',
|
||||
/** 请求悔棋,ack: ActionAck */
|
||||
undoRequest: 'undo:request',
|
||||
/** 响应悔棋,ack: ActionAck */
|
||||
undoRespond: 'undo:respond',
|
||||
/** 认输,ack: ActionAck */
|
||||
resign: 'game:resign',
|
||||
/** 对手离线超时后判对方负,ack: ActionAck */
|
||||
claimOffline: 'game:claim-offline',
|
||||
/** 再来一局,ack: ActionAck */
|
||||
restart: 'game:restart',
|
||||
/** 服务端 → 客户端:全量状态推送 */
|
||||
state: 'game:state',
|
||||
/** 服务端 → 客户端:一次性提示(错误/事件) */
|
||||
notice: 'game:notice',
|
||||
} as const
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 载荷类型 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export interface CreateAck {
|
||||
ok: true
|
||||
roomId: string
|
||||
seat: Player
|
||||
/** 重连凭证,客户端存 localStorage */
|
||||
resumeToken: string
|
||||
}
|
||||
|
||||
export interface JoinAck {
|
||||
ok: true
|
||||
roomId: string
|
||||
seat: Player
|
||||
resumeToken: string
|
||||
state: RoomState
|
||||
}
|
||||
|
||||
export interface ActionAck {
|
||||
ok: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type Ack<T> = T | { ok: false; error: string }
|
||||
|
||||
export interface NoticePayload {
|
||||
level: 'info' | 'error'
|
||||
message: string
|
||||
}
|
||||
|
||||
/** 悔棋请求自动失效时长 */
|
||||
export const UNDO_TIMEOUT_MS = 60_000
|
||||
/** 对手离线多久后允许判负 */
|
||||
export const OFFLINE_CLAIM_MS = 60_000
|
||||
/** 双方均离线多久后回收房间 */
|
||||
export const ROOM_GC_MS = 30 * 60_000
|
||||
Reference in New Issue
Block a user