| 1 | import type { Claim, ClaimOption, GameState, SoundCue } from './engine'; |
| 2 | import type { Tile } from './tiles'; |
| 3 | import type { Rules, SeatId } from './types'; |
| 4 | |
| 5 | /** |
| 6 | * How hard a tile left a phone, in the thrower's own frame: `u` is where along |
| 7 | * their edge it was let go of (0 = their left, 1 = their right), `vx` is speed |
| 8 | * along that edge, `vy` speed *into* the table, both px/s. The common screen |
| 9 | * turns this into a throw from that player's side of the table. |
| 10 | */ |
| 11 | export interface NetThrow { |
| 12 | u: number; |
| 13 | vx: number; |
| 14 | vy: number; |
| 15 | spin: number; |
| 16 | } |
| 17 | |
| 18 | /** |
| 19 | * What the table's UI needs from whatever is running the game. |
| 20 | * |
| 21 | * `Game` satisfies this directly — the hotseat and solo tables talk straight to |
| 22 | * the engine, exactly as before. The online tables put `net/table.ts` here |
| 23 | * instead: same surface, but a call either drives the authoritative engine (on |
| 24 | * the host) or is sent to whoever has it. The UI cannot tell the difference, |
| 25 | * which is the point — every screen is drawn from a `GameState` and every |
| 26 | * button is one of these calls, wherever the state actually lives. |
| 27 | */ |
| 28 | export interface TableCtl { |
| 29 | readonly state: GameState; |
| 30 | subscribe(fn: () => void): () => void; |
| 31 | getSnapshot(): number; |
| 32 | |
| 33 | discard(seat: SeatId, tile: Tile, thrown?: NetThrow): void; |
| 34 | respond(seat: SeatId, claim: Claim | 'pass'): void; |
| 35 | resolveNow(): void; |
| 36 | declareConcealedKong(seat: SeatId, tile: Tile): void; |
| 37 | declareAddedKong(seat: SeatId, tile: Tile): void; |
| 38 | declareSelfDraw(seat: SeatId): void; |
| 39 | reorderHand(seat: SeatId, from: number, to: number): void; |
| 40 | sortHand(seat: SeatId): void; |
| 41 | nextHand(): void; |
| 42 | newGame(bots?: boolean[], names?: string[]): void; |
| 43 | applySettings(names: string[], rules: Rules): void; |
| 44 | |
| 45 | undoable(): { zh: string; en: string } | null; |
| 46 | undo(): void; |
| 47 | |
| 48 | /** |
| 49 | * Throw the dice again, moving where the wall is broken and nothing else. |
| 50 | * Optional because only the engine has it: an online table's wall is the |
| 51 | * host's business. From the console; see `exposeGame` in ui/colliders. |
| 52 | */ |
| 53 | reroll?(...dice: number[]): number[]; |
| 54 | |
| 55 | canSelfDrawWin(seat: SeatId): boolean; |
| 56 | concealedKongs(seat: SeatId): Tile[]; |
| 57 | addedKongs(seat: SeatId): Tile[]; |
| 58 | tenpai(seat: SeatId): Tile[]; |
| 59 | sacredTiles(seat: SeatId): Tile[]; |
| 60 | nextDrawer(): SeatId | null; |
| 61 | |
| 62 | readonly hasBots: boolean; |
| 63 | isBot(seat: SeatId): boolean; |
| 64 | readonly wallLeft: number; |
| 65 | readonly roundWind: number; |
| 66 | |
| 67 | onSound(fn: (cue: SoundCue) => void): () => void; |
| 68 | } |
| 69 | |
| 70 | /** Claim options a seat is still owed an answer on, or undefined. */ |
| 71 | export const pendingOptions = ( |
| 72 | state: GameState, |
| 73 | seat: SeatId, |
| 74 | ): ClaimOption[] | undefined => |
| 75 | (state.phase === 'claims' || state.phase === 'robkong') && |
| 76 | state.responses[seat] === undefined |
| 77 | ? state.options[seat] |
| 78 | : undefined; |