A curling sheet with the three things that actually make curling curling:
- The stone curls hardest at the end. Lateral acceleration scales as
1 / (v + floor), so a stone runs nearly straight down the sheet and spends its last couple of metres bending. On a draw to the button that works out to about 0.86m of total drift with 59% of it arriving in the final 10% of travel — which is why you aim a metre wide of the target and let the ice bring it back, and why the dashed aim line in the demo is a line the stone visibly leaves. - Sweeping is a resource, not a button. It cuts friction to 74% and curl to 42% for as long as you hold it, out of 3.4 seconds per stone. Spend it early and the stone runs 1.9m farther but arrives straight; spend it late and you buy 1.0m without giving up the bend. Deciding when is the whole skill, because the two effects fight each other.
- Granite barely deforms. Collisions resolve along the line of centres with restitution 0.97 and the tangential components untouched, which is what makes a takeout so directional: a hit 10cm off-centre rolls the shooter away at an angle instead of sticking, and a hit dead-on sends the shooter straight through the back.
The demo plays a three-end game against an opponent that isn’t faking it: the
physics is deterministic, so the rival plays out ~108 candidate shots headlessly
with simulateShot() before picking one, then throws it with human-sized
execution error. It draws when it’s behind, takes your stone out when you’re
sitting shot rock, and gets the same 3.4 seconds of sweep you do. Hog line,
sidelines, back line and end scoring are all in.
Reuse
The mechanic lives in src/hammer.js as a framework-free ES module. Geometry is
in metres: x runs down the sheet from the release point, y is lateral offset
from the centre line.
createEnd(opts)— an end in progress. Set up a shot withsetBroom(y)(the aim target at the tee line),setWeight(w)(0..1),setTurn(±1), thendeliver(). Callupdate(dt)each frame andsetSweeping(bool)while a stone is moving.phasewalksaim → sliding → aim … → scored.- State exposes
stones(each{x, y, vx, vy, spin, rotation, team, moving}),live(the stone in motion),sweepLeft,lastShot,result, andevents— a queue of{type: 'hit'|'removed', x, y, force}you drain yourself for impact effects. simulateShot(stones, shot, opts)plays a shot out headlessly and returns the resulting stones. The physics has no randomness in it, so this is an exact preview rather than an estimate — it is how the demo’s opponent thinks, and it is equally useful for a shot-suggestion overlay or for testing.scoreStones(),distanceFromTee(),inHouse(),weightForDistance()anddistanceForWeight()are exported standalone; scoring is pure, so you can run it on any array of stones.- Every constant worth touching is in
DEFAULTS—friction,curl,sweepFriction,sweepCurl,restitution,sweepBudget. Rendering and input are entirely the caller’s job.
Port notes for DragonRuby: advance() is pure state math over an array with no
DOM dependencies, so it translates directly into a tick method; stones maps to
args.outputs.sprites with rotation driving the handle.
Gotchas
- Weight is linear in reach, not velocity.
weight0..1 maps to how far the stone would run if nothing stopped it (6m to 26m), and the velocity is derived from that. It makes the control perceptually even where it matters: the draw band is a wide, usable stretch of the bar instead of a sliver near the bottom. Anything past ~0.47 is takeout weight and leaves the sheet if it misses, exactly as it should. advance()sub-steps at 1/240s internally. Don’t step the world by hand at frame rate — a takeout at hack weight covers most of a stone’s diameter per frame and will tunnel straight through its target.- The hog line is judged once, when the shot settles, and only against the stone that was thrown. Stones already in play that get pushed back short of it stay on the sheet — they were legally delivered, and shoving a rock back behind the hog line is a real shot, not a foul.
setSweeping(true)only ever helps the stone being thrown, even if the shot set three others rolling.- The demo in
demo/bundles its own copy of the module (self-contained by contract). If you touchsrc/, re-copy it intodemo/.