Reviewed-on: #3
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.
Product must-haves and decided hows live in .pm/board.yaml.
- Backend — Rust, Axum, WebSocket, Rapier2D physics (server-authoritative, 120 Hz).
- Frontend — TypeScript, Vite, Canvas2D, portrait-first touch UI.
Run locally
- Install Rust via rustup (if not already installed).
- Start the backend:
cd backend cargo run --release # listens on 0.0.0.0:3000 - Start the frontend dev server:
cd frontend npm install npm run dev # opens on 0.0.0.0:5173 by default - Open one or more browser tabs to the same room URL, e.g.:
Use the team dropdown to switch between Team 1 and Team 2 anytime. Use Copy share link to invite another device.http://localhost:5173/?room=DEMO1
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).
- Weight slider: draws 1–10 then takeouts hack, board, control, normal, peel. Curl buttons.
- 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 overlay with scoreboard (player dismisses).
Physics (high level)
- Discrete weight labels map to calibrated release speeds (m/s) on the server.
- Ice friction µ(v) table (interpolated) on the server; 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_stateincludes totals, hammer, turn_team, scoreboard (per end: hammer, team1 pts, team2 pts), stones_remaining, stones.trajectoriesmessage:{ 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_scoredmessage (scoreboard replaces it).
E2E tests
With the backend running (cargo run --release on :3000):
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.
Limitations
- No accounts, persistence, anti-cheat, replay log, or turn timer.
- Sweeping not implemented.
- Stones past back/sideline/hog rules are removed from play after simulation.
Description
Languages
TypeScript
43.5%
Rust
31.7%
JavaScript
15.6%
CSS
7.9%
Dockerfile
1%
Other
0.3%