Kodo Hockey
← articles

How the draft optimiser thinks

Published 20 September 2026, 7:58 am ET

Every draft board answers one question: who is the best player left. That is the wrong question at a draft table. You do not finish the night holding one player, you finish holding a roster, and the pick that maximises the roster is routinely not the pick that maximises the round.

This is how the optimiser scores that difference, what each constant in it is set to and why, and what the measurements actually said, including the ones that went against it.

The objective is a finished roster

A candidate is scored as the difference between two completed rosters: the one you finish with if you take him now, and the one you finish with if you spend this pick elsewhere and he goes back to the room.

The second half is the honest counterfactual. Passing on a player is not the same as never getting him. It leaves him on the board, weighted by the odds he survives to your next turn. A player certain to last is therefore worth almost nothing to take now, and a player certain to go is worth everything. Scarcity is never written down as a rule anywhere in the file. It falls out of that arithmetic.

The pipeline runs in a fixed order, and the order is the argument. Seat the roster you already hold, exactly, to learn which seats are open. Read the survival odds for every player on the board at each of your future picks. For each candidate, fill both rosters forward and then locally re-order them. Score each finished roster, by points or by expected categories won. Rank by the difference, and write a reason a reader can check against the board in front of him.

Seats are a matching problem, not a loop

Which seats are open depends on where the players you already hold are sitting, and a dual-eligible player makes that a real question. With one centre seat, one left wing and one utility, holding a centre-and-left-wing player plus a pure centre, greedy seating sat the dual player at centre, pushed the pure centre to utility, and concluded only left wing was open. Sitting the dual player at left wing instead leaves utility genuinely free.

The consequence was not cosmetic. A high-value centre at the top of the board was absent from the recommendations entirely, because the eligibility filter believed no seat existed for him. Seating is now a maximum matching over about 25 players and seven seat codes, which is exact and costs microseconds. Roughly 200 of the 670 seated players are dual-eligible, and the failure grows likelier every round as seats fill, which is exactly when the margin is thinnest.

A second version of that bug is worth naming because it survived the first fix. Two functions computed maximum matchings independently, and a maximum matching is not unique. One seated a dual player at centre and reported utility spent; the other found the wing and reported utility free. Same size, opposite answers, so a fourth centre read as a starter at full weight while the fill was still planning to buy the seat he had just taken. Both questions are now one subtraction on one function: seat the roster, seat the roster plus him, see which seat disappeared.

Survival is a lottery, not a maximum

What do you expect to get if, at some future pick, you spend it on a seat and take the best player still sitting there? Not the best player on the list. The first arrives only if he lasts, the second only if the first is gone, and so on down the board. Summing value against that weight is the expectation of the best survivor. The leftover weight at the bottom is the chance every one of them is gone, and it contributes nothing, which is the correct answer.

Below, a player's survival to a pick some distance away decays geometrically from his market rank. The rule is one line and worth stating so you can disagree with it: by the time the room reaches his average draft position, he is even money to be gone. Everything else follows from that.

The expected best survivor at one seat
Drag the gap to your next pick. Each man's survival decays geometrically from the market's read of him. The weight column is his share of the lottery: it counts only if everyone above him is gone.
6
PlayerValueSurvivesWeightValue x weight
0.00expected value at this seat100%chance they are all gone-most likely survivor

A player with no market read at all is treated as certain to last, and that is recorded rather than hidden. The reason line says there is no read on him, because we have no information and the room will not touch him are different statements and only one of them is true. Past the last measured horizon the odds extrapolate geometrically from the final two, which is the honest shape: each further turn is another round of the room taking its shot.

Filling the roster forward

Both rosters are completed to the same size. If you take him you have one fewer pick remaining, so the counterfactual gets one more. Comparing a roster of N plus one against a roster of N would recommend whoever is standing in front of you every single time.

The fill is greedy, each future pick going to whichever open seat has the best expected survivor, and then a bounded local search re-orders adjacent pairs and keeps a swap only when the finished roster is better. Greedy is the right first answer and the wrong final one, because survival falls with every turn and two seats can be worth taking in the other order.

A bench seat pays a bench seat's share. The bench weight is 0.33, roughly how often a bench skater is actually in a lineup across a season of injuries and light nights. Not zero, or the model would refuse to draft a bench at all. A player who cannot be seated is discounted by the depth at the loosest seat he covers: the only spare left wing enters the lineup the moment somebody is hurt, and the fifth centre enters when four are, which is to say never.

In the table below the fill constant is 8.8, the median value of the players who would plausibly still be there at the next turn. It is the same in both branches, which is what makes the difference clean.

Best player, or best team
Three candidates for one pick. The score is the finished roster with him, minus the finished roster without him, where 'without' leaves him on the board at his survival odds. Move his availability and watch the recommendation change while his value never does.
15%
CandidateValueSurvivesU(with)U(without)Score
Brady Tkachuk12.015%20.810.610.20
Tim Stützle10.286%19.017.61.43
Charlie Coyle4.998%13.713.60.10
The star is the pick. At 15% to last, passing on him means probably never having him. His value did not change, his availability did.

Categories are won, not summed

A head-to-head week is decided category by category, and scoring a roster as value minus an imbalance penalty is the wrong shape for that in two directions at once. A category you have already lost keeps costing, so the model goes on paying to repair a punt the market has made unrepairable. That is why the balance term saturates: at 200 it returns a byte-identical roster to 60. And a category you have won enormously keeps paying, even though a win is binary and the surplus converts into nothing at all.

The probability of winning a category has both shapes built into it. It flattens at both ends and counts fully in the middle, where the category is genuinely contested. A punt becomes a first-class optimum rather than a permanent fine.

The opponent is built by symmetry. Every team drafts from one pool under one set of rules, so the team across from you is a roster of the same size at the mean drafted quality. The spread is the square root of twice the roster size, for two independent rosters, widened by 2.2 because a seven-game week is far noisier than a roster's true edge.

What one more unit of a category is worth
Drag your lead in a category. Under a variance penalty the marginal value is linear and never stops charging. Under the probability of winning it is the slope of the curve: near zero, more of a lost category buys almost nothing; near one, more of a won category buys almost nothing; in the middle every unit counts.
+0.40
66%chance of winning this category+3.6 ptsvalue of the next tenth of a deviationcontested: every unit counts

Measured. Across 24 paired drafts on two disjoint sets of seats and seeds, the optimiser against itself with two seats in one room, the probability objective won 19, lost 3 and tied 2. That is plus 1.83 category wins per draft on the first set and plus 1.50 on the confirmation, each about 0.47 of a standard error. Assists and power-play points moved from dead last in an eighteen-team room to fifteenth, because a hopeless category stopped charging rent. It is the largest effect anything in the file has produced.

Measured, and switched off. Per-category spreads were measured from a real eighteen-team season and were meant to make a lead in a bunched category decisive and a lead in a strung-out one worth little. Over 12 paired drafts every quantity they were supposed to move either did not move or moved the wrong way. The table is real, the argument still reads well, and it ships behind a flag so the next person to have the idea finds the result rather than the hope.

The constants, and what set them

ConstantValueWhy that value
Fill depth20The old claim that past a dozen picks both branches draw from the same tail and cancel was only approximate. At 14 it still disagreed with 18 on 5 of 51 top recommendations, and a solve costs single-digit milliseconds.
Candidate limit128Raised, not lowered. The pick the model wants is regularly 21st to 40th by raw value, which is the entire point of a scarcity-aware model, so a tight cap truncates exactly the picks the tool exists to find. 40 against 64 disagreed on 8 of 51, 64 against 96 on 4, 96 against 160 on 2, and 128 against 160 on nothing.
Survivor window18How deep the lottery looks before the leftover probability is small enough to ignore.
Swap pairs5Changes no recommendation and is kept anyway. 3,179 swaps tried, 864 accepted, 0 of 51 decisions changed, because the gain lands in both branches and cancels in the difference. Kept because the absolute utilities it improves are what the probability objective reads.
Balance lambda8Zero in a points league, always, and checked twice by the harness. A points league has already converted every stat into one currency, so a balance penalty would push a manager off the best player for a reason his own scoring page does not recognise.
Bench weight0.33Before it, the fill paid every seat the same and drafted eight centres for two seats and a flex. Depth-weighting on top of it took 6.6 centres to 6.1 with outcomes identical on every measure.
Probability spread2.2Widens the two-roster spread for the fact that a week is not a season, and stops a small lead reading as a certain win.
Scarcity re-pricingoffFour single-draft runs came back no better than without it, and a single draft is noise. Two earlier attempts both read beautifully and both measured worse. Ships off until a paired harness says otherwise.

Faceoffs, and the eight-centre roster

A manager reported it exactly: two centre seats and a flex, and the optimiser took four centres in the first five rounds. Reproduced, it was worse than reported, eight centres in seventeen picks and no left wings at all against two left wing seats, at every seat tried.

Three explanations were tested and all three measured as noise: the seating inconsistency above, a harder bench discount, and unknown-odds players on the counterfactual board. It was faceoffs. Only centres win them, 99% of the pool's supply, so a league that counts them rewards stacking the one position you can dress three of, and a benched centre was still credited a third of his faceoff line the way he is credited a third of everything else. Remove the category and the same code drafts eleven centres and fifteen wingers instead of 25 and six.

Which categories only one position can supply
Measured off the draft pool: each category's positive contribution, split by position. A category one position supplies 80% of is locked, and a man who is not in the lineup is credited a twentieth of his line in it rather than a third.
CategorySupply by positionLocked to
G
C:44% R:27% L:24% D:4%
open
A
C:38% D:24% R:21% L:17%
open
PPP
C:38% R:24% D:20% L:18%
open
SOG
C:36% R:24% L:22% D:18%
open
PIM
D:34% C:32% R:17% L:17%
open
HIT
C:33% D:27% L:22% R:19%
open
BLK
D:85% C:11% R:3% L:1%
D (85%)
FOW
C:99% L:1% R:0%
C (99%)
W
G:100%
G (100%)
GA
G:100%
G (100%)
SV
G:100%
G (100%)
68 picks across seats 1, 8, 12 and 15
Centres
25
ideal ~12
Left wings
6
ideal ~10
Right wings
7
ideal ~10
Defence
22
ideal ~20
Goalies
8
ideal ~8

Measured, and neutral rather than a win. 48 paired drafts, eight seeds across six seatings with faceoffs counted, scored on the dressed lineup: 78.06% against 78.43%, a loss of 0.37 with 22 wins from 48, which is a dead heat. Centres per roster fell from 6.83 to 4.40 and the wingers came back. Stronger locks did no better. It is on by default for category leagues because a manager reported the roster and this removes it at no measured cost, and it can be turned off per league. Points leagues have no categories and are untouched.

The larger finding, which is about the harness rather than the model. The tuning harness was scoring the bench. It summed every counting category over all seventeen players, and its own comment named the failure for the rate columns and fixed only those. Stacking centres won the harness because the harness was playing a game no league plays, and every earlier verdict in the file had been measured on that scoreboard. On it, the supply-aware version lost by 4.4. The harness now has a lineup basis that seats the best legal lineup and counts nobody else. Re-tuning the older constants on it is open work.

The reason line

A recommendation nobody can audit is worse than no recommendation, so every clause is checkable against the board in front of the reader: his survival to your next pick, the value cliff behind him at the seat where it is widest, the category you are thinnest in if he fills it, and the market's number the model is disagreeing with.

The alternative it names is the player the model expects you would have had instead, which is the first pick of the counterfactual fill and never the candidate himself. The delta is his score against the best other candidate's, so it is positive on exactly one entry and its size is how much the decision is worth at all. Nothing in the module may throw: it sits in a live draft render path, every input is treated as hostile, and an empty list is a bad answer where an exception is no page.

If you would rather watch it than read it, the model drafts seventeen rounds against a live bot room and prints the reason it gave for every pick.

Every constant on this page is a value in the optimiser module, and every measurement is a result recorded in that module's comments or in the tuning harness on 19 September 2026. The players in the two demonstrations are read from the same pool the draft room runs on.

Feedback