diff --git a/package.json b/package.json index 8efb35a..5f79224 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test:e2e": "playwright test" }, "dependencies": { + "pinia": "^3.0.4", "vue": "^3.5.25", "vue-router": "4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 116355b..c98fc8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/src/game/actions.ts b/src/game/actions.ts index 351642c..21d7728 100644 --- a/src/game/actions.ts +++ b/src/game/actions.ts @@ -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 = {} +} // 出牌 - 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 } \ No newline at end of file diff --git a/src/game/dispatcher.ts b/src/game/dispatcher.ts new file mode 100644 index 0000000..dd32025 --- /dev/null +++ b/src/game/dispatcher.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/game/types/game.ts b/src/game/types/game.ts deleted file mode 100644 index 2191ef0..0000000 --- a/src/game/types/game.ts +++ /dev/null @@ -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 // 是否包含红中 -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 28e7fc8..693648c 100644 --- a/src/main.ts +++ b/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') \ No newline at end of file diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts new file mode 100644 index 0000000..986aa93 --- /dev/null +++ b/src/store/gameStore.ts @@ -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] || '' + }, + }, +}) \ No newline at end of file diff --git a/src/types/state/claimOption.ts b/src/types/state/claimOptionState.ts similarity index 57% rename from src/types/state/claimOption.ts rename to src/types/state/claimOptionState.ts index 871f4de..02613ba 100644 --- a/src/types/state/claimOption.ts +++ b/src/types/state/claimOptionState.ts @@ -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] \ No newline at end of file diff --git a/src/types/state/gamePhase.ts b/src/types/state/gamePhaseState.ts similarity index 78% rename from src/types/state/gamePhase.ts rename to src/types/state/gamePhaseState.ts index 1ae36cf..9b89076 100644 --- a/src/types/state/gamePhase.ts +++ b/src/types/state/gamePhaseState.ts @@ -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 = { +export const GAME_PHASE_LABEL: Record = { waiting: '等待中', dealing: '发牌', playing: '对局中', + action: '操作中', settlement: '结算', } \ No newline at end of file diff --git a/src/types/state/gamestate.ts b/src/types/state/gameState.ts similarity index 55% rename from src/types/state/gamestate.ts rename to src/types/state/gameState.ts index 1e19a9a..676266a 100644 --- a/src/types/state/gamestate.ts +++ b/src/types/state/gameState.ts @@ -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 // 剩余数量 remainingTiles: number // 操作响应窗口(碰/杠/胡) - pendingClaim?: PendingClaim + pendingClaim?: PendingClaimState // 胡牌玩家 winners: string[] diff --git a/src/types/state/huWay.ts b/src/types/state/huWayState.ts similarity index 85% rename from src/types/state/huWay.ts rename to src/types/state/huWayState.ts index e9a720d..6160c63 100644 --- a/src/types/state/huWay.ts +++ b/src/types/state/huWayState.ts @@ -8,5 +8,5 @@ export const HU_WAY = { } as const // 胡牌类型(从常量中推导) -export type HuWay = +export type HuWayState = typeof HU_WAY[keyof typeof HU_WAY] \ No newline at end of file diff --git a/src/types/state/index.ts b/src/types/state/index.ts index 096ee3c..5e53694 100644 --- a/src/types/state/index.ts +++ b/src/types/state/index.ts @@ -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' \ No newline at end of file +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' \ No newline at end of file diff --git a/src/types/state/meld.ts b/src/types/state/meldState.ts similarity index 90% rename from src/types/state/meld.ts rename to src/types/state/meldState.ts index 70d7a78..47a94ad 100644 --- a/src/types/state/meld.ts +++ b/src/types/state/meldState.ts @@ -1,6 +1,6 @@ import type { Tile } from '../tile' -export type Meld = +export type MeldState = | { type: 'peng' tiles: Tile[] diff --git a/src/types/state/pendingClaim.ts b/src/types/state/pendingClaimState.ts similarity index 55% rename from src/types/state/pendingClaim.ts rename to src/types/state/pendingClaimState.ts index d1a660d..be57b33 100644 --- a/src/types/state/pendingClaim.ts +++ b/src/types/state/pendingClaimState.ts @@ -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[] } \ No newline at end of file diff --git a/src/types/state/player.ts b/src/types/state/playerState.ts similarity index 75% rename from src/types/state/player.ts rename to src/types/state/playerState.ts index a129a99..a596e02 100644 --- a/src/types/state/player.ts +++ b/src/types/state/playerState.ts @@ -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[] diff --git a/src/ws/handler.ts b/src/ws/handler.ts index 91979ca..a92cf64 100644 --- a/src/ws/handler.ts +++ b/src/ws/handler.ts @@ -2,22 +2,24 @@ import {wsClient} from './client' type Handler = (msg: any) => void -const handlerMap: Record = {} +const handlerMap: Record = {} // 注册 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) } diff --git a/src/ws/sender.ts b/src/ws/sender.ts new file mode 100644 index 0000000..80f995b --- /dev/null +++ b/src/ws/sender.ts @@ -0,0 +1,6 @@ +import { wsClient } from './client' + +export function sendWsMessage(message: any) { + console.log('[WS SEND]', message) + wsClient.send(message) +} \ No newline at end of file