import { asRecord, normalizeMelds, normalizePendingClaim, normalizeTiles, readBoolean, readMissingSuitWithPresence, readNumber, readString, readStringArray, } from '../../../../game/chengdu/messageNormalizers' import type { PendingClaimState, PlayerState } from '../../../../types/state' export interface ParsedRoomStateSnapshot { roomId: string nextPlayers: Record phase: string wallCount: number | null currentTurnPlayerId: string currentTurn: number | null needDraw: boolean pendingClaim?: PendingClaimState scores?: Record winners: string[] currentRound: number | null totalRounds: number | null settlementDeadlineMs: number | null } interface ParseRoomStateSnapshotOptions { payload: Record roomId: string loggedInUserId: string previousPlayers: Record } export function parseRoomStateSnapshot( options: ParseRoomStateSnapshotOptions, ): ParsedRoomStateSnapshot { const { payload, previousPlayers } = options const nextPlayers: Record = {} const gamePlayers = Array.isArray(payload.players) ? payload.players : [] gamePlayers.forEach((item, fallbackIndex) => { const player = asRecord(item) if (!player) { return } const playerId = readString(player, 'player_id', 'PlayerID') if (!playerId) { return } const previous = previousPlayers[playerId] const seatIndex = previous?.seatIndex ?? fallbackIndex const handCount = readNumber(player, 'hand_count', 'handCount') ?? previous?.handCount ?? 0 const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles) const melds = normalizeMelds(player.melds ?? player.exposed_melds ?? player.exposedMelds ?? player.claims) const hasHu = Boolean(player.has_hu ?? player.hasHu) const dingQue = readMissingSuitWithPresence(player) const scores = asRecord(payload.scores) const score = scores?.[playerId] nextPlayers[playerId] = { playerId, seatIndex, displayName: previous?.displayName ?? playerId, avatarURL: previous?.avatarURL, isTrustee: previous?.isTrustee ?? false, missingSuit: dingQue.present ? dingQue.value : (previous?.missingSuit ?? null), handTiles: previous?.handTiles ?? [], handCount, melds, discardTiles: outTiles, hasHu, score: typeof score === 'number' ? score : previous?.score ?? 0, isReady: previous?.isReady ?? false, } }) const phase = readString(payload, 'phase') || readString(payload, 'status') || 'waiting' const wallCount = readNumber(payload, 'wall_count', 'wallCount') const currentTurnSeat = readNumber(payload, 'current_turn', 'currentTurn') const currentTurnPlayerId = readString(payload, 'current_turn_player', 'currentTurnPlayer') || '' const currentTurn = currentTurnSeat ?? (currentTurnPlayerId && nextPlayers[currentTurnPlayerId] ? nextPlayers[currentTurnPlayerId].seatIndex : null) return { roomId: options.roomId, nextPlayers, phase, wallCount, currentTurnPlayerId, currentTurn, needDraw: readBoolean(payload, 'need_draw', 'needDraw') ?? false, pendingClaim: normalizePendingClaim(payload, options.loggedInUserId), scores: asRecord(payload.scores) ? (Object.fromEntries( Object.entries(asRecord(payload.scores) ?? {}).filter(([, value]) => typeof value === 'number'), ) as Record) : undefined, winners: readStringArray(payload, 'winners'), currentRound: readNumber(payload, 'current_round', 'currentRound'), totalRounds: readNumber(payload, 'total_rounds', 'totalRounds'), settlementDeadlineMs: readNumber(payload, 'settlement_deadline_ms', 'settlementDeadlineMs'), } }