refactor(game): 重构游戏状态管理和WebSocket通信
- 定义统一的游戏动作类型GameAction替代原有发送函数 - 创建游戏状态管理store使用Pinia进行状态管理 - 实现游戏状态分发器处理各种游戏事件 - 重构WebSocket处理器支持多处理器注册 - 重命名状态类型文件统一使用State后缀 - 添加ACTION游戏阶段处理操作窗口逻辑 - 集成Pinia依赖管理应用状态
This commit is contained in:
@@ -1,145 +1,55 @@
|
||||
import type {ActionButtonState} from "./types.ts"
|
||||
import {wsClient} from "../ws/client.ts"
|
||||
import type {GameState, PendingClaimState} from "../types/state";
|
||||
import type {Tile} from "../types/tile.ts";
|
||||
|
||||
|
||||
export function sendGameAction(
|
||||
params: {
|
||||
type: ActionButtonState['type']
|
||||
userID: string
|
||||
roomId: string
|
||||
selectedTile?: string | null
|
||||
},
|
||||
ctx: {
|
||||
actionPending: { value: boolean }
|
||||
logWsSend: (msg: any) => void
|
||||
pushWsMessage: (msg: string) => void
|
||||
/**
|
||||
* 游戏动作定义(只描述“发生了什么”)
|
||||
*/
|
||||
export type GameAction =
|
||||
// 初始化整局(进入房间 / 断线重连)
|
||||
| {
|
||||
type: 'GAME_INIT'
|
||||
payload: GameState
|
||||
}
|
||||
|
||||
// 开始游戏(发牌完成)
|
||||
| {
|
||||
type: 'GAME_START'
|
||||
payload: {
|
||||
dealerIndex: number
|
||||
}
|
||||
): void {
|
||||
}
|
||||
|
||||
const {type, userID, roomId, selectedTile} = params
|
||||
const {actionPending, logWsSend, pushWsMessage} = ctx
|
||||
|
||||
// 简单登录判断
|
||||
if (!userID) {
|
||||
console.log('当前用户未登录')
|
||||
return
|
||||
// 摸牌
|
||||
| {
|
||||
type: 'DRAW_TILE'
|
||||
payload: {
|
||||
playerId: string
|
||||
tile: Tile
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = {}
|
||||
}
|
||||
|
||||
// 出牌
|
||||
if (type === 'discard' && selectedTile) {
|
||||
payload.tile = selectedTile
|
||||
payload.discard_tile = selectedTile
|
||||
payload.code = selectedTile
|
||||
| {
|
||||
type: 'PLAY_TILE'
|
||||
payload: {
|
||||
playerId: string
|
||||
tile: Tile
|
||||
nextSeat: number
|
||||
}
|
||||
|
||||
actionPending.value = true
|
||||
|
||||
const message = {
|
||||
type,
|
||||
sender: userID,
|
||||
target: 'room',
|
||||
roomId,
|
||||
seq: Date.now(),
|
||||
payload,
|
||||
}
|
||||
|
||||
logWsSend(message)
|
||||
wsClient.send(message)
|
||||
pushWsMessage(`[client] 请求${type}`)
|
||||
}
|
||||
|
||||
|
||||
export function sendStartGame(params: {
|
||||
userID: string
|
||||
roomId: string
|
||||
canStartGame: boolean
|
||||
startGamePending: { value: boolean }
|
||||
logWsSend: (msg: any) => void
|
||||
pushWsMessage: (msg: string) => void
|
||||
}): void {
|
||||
|
||||
const {
|
||||
userID,
|
||||
roomId,
|
||||
canStartGame,
|
||||
startGamePending,
|
||||
logWsSend,
|
||||
pushWsMessage
|
||||
} = params
|
||||
|
||||
if (!canStartGame || startGamePending.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!userID) {
|
||||
return
|
||||
}
|
||||
|
||||
startGamePending.value = true
|
||||
|
||||
const message = {
|
||||
type: 'start_game',
|
||||
sender: userID,
|
||||
target: 'room',
|
||||
roomId,
|
||||
seq: Date.now(),
|
||||
|
||||
payload: {},
|
||||
}
|
||||
|
||||
logWsSend(message)
|
||||
wsClient.send(message)
|
||||
|
||||
pushWsMessage(`[client] 请求开始游戏`)
|
||||
// 进入操作窗口(碰/杠/胡)
|
||||
| {
|
||||
type: 'PENDING_CLAIM'
|
||||
payload: PendingClaimState
|
||||
}
|
||||
|
||||
|
||||
export function sendLeaveRoom(params: {
|
||||
userID: string
|
||||
roomId: string
|
||||
wsError: { value: string }
|
||||
leaveRoomPending: { value: boolean }
|
||||
logWsSend: (msg: any) => void
|
||||
pushWsMessage: (msg: string) => void
|
||||
}): boolean {
|
||||
|
||||
const {
|
||||
userID,
|
||||
roomId,
|
||||
wsError,
|
||||
leaveRoomPending,
|
||||
logWsSend,
|
||||
pushWsMessage
|
||||
} = params
|
||||
|
||||
if (!userID) {
|
||||
wsError.value = '缺少当前用户 ID,无法退出房间'
|
||||
return false
|
||||
// 操作结束(碰/杠/胡/过)
|
||||
| {
|
||||
type: 'CLAIM_RESOLVED'
|
||||
payload: {
|
||||
playerId: string
|
||||
action: 'peng' | 'gang' | 'hu' | 'pass'
|
||||
}
|
||||
|
||||
if (!roomId) {
|
||||
wsError.value = '缺少房间 ID,无法退出房间'
|
||||
return false
|
||||
}
|
||||
|
||||
leaveRoomPending.value = true
|
||||
|
||||
const message = {
|
||||
type: 'leave_room',
|
||||
sender: userID,
|
||||
target: 'room',
|
||||
roomId: roomId,
|
||||
seq: Date.now(),
|
||||
payload: {},
|
||||
}
|
||||
|
||||
logWsSend(message)
|
||||
|
||||
wsClient.send(message)
|
||||
|
||||
pushWsMessage(`[client] 请求退出房间`)
|
||||
|
||||
return true
|
||||
}
|
||||
33
src/game/dispatcher.ts
Normal file
33
src/game/dispatcher.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type {GameAction} from './actions'
|
||||
import {useGameStore} from "../store/gameStore.ts";
|
||||
|
||||
export function dispatchGameAction(action: GameAction) {
|
||||
const store = useGameStore()
|
||||
|
||||
switch (action.type) {
|
||||
case 'GAME_INIT':
|
||||
store.initGame(action.payload)
|
||||
break
|
||||
|
||||
case 'GAME_START':
|
||||
store.phase = 'playing'
|
||||
store.dealerIndex = action.payload.dealerIndex
|
||||
break
|
||||
|
||||
case 'DRAW_TILE':
|
||||
store.onDrawTile(action.payload)
|
||||
break
|
||||
|
||||
case 'PLAY_TILE':
|
||||
store.onPlayTile(action.payload)
|
||||
break
|
||||
|
||||
case 'PENDING_CLAIM':
|
||||
store.onPendingClaim(action.payload)
|
||||
break
|
||||
|
||||
case 'CLAIM_RESOLVED':
|
||||
store.clearPendingClaim()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// 游戏对象
|
||||
import type {GameState} from "../../types/state";
|
||||
|
||||
export interface Game {
|
||||
rule: Rule
|
||||
state: GameState
|
||||
}
|
||||
|
||||
// 规则配置(仅描述,不包含判定逻辑)
|
||||
export interface Rule {
|
||||
name: string // 玩法名称
|
||||
isBloodFlow: boolean // 是否血流玩法
|
||||
hasHongZhong: boolean // 是否包含红中
|
||||
}
|
||||
16
src/main.ts
16
src/main.ts
@@ -1,6 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import './assets/styles/style.css'
|
||||
import {createApp} from 'vue'
|
||||
import {createPinia} from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
import './assets/styles/style.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
|
||||
app.mount('#app')
|
||||
105
src/store/gameStore.ts
Normal file
105
src/store/gameStore.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import {
|
||||
GAME_PHASE,
|
||||
type GameState,
|
||||
type PendingClaimState,
|
||||
} from '../types/state'
|
||||
|
||||
import type { Tile } from '../types/tile'
|
||||
|
||||
export const useGameStore = defineStore('game', {
|
||||
state: (): GameState => ({
|
||||
roomId: '',
|
||||
|
||||
phase: GAME_PHASE.WAITING,
|
||||
|
||||
dealerIndex: 0,
|
||||
currentTurn: 0,
|
||||
|
||||
players: {},
|
||||
|
||||
remainingTiles: 0,
|
||||
|
||||
pendingClaim: undefined,
|
||||
|
||||
winners: [],
|
||||
|
||||
scores: {},
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// 初始化
|
||||
initGame(data: GameState) {
|
||||
Object.assign(this, data)
|
||||
},
|
||||
|
||||
// 摸牌
|
||||
onDrawTile(data: { playerId: string; tile: Tile }) {
|
||||
const player = this.players[data.playerId]
|
||||
if (!player) return
|
||||
|
||||
// 只更新自己的手牌
|
||||
if (player.playerId === this.getMyPlayerId()) {
|
||||
player.handTiles.push(data.tile)
|
||||
}
|
||||
|
||||
// 剩余牌数减少
|
||||
this.remainingTiles = Math.max(0, this.remainingTiles - 1)
|
||||
|
||||
// 更新回合(seatIndex)
|
||||
this.currentTurn = player.seatIndex
|
||||
|
||||
// 清除操作窗口
|
||||
this.pendingClaim = undefined
|
||||
|
||||
// 进入出牌阶段
|
||||
this.phase = GAME_PHASE.PLAYING
|
||||
},
|
||||
|
||||
// 出牌
|
||||
onPlayTile(data: {
|
||||
playerId: string
|
||||
tile: Tile
|
||||
nextSeat: number
|
||||
}) {
|
||||
const player = this.players[data.playerId]
|
||||
if (!player) return
|
||||
|
||||
// 如果是自己,移除手牌
|
||||
if (player.playerId === this.getMyPlayerId()) {
|
||||
const index = player.handTiles.findIndex(
|
||||
(t) => t.id === data.tile.id
|
||||
)
|
||||
if (index !== -1) {
|
||||
player.handTiles.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 加入出牌区
|
||||
player.discardTiles.push(data.tile)
|
||||
|
||||
// 更新回合
|
||||
this.currentTurn = data.nextSeat
|
||||
|
||||
// 等待其他玩家响应
|
||||
this.phase = GAME_PHASE.ACTION
|
||||
},
|
||||
|
||||
// 触发操作窗口(碰/杠/胡)
|
||||
onPendingClaim(data: PendingClaimState) {
|
||||
this.pendingClaim = data
|
||||
this.phase = GAME_PHASE.ACTION
|
||||
},
|
||||
|
||||
// 清理操作窗口
|
||||
clearPendingClaim() {
|
||||
this.pendingClaim = undefined
|
||||
this.phase = GAME_PHASE.PLAYING
|
||||
},
|
||||
|
||||
// 获取当前玩家ID(后续建议放到 userStore)
|
||||
getMyPlayerId(): string {
|
||||
return Object.keys(this.players)[0] || ''
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -5,5 +5,5 @@ export const CLAIM_OPTIONS = {
|
||||
PASS: 'pass',
|
||||
} as const
|
||||
|
||||
export type ClaimOption =
|
||||
export type ClaimOptionState =
|
||||
typeof CLAIM_OPTIONS[keyof typeof CLAIM_OPTIONS]
|
||||
@@ -3,17 +3,19 @@ export const GAME_PHASE = {
|
||||
WAITING: 'waiting', // 等待玩家准备 / 开始
|
||||
DEALING: 'dealing', // 发牌阶段
|
||||
PLAYING: 'playing', // 对局进行中
|
||||
ACTION: 'action',
|
||||
SETTLEMENT: 'settlement', // 结算阶段
|
||||
} as const
|
||||
|
||||
// 游戏阶段类型(取自 GAME_PHASE 的值)
|
||||
export type GamePhase =
|
||||
export type GamePhaseState =
|
||||
typeof GAME_PHASE[keyof typeof GAME_PHASE]
|
||||
|
||||
|
||||
export const GAME_PHASE_LABEL: Record<GamePhase, string> = {
|
||||
export const GAME_PHASE_LABEL: Record<GamePhaseState, string> = {
|
||||
waiting: '等待中',
|
||||
dealing: '发牌',
|
||||
playing: '对局中',
|
||||
action: '操作中',
|
||||
settlement: '结算',
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {Player} from "./player.ts";
|
||||
import type {PendingClaim} from "./pendingClaim.ts";
|
||||
import type {GamePhase} from "./gamePhase.ts";
|
||||
import type {PlayerState} from "./playerState.ts";
|
||||
import type {PendingClaimState} from "./pendingClaimState.ts";
|
||||
import type {GamePhaseState} from "./gamePhaseState.ts";
|
||||
|
||||
export interface GameState {
|
||||
// 房间ID
|
||||
roomId: string
|
||||
// 当前阶段
|
||||
phase: GamePhase
|
||||
phase: GamePhaseState
|
||||
|
||||
// 庄家位置
|
||||
dealerIndex: number
|
||||
@@ -13,13 +15,13 @@ export interface GameState {
|
||||
currentTurn: number
|
||||
|
||||
// 玩家列表
|
||||
players: Player[]
|
||||
players: Record<string, PlayerState>
|
||||
|
||||
// 剩余数量
|
||||
remainingTiles: number
|
||||
|
||||
// 操作响应窗口(碰/杠/胡)
|
||||
pendingClaim?: PendingClaim
|
||||
pendingClaim?: PendingClaimState
|
||||
|
||||
// 胡牌玩家
|
||||
winners: string[]
|
||||
@@ -8,5 +8,5 @@ export const HU_WAY = {
|
||||
} as const
|
||||
|
||||
// 胡牌类型(从常量中推导)
|
||||
export type HuWay =
|
||||
export type HuWayState =
|
||||
typeof HU_WAY[keyof typeof HU_WAY]
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './gamestate'
|
||||
export * from './player'
|
||||
export * from './meld'
|
||||
export * from './pendingClaim'
|
||||
export * from './claimOption'
|
||||
export * from './gamePhase'
|
||||
export * from './huWay'
|
||||
export * from './gameState.ts'
|
||||
export * from './playerState.ts'
|
||||
export * from './meldState.ts'
|
||||
export * from './pendingClaimState.ts'
|
||||
export * from './claimOptionState.ts'
|
||||
export * from './gamePhaseState.ts'
|
||||
export * from './huWayState.ts'
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Tile } from '../tile'
|
||||
|
||||
export type Meld =
|
||||
export type MeldState =
|
||||
| {
|
||||
type: 'peng'
|
||||
tiles: Tile[]
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import type {ClaimOption} from "./claimOption.ts";
|
||||
import type {ClaimOptionState} from "./claimOptionState.ts";
|
||||
import type {Tile} from "../tile.ts";
|
||||
|
||||
export interface PendingClaim {
|
||||
export interface PendingClaimState {
|
||||
// 当前被响应的牌
|
||||
tile: Tile
|
||||
|
||||
@@ -10,5 +10,5 @@ export interface PendingClaim {
|
||||
fromPlayerId: string
|
||||
|
||||
// 当前玩家可执行操作
|
||||
options: ClaimOption[]
|
||||
options: ClaimOptionState[]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Tile } from '../tile'
|
||||
import type { Meld } from './meld'
|
||||
import type { MeldState } from './meldState.ts'
|
||||
|
||||
export interface Player{
|
||||
export interface PlayerState {
|
||||
playerId: string
|
||||
seatIndex: number
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface Player{
|
||||
handTiles: Tile[]
|
||||
|
||||
// 副露(碰/杠)
|
||||
melds: Meld[]
|
||||
melds: MeldState[]
|
||||
|
||||
// 出牌区
|
||||
discardTiles: Tile[]
|
||||
@@ -2,22 +2,24 @@ import {wsClient} from './client'
|
||||
|
||||
type Handler = (msg: any) => void
|
||||
|
||||
const handlerMap: Record<string, Handler> = {}
|
||||
const handlerMap: Record<string, Handler[]> = {}
|
||||
|
||||
|
||||
// 注册 handler
|
||||
export function registerHandler(type: string, handler: Handler) {
|
||||
handlerMap[type] = handler
|
||||
if (!handlerMap[type]) {
|
||||
handlerMap[type] = []
|
||||
}
|
||||
handlerMap[type].push(handler)
|
||||
}
|
||||
|
||||
|
||||
// 初始化监听
|
||||
export function initWsHandler() {
|
||||
wsClient.onMessage((msg) => {
|
||||
const handler = handlerMap[msg.type]
|
||||
const handlers = handlerMap[msg.type]
|
||||
|
||||
if (handler) {
|
||||
handler(msg)
|
||||
if (handlers && handlers.length > 0) {
|
||||
handlers.forEach(h => h(msg))
|
||||
} else {
|
||||
console.warn('[WS] 未处理消息:', msg.type, msg)
|
||||
}
|
||||
|
||||
6
src/ws/sender.ts
Normal file
6
src/ws/sender.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { wsClient } from './client'
|
||||
|
||||
export function sendWsMessage(message: any) {
|
||||
console.log('[WS SEND]', message)
|
||||
wsClient.send(message)
|
||||
}
|
||||
Reference in New Issue
Block a user