| 1 | import { randomUUID } from 'node:crypto'; |
| 2 | import type { GameState, GameView, GameEventEnvelope, ClientMessage, Card } from '../src/engine/types'; |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | export class BlindChoices { |
| 8 | private ids = new Map<number, Map<string, string>>(); |
| 9 | private token(viewer: number, id: string): string { |
| 10 | let map = this.ids.get(viewer); |
| 11 | if (!map) this.ids.set(viewer, map = new Map()); |
| 12 | if (!map.has(id)) map.set(id, `hidden-${randomUUID()}`); |
| 13 | return map.get(id)!; |
| 14 | } |
| 15 | private prompt(state: GameState, viewer: number, prompt: unknown): unknown { |
| 16 | if (!prompt || typeof prompt !== 'object') return prompt; |
| 17 | const p = prompt as { type?: string; cardIds?: string[] }; |
| 18 | if (p.type !== 'distribute' || !p.cardIds) return prompt; |
| 19 | const hidden = new Set(state.players.flatMap((x, i) => i !== viewer ? x.hand : [])); |
| 20 | return { ...p, cardIds: p.cardIds.map((id) => hidden.has(id) ? this.token(viewer, id) : id) }; |
| 21 | } |
| 22 | view(state: GameState, viewer: number, view: GameView): GameView { |
| 23 | if (!view.pending) return view; |
| 24 | const atom = view.pending.atom as unknown as { prompt?: unknown }; |
| 25 | const prompt = this.prompt(state, viewer, view.pending.prompt); |
| 26 | if (prompt === view.pending.prompt) return view; |
| 27 | const cardMap = { ...view.cardMap }; |
| 28 | for (const token of this.ids.get(viewer)?.values() ?? []) cardMap[token] = blindCard(token); |
| 29 | return { ...view, cardMap, pending: { ...view.pending, prompt, atom: { ...atom, prompt } } } as GameView; |
| 30 | } |
| 31 | events(state: GameState, viewer: number, events: GameEventEnvelope[]): GameEventEnvelope[] { |
| 32 | return events.map((e) => { |
| 33 | if (e.view?.type !== '请求回应' || !e.view.prompt) return e; |
| 34 | return { ...e, view: { ...e.view, prompt: this.prompt(state, viewer, e.view.prompt) } } as GameEventEnvelope; |
| 35 | }); |
| 36 | } |
| 37 | message(state: GameState, viewer: number, message: ClientMessage): ClientMessage | null { |
| 38 | const back = new Map([...this.ids.get(viewer)?.entries() ?? []].map(([a, b]) => [b, a])); |
| 39 | const hidden = new Set(state.players.flatMap((p, i) => i !== viewer ? p.hand : [])); |
| 40 | const atom = state.pendingSlots.get(viewer)?.atom as unknown as { prompt?: { type?: string; cardIds?: string[] } } | undefined; |
| 41 | |
| 42 | |
| 43 | const protectedIds = new Set(atom?.prompt?.type === 'distribute' ? (atom.prompt.cardIds ?? []).filter((id) => hidden.has(id)) : []); |
| 44 | let invalid = false; |
| 45 | const visit = (value: unknown): unknown => { |
| 46 | if (typeof value === 'string') { |
| 47 | if (back.has(value)) return back.get(value); |
| 48 | if (protectedIds.has(value)) invalid = true; |
| 49 | return value; |
| 50 | } |
| 51 | if (Array.isArray(value)) return value.map(visit); |
| 52 | if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, visit(v)])); |
| 53 | return value; |
| 54 | }; |
| 55 | const out = visit(message) as ClientMessage; |
| 56 | return invalid ? null : out; |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | export function blindCard(id: string): Card { |
| 61 | return { id, name: '暗牌', type: '基本牌', subtype: '暗牌', suit: '', rank: '', color: '无色', description: '对方的手牌;只能盲选,牌面不可见。' } as Card; |
| 62 | } |