export const TICK_RATE_HZ = 120 export const SAMPLE_RATE_HZ = 40 export const STONES_PER_TEAM = 8 export const ENDS = 10 export const SHEET_WIDTH = 5.0 export const SHEET_LENGTH = 45.0 export const FEET_TO_METERS = 0.3048 export const HOUSE_CENTER = { x: 0, y: 38.5 } export const HOUSE_RADIUS = 6 * FEET_TO_METERS export const BUTTON_RADIUS = 0.5 * FEET_TO_METERS export const FOUR_FT_RADIUS = 2 * FEET_TO_METERS export const EIGHT_FT_RADIUS = 4 * FEET_TO_METERS export const TWELVE_FT_RADIUS = 6 * FEET_TO_METERS export const HOG_LINE_Y = 21.0 /** Backline on outer house ring (tee + 6 ft). */ export const BACK_LINE_Y = HOUSE_CENTER.y + HOUSE_RADIUS export const HACK_Y = 2.0 export const STONE_RADIUS = 0.15 /** Feet relative to tee for weights 1..10 (front is negative). */ export const WEIGHT_STOP_OFFSET_FT = [-11, -9, -7, -5, -3, -1, 0, 1, 3, 5] as const export const HACK_STOP_OFFSET_FT = 12 /** Calibrated m/s for weights 1..10 (open-ice stop vs tee offsets). */ export const WEIGHT_SPEEDS = [ 2.2573, 2.2783, 2.2992, 2.3199, 2.3404, 2.3608, 2.3709, 2.3810, 2.4012, 2.4211, ] as const export const HACK_SPEED = 2.4899 /** Takeouts: open-ice stop at hack+N feet (N = 6,12,18,24). */ export const BOARD_SPEED = 2.5492 export const CONTROL_SPEED = 2.6408 export const NORMAL_SPEED = 2.7448 export const PEEL_SPEED = 2.8499 export const BOARD_STOP_OFFSET_FT = HACK_STOP_OFFSET_FT + 6 export const CONTROL_STOP_OFFSET_FT = HACK_STOP_OFFSET_FT + 15 export const NORMAL_STOP_OFFSET_FT = HACK_STOP_OFFSET_FT + 25 export const PEEL_STOP_OFFSET_FT = HACK_STOP_OFFSET_FT + 35 /** Tee-line weight (category 7). */ export const DRAW_VELOCITY = WEIGHT_SPEEDS[6] export const MIN_SPEED = WEIGHT_SPEEDS[0] export const MAX_SPEED = WEIGHT_SPEEDS[9] export type SpeedLabel = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 'hack' | 'board' | 'control' | 'normal' | 'peel' export const SPEED_TABLE: Record = { 1: WEIGHT_SPEEDS[0], 2: WEIGHT_SPEEDS[1], 3: WEIGHT_SPEEDS[2], 4: WEIGHT_SPEEDS[3], 5: WEIGHT_SPEEDS[4], 6: WEIGHT_SPEEDS[5], 7: WEIGHT_SPEEDS[6], 8: WEIGHT_SPEEDS[7], 9: WEIGHT_SPEEDS[8], 10: WEIGHT_SPEEDS[9], hack: HACK_SPEED, board: BOARD_SPEED, control: CONTROL_SPEED, normal: NORMAL_SPEED, peel: PEEL_SPEED, } export type Team = 'team1' | 'team2' export type Phase = 'waiting' | 'playing' | 'simulating' | 'scoring' | 'end_complete' | 'game_complete' export interface StoneId { team: Team n: number } export interface StoneState { id: StoneId team: Team x: number y: number rotation: number } export interface DrawableStone { x: number y: number rotation: number team: Team } export interface EndScore { end: number hammer: Team team1: number team2: number } export interface ClientThrowMessage { type: 'throw' team: Team broom_x: number broom_y: number velocity: number curl: number } export interface ServerJoinedMessage { type: 'joined' room: string } export interface ServerWaitingMessage { type: 'waiting' message: string } export interface ServerGameStateMessage { type: 'game_state' end: number scores: number[] hammer: Team turn_team: Team scoreboard: EndScore[] stones_remaining: number[] stones: StoneState[] phase: Phase } /** Path samples are (x, y, theta). Time is sample_index / SAMPLE_RATE_HZ. */ export interface StonePath { stone_id: StoneId rotation: number team: Team trajectory: [number, number, number][] } export interface ServerTrajectoriesMessage { type: 'trajectories' stones: StonePath[] } export interface ServerGameOverMessage { type: 'game_over' scores: number[] winner: Team | null } export interface ServerErrorMessage { type: 'error' message: string } export type ServerMessageTyped = | ServerJoinedMessage | ServerWaitingMessage | ServerGameStateMessage | ServerTrajectoriesMessage | ServerGameOverMessage | ServerErrorMessage export type ServerMessage = ServerMessageTyped export function isTeam(value: unknown): value is Team { return value === 'team1' || value === 'team2' } export function stoneIdKey(id: StoneId): string { return `${id.team}:${id.n}` } export function stoneIdsEqual(a: StoneId, b: StoneId): boolean { return a.team === b.team && a.n === b.n }