| 1 | import { STACKS_PER_SIDE, WALL_SIZE, WALL_STACKS } from './tiles'; |
| 2 | import type { SeatId } from './types'; |
| 3 | |
| 4 | /** |
| 5 | * What the wall square looks like right now. |
| 6 | * |
| 7 | * The square is eaten from both ends at once — normal draws come off the front, |
| 8 | * kong and flower replacements off the back — so how much of it is left is not |
| 9 | * one number but a per-stack question, and two things want the answer: the |
| 10 | * drawing of it (ui/WallRing) and the physics (table/geometry), since a stack |
| 11 | * still standing is something a thrown tile has to get past. |
| 12 | * |
| 13 | * Deliberately typed against the fields it reads rather than `GameState`, so it |
| 14 | * stays a function of the wall and nothing else. |
| 15 | */ |
| 16 | export interface WallProgress { |
| 17 | drawnFront: number; |
| 18 | drawnBack: number; |
| 19 | rules: { wallReserve: number }; |
| 20 | } |
| 21 | |
| 22 | export interface Stack { |
| 23 | /** Tiles still in this stack: 2 full, 1 half, 0 spent. */ |
| 24 | count: 0 | 1 | 2; |
| 25 | /** Part of the 16-tile 底牌 tail that ends the hand. */ |
| 26 | dead: boolean; |
| 27 | /** The break point — the stack the next draw comes off. */ |
| 28 | next: boolean; |
| 29 | } |
| 30 | |
| 31 | /** |
| 32 | * Position 0 is the break point. Stack `i` holds positions `2i` and `2i+1`, and |
| 33 | * a position is still there if it is past the front and short of the back. |
| 34 | */ |
| 35 | export function wallStacks(s: WallProgress): Stack[] { |
| 36 | const front = s.drawnFront; |
| 37 | const back = WALL_SIZE - s.drawnBack; |
| 38 | const deadFrom = back - s.rules.wallReserve; |
| 39 | |
| 40 | return Array.from({ length: WALL_STACKS }, (_, i) => { |
| 41 | const a = i * 2; |
| 42 | const b = a + 1; |
| 43 | const live = (p: number) => p >= front && p < back; |
| 44 | const count = ((live(a) ? 1 : 0) + (live(b) ? 1 : 0)) as 0 | 1 | 2; |
| 45 | return { count, dead: b >= deadFrom, next: front === a || front === b }; |
| 46 | }); |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Whether a stack is something a thrown tile has to get past. |
| 51 | * |
| 52 | * A stack of two stands as tall as the tile being thrown at it. One of one is |
| 53 | * low enough to sail over, and a spent one is not there at all — so only a full |
| 54 | * stack is a barrier. This is the rule behind both halves of the throw: the |
| 55 | * colliders the pile bounces off, and whether a seat may flick at all. |
| 56 | */ |
| 57 | export const isBarrier = (s: Stack) => s.count === 2; |
| 58 | |
| 59 | /** The four sides in the order WallRing lays them out, clockwise from the top. */ |
| 60 | export const WALL_SIDES = ['top', 'right', 'bottom', 'left'] as const; |
| 61 | export type WallSide = (typeof WALL_SIDES)[number]; |
| 62 | |
| 63 | /** |
| 64 | * Which side of the square sits in front of each seat — the one a seat has to |
| 65 | * throw over. Seats run bottom, right, top, left (see ui/rotation.ts) and the |
| 66 | * sides are drawn top, right, bottom, left, so the two orders are not the same. |
| 67 | */ |
| 68 | export const SEAT_WALL_SIDE: Record<SeatId, number> = { 0: 2, 1: 1, 2: 0, 3: 3 }; |
| 69 | |
| 70 | /** The stacks making up one side, in the order that side is drawn. */ |
| 71 | export function sideStacks(stacks: Stack[], side: number): Stack[] { |
| 72 | return stacks.slice(side * STACKS_PER_SIDE, (side + 1) * STACKS_PER_SIDE); |
| 73 | } |