## curltastic A multiplayer 2D curling game for mobile browser. Any number of clients can join a room, pick **Team 1** or **Team 2** freely, and throw when it is that team's turn (solo pass-and-play or remote opponents). Default palette: Team 1 red, Team 2 yellow. - **Backend** — Rust, Axum, WebSocket, Rapier2D physics (server-authoritative, 120 Hz). - **Frontend** — TypeScript, Vite, Canvas2D, portrait-first touch UI. ### Run locally 1. Install Rust via [rustup](https://rustup.rs/) (if not already installed). 2. Start the backend: ```bash cd backend cargo run --release # listens on 0.0.0.0:3000 ``` 3. Start the frontend dev server: ```bash cd frontend npm install npm run dev # opens on 0.0.0.0:5173 by default ``` 4. Open one or more browser tabs to the same room URL, e.g.: ``` http://localhost:5173/?room=DEMO1 ``` Use the team dropdown to switch between Team 1 and Team 2 anytime. Use *Copy share link* to invite another device. ### Controls - On your team's turn, drag on solid ice to place the broom (aim is **not** limited to the house). - Otherwise, drag to pan (default framing shows house + sidelines; pan up toward the hog line). - Team dropdown: free switch mid-game (solo: throw for Team 1, switch, throw for Team 2). - Velocity slider (release speed, m/s), curl buttons, friction scalar **0.5–1.5** (local; multiplies ice µ(v) table). - Tap **THROW**. Anyone joined as the current turn team may throw. - Parallel multi-stone trajectories; rotation θ comes from physics. - Skeuomorphic 2×8 stones-left HUD; end-of-end modal with scoreboard (auto ~5s + manual dismiss). ### Physics (high level) - Pure initial **velocity** (m/s), not discrete weight. - Ice friction µ(v) table (interpolated) × local scalar; linear and angular damping share the table. - Curl: initial |ω| = 5 rot / 14 s; lateral continuous model (clockwise → right). - Stone–stone contacts are **nearly elastic** (`STONE_RESTITUTION = 0.9`) so takeouts launch both rocks along the impact line instead of plastic-sticking. - Back line and sidelines are **not** colliders — touch → out of play after sim. - Stone ids: `{ team, n }` with n = 1…8 per team per end. - Foot-derived radii use `FEET_TO_METERS = 0.3048`. ### Architecture / protocol - WebSocket JSON, snake_case tags. - `game_state` includes totals, hammer, turn_team, **scoreboard** (per end: hammer, team1 pts, team2 pts), **stones_remaining**, stones. - `trajectories` message: `{ stones: [{ stone_id, rotation, team, trajectory: [[x,y,theta], ...] }] }` sampled at 40 Hz from 120 Hz sim (`t = index / 40`). - No room-full limit; no `end_scored` message (scoreboard replaces it). ### E2E tests With the backend running (`cargo run --release` on :3000): ```bash cd e2e npm install -g ws # or npm install ws node e2e_test.cjs node e2e_score.cjs node e2e_persistence.cjs node e2e_multi_client.cjs node e2e_end_score.cjs # full end → scoreboard entry + next end node collision_trajectory_qa.cjs node load_test.cjs # optional concurrent rooms ``` Unit tests: `cd backend && cargo test`, `cd frontend && npm test`. ### Branches (this work) - **`jasonlooked`** — PR-A refactor: multi-client, multi-path animation, free broom, camera, FEET_TO_METERS. - **`pr2-features`** — PR-B features: physics rewrite, team1/2 wire, scoreboard, HUD, modal (branched from PR-A tip). ### Limitations - No accounts, persistence, anti-cheat, replay log, or turn timer. - Sweeping not implemented; friction/curl not shared-room state. - Stones past back/sideline/hog rules are removed from play after simulation.