Repository navigation
Jinx Handicaps
Jinx is a self-correcting, deliberately punitive handicap system for pure pursuit race series.
This page is the committee's description of the algorithm. The implementation is
PursuitHandicapEngine
(the arithmetic) and
scoring.js
(times, flags and places); where the two disagree, the code is what runs.
Intended for pure pursuit races: slower boats start earlier, faster boats later, and whoever crosses the finish line first wins. No time correction is applied to the results.
After each race, each boat's handicap is adjusted to change its next start time:
- Top five finishes have 5, 4, 3, 2, 1 minutes added to the next race's start time.
- The total penalty time is given back to the fleet: DNFs, then tail enders, then others.
- The fleet is adjusted for the earliest starter and sunset.
- The penalties are configurable —
4,3,2,1,6,4,2,2,1and so on — but always whole minutes. Size the ladder to the series (§9). - Nobody who raced ever gets more than a minute back.
A handicap is not held as a start time. Each boat has a TCF, which works for a race of any length, so a penalty in minutes is converted into a TCF change (§7).
That conversion is measured against the next race's expected duration — the race the new TCF will be sailed in — so a boat penalised five minutes starts five minutes later next race, whatever length either race is, and however much sunset shortens it (§7). Two things blur it slightly: guns are rounded to the whole minute, and the fleet's median TCF can move.
Giving back what was charged keeps the fleet's TCFs from drifting away from those of other handicap systems. When few boats come, the boats that stayed home take what the racers cannot, so a thin night cannot hand the pool straight back to the few that raced.
Each boat carries a TCF per race, held to four decimal places. It is what the start time is computed from.
| Symbol | Meaning |
|---|---|
t_target_elapsed |
Expected race duration, in minutes — e.g. 60 early in a twilight season and 90 at midsummer. It sets this race's start times (§4) and is what the previous race's time adjustments are measured against (§7). Each new race takes defaultRaceDuration. |
t_earliest_start |
Wall-clock start of the slowest boat. Default 18:00. |
Set club-wide in config.yaml; a series can override any of them on its Handicap
settings form.
| Parameter | Default | Meaning |
|---|---|---|
penaltyList |
[5, 4, 3, 2, 1] |
Whole minutes for 1st, 2nd, 3rd…; a fraction is rounded. Size it to the series — §9. |
defaultRaceDuration |
90 |
Minutes; the expected duration each new race starts with. |
earliestStart |
18:00 |
The slowest boat's gun, for each new race. |
limitBySunset |
false |
Shorten the expected duration so the fleet is home by sunset. |
minutesAfterSunset |
8 |
With limitBySunset, how long after sunset the fleet may be expected home. Twilight lasts beyond sunset; negative means home before it. |
Each boat's expected elapsed time:
τᵢ = t_target_elapsed × TCF_med / TCFᵢ
where TCF_med is the median TCF of the boats entered. The median boat is expected to
take t_target_elapsed; faster boats (higher TCF) less, slower boats more.
Δtᵢ = τ_max − τᵢ slowest boat: 0
t_startᵢ = t_earliest_start + round(Δtᵢ) to the nearest minute
The slowest boat starts on t_earliest_start, itself rounded to the nearest minute.
With limitBySunset, t_target_elapsed is first capped so that the fleet is expected
home by minutesAfterSunset after sunset. Every boat is expected to finish at t_earliest_start + τ_max, and the
slowest boat's τ_max is longer than the median boat's t_target_elapsed, so the cap
is on τ_max:
t_daylight = sunset + minutesAfterSunset − t_earliest_start
t_target_elapsed ≤ floor(t_daylight × TCF_min / TCF_med)
For example, an 18:00 start, a 19:07 sunset and the default 8 minutes after it, a median
TCF of 0.900 and a slowest boat on 0.800 give 75 minutes of daylight, and a target of at
most floor(75 × 0.8 / 0.9) = 66 minutes. The slowest boat then expects 74.25 minutes,
and is home at 19:14. Capping the target itself to 75 would have brought the fleet home
at 19:24.
If that allowance after sunset is at or before the earliest start, or too short to fit even one minute's target, the start times are refused rather than computed.
| Status | How it is decided | Gets back (§6.3) |
|---|---|---|
| Finished | has a finish time | a minute, from the back forwards — nothing in a penalty place |
| DNF | started, did not finish | a minute, before any finisher |
| AVG | the duty boat | up to a minute of what the racers could not take |
| DNS, RET | came and did not start (set by the race officer, or from the times), or retired | up to a minute of what the duty boat left |
| DNC | did not come | an even share of whatever is still left |
| DSQ, ABN | set by the race officer | nothing — frozen, TCF unchanged |
DNF and RET are different. A DNF was still racing when the race ended: it ran out of time, which is about its speed, so its handicap eases. A boat that retired stopped for its own reasons — gear, injury, somewhere to be — which says nothing about its speed, so it is not served with the DNFs. It was there, though, so it is scored exactly as a DNS.
A boat with a finish time counts as finished even if its start was not timed.
OCS_PENALTY = 5 min
OCS_FLAGGED = the RO has flagged the boat OCS
effective_start = actual_start captured ? actual_start
: OCS_FLAGGED ? allocated_start − 1s
: allocated_start
late = effective_start − allocated_start positive = after the gun
early = −late
OCS = (OCS_FLAGGED or early > 0), unless the RO has removed the flag
scored_finish = OCS ? finish + early + OCS_PENALTY : finish
corrected_finish = finish + max(early, 0) the printed, transcribed time
scored_elapsed = scored_finish − allocated_start
handicap_elapsed = finish − effective_start what the engine is given
Corrected finish is not scored finish, deliberately:
| Gives back the head start | Adds the 5-min penalty | Used for | |
|---|---|---|---|
scored_finish |
yes | yes | places |
corrected_finish |
yes | no | the finish sheet |
The finish sheet is typed into SailSys with the OCS flag, and SailSys applies the penalty that flag implies — including it in the printed time would apply it twice. A boat finishing at 19:46:10 having crossed 5 s early prints as 19:46:15, OCS.
The engine is given handicap_elapsed, because a boat that started 30 s late really did
sail 30 s less.
Places rank by scored_finish; ties share the better place and the next place
skips (5, 5, 5, 8). Boats flagged AVG, DNC, DNS, DNF, DSQ, RET or ABN are unplaced. The
finish sheet's Elapsed Place ranks scored_elapsed the same way.
Flags mostly follow from the times: DNC if the boat did not come, DNF or DNS if it came and did not finish, OCS if it crossed early, AVG for the duty boat. The race officer can override any of them, and the override is saved with the race. A boat has at most one of AVG, DNF, RET, DNS, DSQ, DNC; setting RET on a boat that did not finish replaces the DNF its times imply.
Series points are not computed here.
- Finishers — may pay a penalty (§6.2) and get a minute back (§6.3).
- DNF, AVG, DNS, RET and DNC — pay nothing, may get time back.
- DSQ, ABN — frozen; out of the placings and the giveback.
- Casuals — handicapped in a second pass (§6.5).
Nothing is measured against the fleet's median time.
penalty_i = penaltyList[rank − 1] (0 beyond the end of the list)
P = Σ penalty_i whole minutes
rank is the boat's place among the finishers in this pass, so a casual that wins does
not take a rung from the series boats (§6.5).
The P minutes come back one minute per boat, in this order, until they run out:
- each boat that ran out of time (DNF);
- the last boat home, then the second last, and so on forwards — stopping short of the penalty places.
Then, if minutes are left:
- the duty boat (AVG) gets up to a minute;
- each boat that came and did not start (DNS) or retired (RET) gets up to a minute;
- what is still left is shared evenly among the boats that stayed home (DNC).
A big night therefore reads +5 +4 +3 +2 +1, 0 … 0, −1 … −1: the ladder, then the middle
of the fleet left alone, then a minute back for each boat at the back.
Why it is shaped like this:
- A penalty place gets nothing back. The ladder is the club's statement of what a good result costs; giving part of it straight back would make the printed 5, 4, 3, 2, 1 a fiction.
- Whole minutes. The committee asked for no sub-minute accounting. The only fractions are at the edges (§6.3.1).
- DNFs first. A boat that could not get round in the time is the one whose handicap should ease soonest.
- The boats at home take the leftovers. Before 2026 the pool went only to the boats that raced, so on a thin night the few boats out got their own penalties straight back, and finishing last of five was the best handicap outcome available. Now the racers take at most a minute each and the rest goes to the fleet that did not have to be beaten.
- The duty boat and the non-starters before the boats at home. They were there: the duty boat was running the race, and a DNS or RET turned up and did not get round. All are capped at a minute, like the racers, so a thin night does not hand them more than a boat that sailed it.
-
More DNFs than minutes — they share
Pevenly; nobody else gets anything. - Fewer minutes than duty boats, or than DNSs and RETs — that group shares what is left evenly; nobody behind it gets anything.
- Minutes left over — shared evenly by the DNCs, typically a few seconds each. A DNC gets more than a minute only if the ladder is too big for the series (§9).
- Minutes left over and nobody stayed home — they are discarded. That needs the whole entry list out and too few of it behind the ladder, so it should not happen.
If every boat that raced is on the penalty ladder, and there is no DNF, no duty boat,
no DNS or RET and nobody stayed home, nothing is charged, and every handicap stays
where it was. That happens
only when the fleet is no bigger than the ladder; the fix is a shorter penaltyList
(§9).
How the minutes come back on six different nights for a thirty-boat entry list on the 5, 4, 3, 2, 1 ladder. One boat is the duty boat (AVG) every night; "come" counts the boats on the water besides it, including any DNS and RET. The first five rows set up the night; the boats that finished are the rest. Below them is one row per boat, finishers in finishing order and then the others in the order they are served, each with its net adjustment in minutes — positive is a penalty, negative is time back.
| #Came | 29 | 25 | 20 | 20 | 19 | 9 |
|---|---|---|---|---|---|---|
| #DNC | 0 | 4 | 9 | 9 | 10 | 20 |
| #AVG | 1 | 1 | 1 | 1 | 1 | 1 |
| #DNS | 0 | 0 | 0 | 2 | 0 | 1 |
| #RET | 0 | 0 | 0 | 0 | 0 | 1 |
| #DNF | 0 | 2 | 0 | 16 | 0 | 1 |
| 1 | +5 | +5 | +5 | +5 | +5 | +5 |
| 2 | +4 | +4 | +4 | +4 | +4 | +4 |
| 3 | +3 | +3 | +3 | −0.56 DNF | +3 | +3 |
| 4 | +2 | +2 | +2 | −0.56 DNF | +2 | +2 |
| 5 | +1 | +1 | +1 | −0.56 DNF | +1 | +1 |
| 6 | 0 | 0 | −1 | −0.56 DNF | −1 | −1 |
| 7 | 0 | 0 | −1 | −0.56 DNF | −1 | −1 DNF |
| 8 | 0 | 0 | −1 | −0.56 DNF | −1 | −1 AVG |
| 9 | 0 | 0 | −1 | −0.56 DNF | −1 | −1 DNS |
| 10 | 0 | 0 | −1 | −0.56 DNF | −1 | −1 RET |
| 11 | 0 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 12 | 0 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 13 | 0 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 14 | 0 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 15 | −1 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 16 | −1 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 17 | −1 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 18 | −1 | −1 | −1 | −0.56 DNF | −1 | −0.5 DNC |
| 19 | −1 | −1 | −1 | 0 AVG | −1 | −0.5 DNC |
| 20 | −1 | −1 | −1 | 0 DNS | −1 AVG | −0.5 DNC |
| 21 | −1 | −1 | 0 AVG | 0 DNS | 0 DNC | −0.5 DNC |
| 22 | −1 | −1 | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 23 | −1 | −1 | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 24 | −1 | −1 DNF | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 25 | −1 | −1 DNF | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 26 | −1 | 0 AVG | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 27 | −1 | 0 DNC | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 28 | −1 | 0 DNC | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 29 | −1 | 0 DNC | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
| 30 | 0 AVG | 0 DNC | 0 DNC | 0 DNC | 0 DNC | −0.5 DNC |
ThirtyBoatFleetTest pins every cell of this table.
net_i = penalty_i − reward_i
Within a pass, Σ net_i = 0 — the pool is given back in full — unless minutes were
discarded (§6.3.1). A positive net means the boat was charged: its TCF rises and it
starts later next race. A negative net means it starts earlier.
A casual sailed, so its handicap should move, but one boat turning up for one night must not change the series boats' handicaps. So the algorithm runs twice:
| Pass | Fleet | Whose answer it is |
|---|---|---|
| 1 | series entrants only | every series entrant |
| 2 | everybody | the casuals only |
Consequences: when a casual wins, the top penalty is charged twice — to the casual, and to the first series boat home, which won the series race. And the combined answer does not sum to zero across both groups; each pass does on its own.
Thirty boats entered. Twenty-five come: twenty-four finish and one runs out of time;
five stay home. Penalties [5, 4, 3, 2, 1]; the next race's expected duration is 90
minutes.
P = 5 + 4 + 3 + 2 + 1 = 15 min
the DNF 1 min (14 left)
24th … 11th home, a minute each 14 min ( 0 left)
10th … 6th home nothing
stayed home nothing
1st place: net = +5
6th place: net = 0
24th place: net = −1
the DNF: net = −1
scale = next expected × TCF_med = 90 × 1.0 = 90
new_TCF(1st) = 1.0 / (1 − 5 / 90) ≈ 1.0588
new_TCF(24th) = 1.0 / (1 + 1 / 90) ≈ 0.9890
Same thirty entered, same ladder. Five boats come and all finish.
P = 15 min
behind the ladder nobody
stayed home = 25 boats, 15 min / 25 = 0.6 min each (36 s)
1st place: net = +5
5th place: net = +1
stayed home: net = −0.6
Every boat that came pays, and every boat that did not gains 36 seconds: five boats took a good result off a night the rest of the fleet sat out.
new_TCFᵢ = TCFᵢ / (1 − netᵢ × TCFᵢ / (expectedDuration × TCF_med))
TCF_med is the median TCF of the pass's finishers, DNFs, duty boats, DNSs, RETs and DNCs. expectedDuration
is the next race's t_target_elapsed, because that is the race the new TCF is sailed
in. Since a start is set from τ = t_target_elapsed × TCF_med / TCF (§4), this moves a
boat's expected time in the next race by exactly net minutes while the median holds.
Measured against this race instead, a five-minute penalty earned on a 90-minute night
would be three and a third minutes on a 60-minute one.
With limitBySunset, it is the next race's t_target_elapsed after the sunset cap
(§4), worked out from that race's date and earliest start and this race's TCFs. The cap
is not otherwise applied until the next race's start times are computed, and against the
uncapped 90 minutes a five-minute penalty on a night capped to 59 would start only
three and a third minutes late. This race's TCFs stand in for the new ones the cap will
really see; a night's adjustments move the slowest-to-median ratio too little to matter.
If the next race has no daylight at all, its uncapped target is used: computing its
start times will refuse anyway, and the race officer must move it before then.
The last race of a series has no next race and uses its own t_target_elapsed, falling
back to defaultRaceDuration. The expected duration is used rather than what any race
actually took: a night that overran because the breeze died should not shrink every
correction. If a penalty is so large that the denominator reaches zero, processing is
refused.
If the next race's expected duration is changed after handicaps are saved, the carried TCFs are not recomputed; Unlock results and process again to pick it up.
- Before the race. Set the expected duration and earliest start; compute and publish the start times (§4).
- The race. Record came, starts, finishes and flags.
-
Processing.
- Classify every boat (§5).
- Charge penalties by rank among the finishers (§6.2).
- Give the pool back a minute at a time, leftovers to the boats at home (§6.3); if nobody may receive, charge nothing (§6.3.2).
- Convert each boat's net into a new TCF against the next race's expected duration (§7).
- If any casuals raced, run 2–4 again over everybody and keep only the casuals' answers (§6.5).
- Save the adjustments, which locks the race, and carry the new TCFs into the next race's entry list.
Each race's entry list keeps the TCF every boat sailed it on, so processing race 5 cannot change what race 4 says. Unlock results discards the saved adjustments so the race can be corrected and processed again; the next race keeps the TCFs it was given until then.
Size the ladder to the series. About one rung per six boats, counting down:
| series | 30 | 24 | 18 | 12 | 8 or fewer |
|---|---|---|---|---|---|
penaltyList |
[5,4,3,2,1] |
[4,3,2,1] |
[3,2,1] |
[2,1] |
[1] |
This is series size, not turnout: a thirty-boat series keeps [5,4,3,2,1] on a six-boat
night, because the boats at home take the leftovers. Sized like this, the ladder's total
is never more than the number of boats that could be at home, so a boat at home never
gets more than a minute either (barring several frozen boats). A small club where
everyone comes needs a short ladder, or nobody is left to receive (§6.3.2) — which is
also why one boat sailing alone finishes the night where it started. The series settings
form says what fleet a ladder suits.
What the winner pays. The winner gets nothing back, so it starts
penaltyList[0] minutes later next race, less any movement in the median.
Rounding. Guns are whole minutes; the TCF keeps the remainder, so a few seconds earned at home accumulate until they move a gun.
Sandbagging. Tail-ending earns at most a minute a race, needs sustained pretence, and has a high social cost in a club fleet.
Convergence. Starting TCFs already reflect known performance, so the system should be near equilibrium from the first race, with each well-sailed boat around mid-fleet — where the minute giveback leaves it alone.
algorithm:
penaltyList: [5, 4, 3, 2, 1] # whole minutes; ~1 rung per 6 boats
defaultRaceDuration: 90 # minutes
earliestStart: "18:00"
limitBySunset: false
minutesAfterSunset: 8 # how long after sunset the fleet may finish-
PursuitHandicapEngine— §§4, 6 and 7. Pinned byMinuteGivebackTest,HandicapRulesTestandPursuitHandicapEngineTest. -
scoring.js— §5: times, flags, places, and what the engine is given. It runs in the browser because the unsaved edits are there, and the race page and the finish sheet share it. Pinned byscoring-test.html. -
JinxConfig— §3.3, including the retired keys that still load.
Settings that have been retired are still accepted in old configuration files and
ignored: the weighted giveback's variant, penaltyScaling, givebackGamma,
dnfWeight and dncWeight, and before them dnfAllowance, givebackFleet,
dnfInRaceDuration, v0knots and the race-median raceDuration.