| 1 | import type { Claim, GameState } from '../game/engine'; |
| 2 | import type { NetThrow } from '../game/ctl'; |
| 3 | import type { Tile } from '../game/tiles'; |
| 4 | import type { SeatId } from '../game/types'; |
| 5 | |
| 6 | /** |
| 7 | * What goes over the wire, in one place. |
| 8 | * |
| 9 | * The room is host-authoritative: exactly one client — whichever one antics |
| 10 | * says is the host — runs the real engine, and everything it decides is |
| 11 | * published as plain `GameState` JSON under one key of the shared room state. |
| 12 | * Everyone else renders that and asks for things by sending intents, which the |
| 13 | * host validates against the seat map before playing them through the same |
| 14 | * engine methods a button would have called. The engine itself never knows any |
| 15 | * of this is happening — the same trick `autoplay.ts` plays, stretched over a |
| 16 | * network. |
| 17 | */ |
| 18 | |
| 19 | /** The two ways a room can be playing. */ |
| 20 | export type NetMode = 'online' | 'party'; |
| 21 | |
| 22 | /** |
| 23 | * Who holds a seat. A list, not a single id: a seat is a hand, and nothing |
| 24 | * stops two people huddling over one hand — a pair playing together from two |
| 25 | * phones simply both hold it, and whoever acts first acts. The name outlives |
| 26 | * every connection — see the antics docs on leavers: record it where it |
| 27 | * arrives, the match outlives the connection. |
| 28 | */ |
| 29 | export interface SeatSlot { |
| 30 | /** antics player ids of everyone holding this hand. */ |
| 31 | ids: string[]; |
| 32 | name: string; |
| 33 | } |
| 34 | |
| 35 | /** The shared room state, as the host writes it. */ |
| 36 | export interface SharedKeys { |
| 37 | mode: NetMode; |
| 38 | seats: SeatSlot[]; |
| 39 | /** The whole engine state. null while the room is still in its lobby. */ |
| 40 | game: GameState | null; |
| 41 | } |
| 42 | |
| 43 | /** Something a player asks the host to play for their seat. */ |
| 44 | export type Act = |
| 45 | | { kind: 'discard'; tile: Tile; thrown?: NetThrow } |
| 46 | | { kind: 'respond'; claim: Claim | 'pass' } |
| 47 | | { kind: 'resolveNow' } |
| 48 | | { kind: 'ankong'; tile: Tile } |
| 49 | | { kind: 'addkong'; tile: Tile } |
| 50 | | { kind: 'selfDraw' } |
| 51 | | { kind: 'nextHand' } |
| 52 | | { kind: 'newGame' }; |
| 53 | |
| 54 | export interface ActMsg { |
| 55 | seat: SeatId; |
| 56 | act: Act; |
| 57 | } |
| 58 | |
| 59 | /** Ask the host for a seat. null means "any free one". */ |
| 60 | export interface SeatMsg { |
| 61 | seat: SeatId | null; |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Host to everyone else: things worth seeing or hearing that are not state — a |
| 66 | * sound cue, or the throw a discard arrived with, so every table can show the |
| 67 | * tile flying in from that player's edge. |
| 68 | */ |
| 69 | export type FxMsg = |
| 70 | | { fx: 'sfx'; kind: string; tile?: Tile } |
| 71 | | { fx: 'throw'; seat: SeatId; t: NetThrow }; |
| 72 | |
| 73 | export const EV_ACT = 'act'; |
| 74 | export const EV_SEAT = 'seat'; |
| 75 | export const EV_FX = 'fx'; |
| 76 | |
| 77 | export const EMPTY_SEATS: SeatSlot[] = [ |
| 78 | { ids: [], name: '' }, |
| 79 | { ids: [], name: '' }, |
| 80 | { ids: [], name: '' }, |
| 81 | { ids: [], name: '' }, |
| 82 | ]; |
| 83 | |
| 84 | /** The seat in the QR's link, if the hosting page passed it through to us. */ |
| 85 | export function seatFromUrl(): SeatId | null { |
| 86 | try { |
| 87 | const raw = new URLSearchParams(window.location.search).get('seat'); |
| 88 | if (raw === null) return null; |
| 89 | const n = Number(raw); |
| 90 | return n === 0 || n === 1 || n === 2 || n === 3 ? (n as SeatId) : null; |
| 91 | } catch { |
| 92 | return null; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | /** Whether the page arrived with a room to join — i.e. we are on antics. */ |
| 97 | export function roomFromUrl(): string | null { |
| 98 | try { |
| 99 | return new URLSearchParams(window.location.search).get('room'); |
| 100 | } catch { |
| 101 | return null; |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * The link the table's QR carries: this very page, with just the room in the |
| 107 | * query. No seat — a phone that scans it lands on the seat picker, which is |
| 108 | * also what lets a pair deliberately share a hand. (An old link with a `seat` |
| 109 | * param still works: seatFromUrl above keeps reading it.) The page's own URL |
| 110 | * (the raw bundle on antics, the dev server at home) serves the game directly |
| 111 | * and keeps every param, which on a phone also means the whole screen belongs |
| 112 | * to the hand. |
| 113 | */ |
| 114 | export function roomLink(code: string): string { |
| 115 | const u = new URL(window.location.href); |
| 116 | u.search = `?room=${code}`; |
| 117 | u.hash = ''; |
| 118 | return u.toString(); |
| 119 | } |