/** * 前后端共享协议:棋盘常量、事件名、状态与载荷类型。 * 仅此一处定义,避免两端漂移。 */ 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 | { 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