Skip to main content

F201v7 — In-Race Horse Position Randomness (Sinusoidal Oscillation)

Jira: HEDGE-223 Phase: 3 (UI & Presentation) Status: ✅ Implemented (V7 only)

Business Rule

A horse race looks more alive when horses jostle for position rather than sliding smoothly to a fixed spot. Each active horse must continuously oscillate around its probability-derived target position, creating organic movement that reflects the uncertainty of a live race. The oscillation must never change the rank order of horses — a horse with a higher win probability must always appear further right than a horse with a lower probability, even at the extremes of each horse’s drift range. Eliminated horses (scroll-off state) do not oscillate.

Technical Design

Base Position

Each horse’s base (target) position is computed by probToPx(p, fi) — a logarithmic mapping of percentWin (0–1) to a pixel x-position on the track:
At RIVER end, positions are overridden to a compact 200px range just right of the TURN card, ranked by hand strength.

Sinusoidal Noise

Each horse has its own horseOscParams[i] object, initialised at game load with random amplitudes and phase offsets across 3 sinusoidal components:
At each rAF frame the raw oscillation offset is:
where t is elapsed race time in seconds. The 3-component sum produces an irregular, aperiodic waveform that avoids mechanical-looking periodicity.

Rank-Order Clamping

After each horse’s oscillating position is computed, a clamping pass runs in rank order (highest probability first) to ensure no horse overtakes its neighbour:
OSC_MIN_GAP = 20px. This guarantees the visual rank order always matches the probability rank, regardless of oscillation extremes.

rAF Lerp Drift Loop

Horses do not use CSS transition: left. Instead, a requestAnimationFrame loop runs continuously while raceStarted=true and raceEnded=false. Each frame:
  1. Compute oscillating target position for every active horse
  2. Apply rank-order clamping
  3. Lerp each horse’s current pixel position toward its clamped target:
  4. Set horseEl.style.left = current + 'px'
The lerp factor smooths out abrupt target changes when percentWin updates between stages, providing an organic deceleration into the new base position rather than a snap.

Active / Inactive Guard

  • Oscillation only runs while raceStarted=true AND raceEnded=false
  • Eliminated horses (horseScrollingOff[i]=true) are excluded from the oscillation loop; they travel left at TRACK_PX_PER_SEC independently until off-screen
  • At HOLE (before first Run click), raceStarted=false — horses are stationary

Key Constants


Acceptance Criteria

AC1. While the race is active (raceStarted=true, raceEnded=false), every active horse continuously changes its horizontal position in a non-linear, irregular pattern — it must not travel in a straight line or exhibit a perfectly regular period. AC2. The oscillation amplitude must not exceed OSC_AMP_FRAC × trackWidth (4% of track width) from each horse’s current probability target position. AC3. At no point during oscillation does a horse with a lower percentWin appear to the right of a horse with a higher percentWin; the visual rank order always matches the probability rank order. AC4. The minimum visual gap between any two adjacent oscillating horses is at least OSC_MIN_GAP (20px) at all times. AC5. Eliminated horses (horseScrollingOff[i]=true) do not participate in the sinusoidal oscillation loop; they move leftward at a constant scroll speed. AC6. At HOLE (before the first Run click), horses are stationary — oscillation is inactive. AC7. When percentWin values update at a new stage, horses lerp smoothly to their new base positions rather than snapping; the transition appears organic, not mechanical.