Is Your Draft Order Randomizer Actually Random? The Shuffle Algorithm That Decides It
Every fantasy league eventually settles its draft order the same way: someone clicks "randomize" and everyone trusts the result. Almost nobody checks what "random" actually means inside the tool. Two draft order generators can both claim to be random and still hand out meaningfully different odds of landing the first pick — and the gap between them comes down to one specific algorithm, not a philosophy.
The shuffle method most tools quietly get wrong
The most common way developers shuffle a list in JavaScript is a one-liner: sort the array with a comparator that returns a random number, something like array.sort(() => Math.random() - 0.5). It reads as correct. It isn't.
Wikipedia's entry on the Fisher–Yates shuffle explains why: random-comparator sorting is biased because the number of possible permutations of a list doesn't evenly divide the number of random outcomes the sort can produce. For a three-team list there are only 6 valid orderings but 27 possible sequences of comparator results, so some orderings come out more often than others by construction — not by bad luck. The same entry notes that random-comparator sorting also violates a property every correct sort depends on, transitivity, which is why the trick can additionally cause outright bugs like an endless loop, on top of the bias.
That means a draft order tool built on the "sort by random" shortcut isn't drawing every possible pick order with equal probability. Some managers are quietly more likely to land the No. 1 pick than others, and the tool has no way of telling you that — because it doesn't know it either.
The algorithm that actually is unbiased
The fix has a name and a six-decade history. The Fisher–Yates shuffle, modernized by Richard Durstenfeld in 1964 and popularized by Donald Knuth in The Art of Computer Programming, walks the list from the last position to the first. At each step it swaps the current item with a randomly chosen item from the remaining unshuffled portion — moving each "struck" item to the end of the list by swapping it with the last unstruck one, rather than maintaining a second output list. That in-place swap is what dropped the running time from the original method's slower approach to a single pass through the array.
The result, per Wikipedia, is that the algorithm "produces an unbiased permutation: every permutation is equally likely." That guarantee is the whole point. A 12-team league has 12! — 479,001,600 — possible draft orders. A correctly implemented Fisher–Yates shuffle gives every one of those roughly 479 million orders the same probability of coming up. A random-sort shuffle does not, and neither does any other shortcut that hasn't been shown to be uniform.
RankFantasy's own Draft Order Randomizer uses Fisher–Yates for exactly this reason: it walks the league roster from the last slot to the first and swaps in a randomly drawn remaining team at every step — the same mechanism Durstenfeld described.
Why the random number, not just the algorithm, has to be fair too
Getting the shuffle algorithm right solves half the problem. The other half is the random number generator deciding each swap, which has its own, subtler way of producing lopsided results — modulo bias.
Computers don't generate a random number "from 0 to 11" directly. They generate a random integer across a much larger range (commonly 32 bits — 0 to roughly 4.29 billion) and squeeze it into the range you actually need with the modulo operator (%). The catch is that 4.29 billion doesn't divide evenly by every number you might need — 11 teams, 12, 13 — so the leftover values at the top of the generator's range get remapped disproportionately onto the low end of your target range, skewing some slots to come up more often than others.
This isn't a concern confined to blog posts and computer-science coursework. Ethereum's own beacon_chain reference implementation shipped a shuffle function with exactly this bug, tracked publicly as GitHub issue #57. The issue lays out the mechanism directly, with the simplest possible case: generating a random value from 0–3 and needing 0–2, calling rng() % 3 maps both 0 and 3 onto 0 — so 0 comes up more often than 1 or 2, purely from the arithmetic, with no malicious intent required. The fix that shipped calculates the largest clean multiple of the target range below the generator's maximum value and discards ("rejects") any draw that lands above it before applying the modulo — a technique called rejection sampling.
RankFantasy's randomizer applies the same correction. It draws from the browser's cryptographic random number source (crypto.getRandomValues) rather than Math.random(), and rejects any draw that would fall in the biased leftover range before taking the modulo — the identical rejection-sampling fix Ethereum's own shuffle bug required, applied here for the same reason: so a 13-team league doesn't quietly favor the last slot in the draw order any more than a 12-team one does.
What this actually changes for your draft
None of this changes who wins your league. It changes whether the manager who ends up picking first genuinely had the same odds as everyone else — and whether you can check that claim instead of taking it on faith.
A tool that documents its algorithm lets you verify the claim rather than trust a label. If a "random" draft order generator doesn't say what shuffle it runs or what random source feeds it, there's no way to know whether your league's pick order came from a fair 1-in-479-million draw, or from a biased shortcut that happened to look fine this one time.
Frequently asked questions
Does using Math.random() instead of crypto.getRandomValues() make a shuffle unfair?
Not by itself. Math.random() combined with a correctly implemented Fisher–Yates shuffle and proper rejection sampling still produces a uniform distribution over many draws, because Math.random() is itself uniformly distributed across its own range. Where things actually go wrong is naive modulo application on top of any generator, or a sort-based shuffle used in place of Fisher–Yates. Cryptographic randomness matters more for unpredictability — making a single draw harder to game or reverse-engineer in advance — than for statistical fairness across repeated draws.
How many possible draft orders are there in a 12-team league?
12 factorial (12!), which is 479,001,600. A correct Fisher–Yates shuffle gives each of those roughly 479 million orders identical probability. A biased shuffle method doesn't necessarily produce a wildly different-looking order on any given draw — but it can systematically over- or under-represent specific slots across many draws, most commonly the first and last positions.
Can I tell if a draft order tool is biased just by using it a few times?
No. Bias in a shuffle algorithm is a statistical property that only shows up over a large number of trials — thousands of draws, not a handful. The only reliable way to know is whether the tool documents, or its source code shows, that it uses a provably unbiased method like Fisher–Yates paired with correctly bounded random draws.
Is the sort(() => Math.random() - 0.5) shuffle really that common?
Yes. It's one of the most frequently copy-pasted shuffle snippets in web development, because it reads as intuitive and runs without throwing an error. The bias it introduces is invisible in casual testing and only shows up under large-sample statistical analysis, which is exactly why it keeps getting reused despite being a documented, known problem.
All content is for fantasy football informational purposes only — not betting, DFS, or financial advice. Data sourced from nflverse (CC-BY 4.0) and Sleeper.