- 四套主题(雅致木纹/心动粉/暗夜紫/水墨江南):CSS 变量 + Canvas 棋盘配色联动 - 落子弹跳、最后一手呼吸光晕、五连扫光动画、终局爱心跳动卡片 - Web Audio 合成音效 + 手机震动反馈(落子/胜利/悔棋/表情/爱心) - 自定义昵称(服务端座位持久化)+ 房间战绩统计(局数/胜负/和棋/决胜手数) - 长按棋盘随机发送甜蜜表情,实时漂浮同步给对手(3 秒冷却防刷) - 爱心连珠彩蛋:服务端模板检测棋盘爱心形状(含旋转镜像),触发全场爱心雨 - 双击棋盘点燃小爱心;随机甜蜜结算文案;大厅双人剪影插画与浪漫渐变 - 首页暴露局域网地址(vite host: true)
151 lines
4.4 KiB
TypeScript
151 lines
4.4 KiB
TypeScript
/**
|
|
* 前后端共享协议:棋盘常量、事件名、状态与载荷类型。
|
|
* 仅此一处定义,避免两端漂移。
|
|
*/
|
|
|
|
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
|
|
/** 昵称,空串表示未设置(前端显示「黑棋/白棋」) */
|
|
nickname: string
|
|
}
|
|
|
|
/** 房间内累计战绩(进程内存,重启即清零) */
|
|
export interface RoomStats {
|
|
/** 当前进行的局数编号,从 1 起 */
|
|
round: number
|
|
blackWins: number
|
|
whiteWins: number
|
|
draws: number
|
|
}
|
|
|
|
/** 可发送给对手的表情符号 */
|
|
export const EMOJIS = ['❤️', '💕', '✨', '🌟', '💗', '😘', '🥰', '💌', '🍀', '💘'] as const
|
|
|
|
/**
|
|
* 完整对局状态(服务端权威,每次变更后全量下发)。
|
|
* 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 = 未触发;否则前端播放爱心雨特效并提示。
|
|
*/
|
|
heartBy: Player | null
|
|
/** 房间累计战绩 */
|
|
stats: RoomStats
|
|
/** 接收者自己执子颜色,未入座为 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',
|
|
/** 发送表情给对手(长按棋盘),ack: ActionAck */
|
|
emojiSend: 'emoji:send',
|
|
/** 服务端 → 客户端:收到对手表情 */
|
|
emojiRecv: 'emoji:recv',
|
|
/** 服务端 → 客户端:全量状态推送 */
|
|
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
|
|
}
|
|
|
|
/** 发送表情载荷:emoji 必须是 EMOJIS 中的字符 */
|
|
export interface EmojiPayload {
|
|
emoji: string
|
|
}
|
|
|
|
/** 音效场景 */
|
|
export type SoundKind = 'place' | 'win' | 'undo' | 'emoji' | 'heart'
|
|
|
|
/** 悔棋请求自动失效时长 */
|
|
export const UNDO_TIMEOUT_MS = 60_000
|
|
/** 对手离线多久后允许判负 */
|
|
export const OFFLINE_CLAIM_MS = 60_000
|
|
/** 双方均离线多久后回收房间 */
|
|
export const ROOM_GC_MS = 30 * 60_000
|