Files
mahjong-web/src/game/chengdu/messageNormalizers.ts
wsy182 e96c45739e feat(game): 添加成都麻将游戏页面和大厅功能
- 实现 ChengduGamePage.vue 组件,包含完整的麻将游戏界面
- 实现 HallPage.vue 组件,支持房间列表展示、创建和加入功能
- 添加 mahjong API 接口用于房间管理操作
- 集成 store 状态管理和本地存储功能
- 实现 ChengduBottomActions 等游戏控制组件
- 添加 websocket 连接和游戏会话管理逻辑
- 实现游戏倒计时、结算等功能模块
2026-04-03 20:46:50 +08:00

284 lines
7.6 KiB
TypeScript

import type {
ClaimOptionState,
MeldState,
PendingClaimState,
PlayerState,
Tile,
} from '../../types/state'
export function normalizeWsType(type: string): string {
return type.replace(/[-\s]/g, '_').toUpperCase()
}
export function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
}
export function readString(source: Record<string, unknown>, ...keys: string[]): string {
for (const key of keys) {
const value = source[key]
if (typeof value === 'string' && value.trim()) {
return value
}
}
return ''
}
export function readNumber(source: Record<string, unknown>, ...keys: string[]): number | null {
for (const key of keys) {
const value = source[key]
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
}
return null
}
export function normalizeTimestampMs(value: number | null): number | null {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
return null
}
return value >= 1_000_000_000_000 ? value : value * 1000
}
export function readStringArray(source: Record<string, unknown>, ...keys: string[]): string[] {
for (const key of keys) {
const value = source[key]
if (Array.isArray(value)) {
return value.filter((item): item is string => typeof item === 'string')
}
}
return []
}
export function readBoolean(source: Record<string, unknown>, ...keys: string[]): boolean | null {
for (const key of keys) {
const value = source[key]
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'number') {
if (value === 1) {
return true
}
if (value === 0) {
return false
}
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
if (normalized === 'true' || normalized === '1') {
return true
}
if (normalized === 'false' || normalized === '0') {
return false
}
}
}
return null
}
export function readMissingSuit(source: Record<string, unknown> | null | undefined): string | null {
if (!source) {
return null
}
return readString(source, 'missing_suit', 'MissingSuit', 'ding_que', 'dingQue', 'suit', 'Suit') || null
}
export function readMissingSuitWithPresence(
source: Record<string, unknown> | null | undefined,
): { present: boolean; value: string | null } {
if (!source) {
return { present: false, value: null }
}
const keys = ['missing_suit', 'MissingSuit', 'ding_que', 'dingQue', 'suit', 'Suit']
const hasMissingSuitField = keys.some((key) => Object.prototype.hasOwnProperty.call(source, key))
if (!hasMissingSuitField) {
return { present: false, value: null }
}
return { present: true, value: readMissingSuit(source) }
}
export function tileToText(tile: Tile): string {
return `${tile.suit}${tile.value}`
}
export function readPlayerTurnPlayerId(payload: Record<string, unknown>): string {
return (
(typeof payload.player_id === 'string' && payload.player_id) ||
(typeof payload.playerId === 'string' && payload.playerId) ||
(typeof payload.PlayerID === 'string' && payload.PlayerID) ||
''
)
}
export function readPlayerTurnAllowActions(payload: Record<string, unknown>): string[] {
const source =
payload.allow_actions ??
payload.allowActions ??
payload.AllowActions ??
payload.available_actions ??
payload.availableActions ??
payload.AvailableActions
if (!Array.isArray(source)) {
return []
}
const actions = source
.filter((item): item is string => typeof item === 'string')
.map((item) => item.trim().toLowerCase())
.filter((item) => item.length > 0)
return Array.from(new Set(actions))
}
export function normalizeTile(tile: unknown): Tile | null {
const source = asRecord(tile)
if (!source) {
return null
}
const id = readNumber(source, 'id')
const suit = readString(source, 'suit') as Tile['suit'] | ''
const value = readNumber(source, 'value')
if (typeof id !== 'number' || !suit || typeof value !== 'number') {
return null
}
if (suit !== 'W' && suit !== 'T' && suit !== 'B') {
return null
}
return { id, suit, value }
}
export function normalizeTiles(value: unknown): Tile[] {
if (!Array.isArray(value)) {
return []
}
return value.map((item) => normalizeTile(item)).filter((item): item is Tile => Boolean(item))
}
export function normalizePendingClaim(
gameState: Record<string, unknown> | null | undefined,
loggedInUserId: string,
): PendingClaimState | undefined {
if (!gameState || !loggedInUserId) {
return undefined
}
const pendingClaim = asRecord(gameState.pending_claim ?? gameState.pendingClaim)
if (!pendingClaim) {
return undefined
}
const selfOptions = asRecord(pendingClaim[loggedInUserId])
if (!selfOptions) {
return undefined
}
const options: ClaimOptionState[] = []
if (readBoolean(selfOptions, 'hu')) {
options.push('hu')
}
if (readBoolean(selfOptions, 'gang')) {
options.push('gang')
}
if (readBoolean(selfOptions, 'peng')) {
options.push('peng')
}
if (options.length === 0) {
return undefined
}
options.push('pass')
return {
tile: normalizeTile(gameState.last_discard_tile ?? gameState.lastDiscardTile) ?? undefined,
fromPlayerId: readString(gameState, 'last_discard_by', 'lastDiscardBy') || undefined,
options,
}
}
export function normalizeMeldType(value: unknown, concealed = false): MeldState['type'] | null {
if (typeof value !== 'string') {
return concealed ? 'an_gang' : null
}
const normalized = value.replace(/[-\s]/g, '_').toLowerCase()
if (normalized === 'peng') {
return 'peng'
}
if (normalized === 'ming_gang' || normalized === 'gang' || normalized === 'gang_open') {
return concealed ? 'an_gang' : 'ming_gang'
}
if (normalized === 'an_gang' || normalized === 'angang' || normalized === 'concealed_gang') {
return 'an_gang'
}
return concealed ? 'an_gang' : null
}
export function normalizeMelds(value: unknown): PlayerState['melds'] {
if (!Array.isArray(value)) {
return []
}
return value
.map((item) => {
if (Array.isArray(item)) {
const tiles = normalizeTiles(item)
if (tiles.length === 3) {
return { type: 'peng', tiles, fromPlayerId: '' } satisfies MeldState
}
if (tiles.length === 4) {
return { type: 'ming_gang', tiles, fromPlayerId: '' } satisfies MeldState
}
return null
}
const source = asRecord(item)
if (!source) {
return null
}
const tiles = normalizeTiles(
source.tiles ??
source.meld_tiles ??
source.meldTiles ??
source.cards ??
source.card_list,
)
if (tiles.length === 0) {
return null
}
const concealed =
readBoolean(source, 'concealed', 'is_concealed', 'isConcealed', 'hidden', 'is_hidden') ?? false
const explicitType = normalizeMeldType(
source.type ?? source.meld_type ?? source.meldType ?? source.kind,
concealed,
)
const type =
explicitType ??
(tiles.length === 4 ? (concealed ? 'an_gang' : 'ming_gang') : tiles.length === 3 ? 'peng' : null)
if (type === 'peng' || type === 'ming_gang') {
return {
type,
tiles,
fromPlayerId: readString(source, 'from_player_id', 'fromPlayerId'),
} satisfies MeldState
}
if (type === 'an_gang') {
return { type, tiles } satisfies MeldState
}
return null
})
.filter((item): item is MeldState => Boolean(item))
}