refactor(game): 重构游戏状态管理和WebSocket通信
- 定义统一的游戏动作类型GameAction替代原有发送函数 - 创建游戏状态管理store使用Pinia进行状态管理 - 实现游戏状态分发器处理各种游戏事件 - 重构WebSocket处理器支持多处理器注册 - 重命名状态类型文件统一使用State后缀 - 添加ACTION游戏阶段处理操作窗口逻辑 - 集成Pinia依赖管理应用状态
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "4"
|
||||
},
|
||||
|
||||
99
pnpm-lock.yaml
generated
99
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3))
|
||||
vue:
|
||||
specifier: ^3.5.25
|
||||
version: 3.5.28(typescript@5.9.3)
|
||||
@@ -393,6 +396,15 @@ packages:
|
||||
'@vue/devtools-api@6.6.4':
|
||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||
|
||||
'@vue/devtools-api@7.7.9':
|
||||
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
|
||||
|
||||
'@vue/devtools-kit@7.7.9':
|
||||
resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==}
|
||||
|
||||
'@vue/devtools-shared@7.7.9':
|
||||
resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==}
|
||||
|
||||
'@vue/language-core@3.2.4':
|
||||
resolution: {integrity: sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==}
|
||||
|
||||
@@ -427,6 +439,13 @@ packages:
|
||||
alien-signals@3.1.2:
|
||||
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
|
||||
|
||||
birpc@2.9.0:
|
||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||
|
||||
copy-anything@4.0.5:
|
||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@@ -461,9 +480,19 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
is-what@5.5.0:
|
||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
@@ -475,6 +504,9 @@ packages:
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -482,6 +514,15 @@ packages:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pinia@3.0.4:
|
||||
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.5.0'
|
||||
vue: ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
playwright-core@1.58.2:
|
||||
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -496,6 +537,9 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rollup@4.57.1:
|
||||
resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -505,6 +549,14 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
superjson@2.2.6:
|
||||
resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -807,6 +859,24 @@ snapshots:
|
||||
|
||||
'@vue/devtools-api@6.6.4': {}
|
||||
|
||||
'@vue/devtools-api@7.7.9':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
|
||||
'@vue/devtools-kit@7.7.9':
|
||||
dependencies:
|
||||
'@vue/devtools-shared': 7.7.9
|
||||
birpc: 2.9.0
|
||||
hookable: 5.5.3
|
||||
mitt: 3.0.1
|
||||
perfect-debounce: 1.0.0
|
||||
speakingurl: 14.0.1
|
||||
superjson: 2.2.6
|
||||
|
||||
'@vue/devtools-shared@7.7.9':
|
||||
dependencies:
|
||||
rfdc: 1.4.1
|
||||
|
||||
'@vue/language-core@3.2.4':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.27
|
||||
@@ -848,6 +918,12 @@ snapshots:
|
||||
|
||||
alien-signals@3.1.2: {}
|
||||
|
||||
birpc@2.9.0: {}
|
||||
|
||||
copy-anything@4.0.5:
|
||||
dependencies:
|
||||
is-what: 5.5.0
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
@@ -893,20 +969,35 @@ snapshots:
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
is-what@5.5.0: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
perfect-debounce@1.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
pinia@3.0.4(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.9
|
||||
vue: 3.5.28(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
playwright-core@1.58.2: {}
|
||||
|
||||
playwright@1.58.2:
|
||||
@@ -921,6 +1012,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rollup@4.57.1:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -954,6 +1047,12 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
superjson@2.2.6:
|
||||
dependencies:
|
||||
copy-anything: 4.0.5
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
|
||||
@@ -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