requirements: - id: R1 title: Mobile 2D overhead curling body: Players play curling in a mobile browser from a 2D overhead view. - id: R2 title: Portrait sheet body: The sheet is shown in portrait. - id: R3 title: Two teams, unlimited clients body: A room has two teams and an unlimited number of clients. - id: R4 title: Team pick per device body: Each client picks Team 1 or Team 2 on that device. - id: R5 title: Anyone on turn may throw body: Anyone on the team whose turn it is may throw. - id: R6 title: Shareable room link body: Players join a room from a shareable link. - id: R7 title: Create room from empty link body: If the link has no room, the client creates one and writes that room into the URL. - id: R8 title: Ten ends, eight stones body: A match has 10 ends. Each team has 8 stones per end. - id: R9 title: Extra ends until a lead body: Extra ends are played while the score is tied after 10 ends. The match ends when one team leads after an end. - id: R10 title: Random hammer body: Hammer is random at the start. The team without hammer throws first in the end. - id: R11 title: Hammer after score or blank body: The team that scores gives hammer away. A blank end keeps hammer. - id: R12 title: Alternate throws body: After a throw, that team has one fewer stone and the other team throws. - id: R13 title: Score when stones are gone body: When all throws have completed, the end is scored. - id: R14 title: House only scores body: Only stones touching the house can score. - id: R15 title: Count to opponent nearest body: The team closest to the tee scores one point for each of its stones nearer than the opponent's nearest stone. - id: R16 title: Blank end is 0-0 body: If no stone can score, the end is 0-0. - id: R17 title: Shared live state body: Every client sees the current end, both scores, hammer, whose turn it is, stones remaining, stone positions, trajectories from all shots, and whether the match is over. - id: R18 title: Winner on match end body: When the match ends, both scores are shown with the winning team. - id: R19 title: Hack to broom body: A thrown stone starts at the hack and travels toward the broom. The player chooses initial speed and curl. - id: R20 title: Clockwise is thrower's right body: Clockwise curl moves the stone to the thrower's right. - id: R21 title: Stone bounce, no line bounce body: Stones bounce off other stones. Stones do not bounce back from a sideline or the backline. - id: R22 title: Out of play rules body: A stone is out if it touches a sideline. A stone must fully cross the backline to be out. A stone is out if it never fully crosses the hog line. - id: R23 title: Sheet markings body: The ice is a curling sheet with a house, hog line, sidelines, and a backline on the 12-foot ring. - id: R24 title: Default house framing body: The default view frames the house, sidelines, and backline above the controls. - id: R25 title: Pan to hog body: The player can pan up and down the sheet to the hog line. - id: R26 title: Broom anywhere then throw body: On turn, the player drags to place the broom anywhere on the sheet, then throws. - id: R27 title: Discrete throw weights body: The player sets throw weight on a discrete scale of draw weights 1 through 10, then named takeouts hack, board, control, normal, and peel. - id: R28 title: Curl direction body: The player sets curl clockwise or counterclockwise. Clockwise is the default. - id: R29 title: Throw only on turn while still body: Throw controls work only on that team's turn and only while stones are not moving. - id: R30 title: Stones, hammer, scoreboard HUD body: The HUD shows eight remaining-stone markers per team, a hammer marker, and a scoreboard with each end and a total. - id: R31 title: End overlay body: After the stones stop and the end is scored, an end-result overlay appears. The player dismisses it. - id: R32 title: Shared motion, hide out stones body: All clients see the same stone motion. A stone that leaves play is hidden when it leaves the sheet. - id: R33 title: Copyable errors body: Errors and the final result appear on the page as copyable text, not as a blocking alert. - id: R34 title: No pinch-zoom, remember team body: Pinch-zoom is off. The client remembers the last team choice. choices: - id: C1 title: Stack body: Backend is Rust, Axum, WebSocket, Rapier2D, server-authoritative at 120 Hz. Frontend is TypeScript, Vite, Canvas2D, portrait-first touch. - id: C2 title: Team colours body: Team 1 is red. Team 2 is yellow. - id: C3 title: Wire protocol body: WebSocket JSON with snake_case tags. game_state carries totals, hammer, turn_team, per-end scoreboard, stones_remaining, and stones. Trajectories sample at 40 Hz from the 120 Hz sim. No room-full limit. No end_scored message. - id: C4 title: Physics how body: Ice friction is a server µ(v) table shared by linear and angular damping. Curl uses initial |ω| = 5 rot / 14 s with clockwise to the thrower's right. Stone-stone restitution is 0.9. Sidelines and backline are not colliders; out-of-play is applied after sim. Stone ids are { team, n } with n 1 through 8. Lengths use FEET_TO_METERS = 0.3048. - id: C5 title: Weight mapping body: Discrete UI weights map to calibrated open-ice stop speeds in SPEED_TABLE. The server takes velocity and curl, not a local friction scalar. - id: C6 title: Out of scope body: No accounts, persistence, anti-cheat, replay log, turn timer, or sweeping.