With three names, the unevenness can be computed directly
There are two ways to shuffle a list to decide who speaks first: pick one of the remaining positions each round, or give the sort a random comparator. The second is shorter, but it was never a draw to begin with. With three or four elements the paths are few enough to walk one by one, so the distribution is computed rather than observed.
The live Text Shuffle tool uses the first, with this loop body from lines 80 to 88 of the template:
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;
}
Line 81 walks i down from n-1 to 1, line 82 draws j uniformly from the i+1 slots between 0 and i, and lines 83 to 85 swap all three values with no i !== j guard, consuming n-1 random numbers per run. One sequence of draws corresponds to n × (n-1) × ... × 2 = n! equally likely paths, each with probability 1/n!, few enough to enumerate (for how steep that gets, see permutations and combinations). Enumeration gives: for n=3 the 6 paths land on 6 permutations at 16.67% each, and for n=4 the 24 paths land on 24 permutations at 4.17% each, with a max/min ratio of 1.0000. The enumeration feeds forced values into line 82: 647 in total, with 0 failed assertions.
The random comparator fails because the comparison order is not drawn by lot
The textbook counterexample is arr.sort(() => Math.random() - 0.5). The template contains .sort( 0 times, yet it is the most common version in tutorials. A sort is not a lottery machine: the engine decides which two elements to compare first. For short arrays V8 takes TimSort's binary insertion branch, and when inserting the k-th element the number of comparisons depends on the answers already given, so with the same 3 elements the count can be 2, 3 or 4.
Enumerating it takes one abstraction: the comparator ignores its arguments and answers only less-than or greater-than, so the c-th comparison is a two-way branch with path probability 2^(-c), and bringing leaves at the same depth to a common denominator gives each permutation's probability. For n=3 there are 8 comparison paths with common denominator 2^4 = 16; for n=4 there are 34 paths, tree depths of 3, 5 and 6, and common denominator 2^6 = 64.
Figure 1 | Probability of each of the 6 permutations under the random comparator with 3 elements, from exhaustive enumeration of all 8 comparison paths rather than sampling; the dashed line is 16.67% from enumerating the 6 Fisher-Yates draw paths.
Figure 2 | The same code on 4 elements splits into 6 buckets totalling 24 permutations, from exhaustive enumeration of all 34 comparison paths rather than sampling; the dashed line is 4.17% from enumerating the 24 Fisher-Yates draw paths.
With 3 elements the original order 0 1 2 alone takes 37.50%, 2.2500 times the uniform value, while the three coldest buckets sit at 6.25% each, a max/min ratio of 6.0000; with 4 elements it is 18.75% against 1.56%, a gap of 12.0000 times. For n=5 the 172 paths give 48.0000 times and for n=6 the 1034 paths give 192.0000 times, while the uniform value is only 1/720 = 0.14%.
Three limits. On these four lengths the reachable permutations found by enumeration are 6/6, 24/24, 120/120 and 720/720, with 0 permutations at probability 0, so we cannot claim some orders never come out, only that the result is uneven. The figures come from exhaustive enumeration against V8's Array.prototype.sort; Chrome, Edge and Node share the implementation, while Firefox and Safari differ. Running many times only corroborates: the 100000 runs used mulberry32 with seed 20260213, giving 1.0696 against 12.8487 at n=4, which matches the enumeration. When the weights differ you need weighted drawing instead.
After you paste, which characters stay nailed in place matters even more
The hint on line 42 says every character in the text is shuffled at random, yet keep spaces and line breaks on line 24 is checked by default, and line 113 tests input[i] === ' ' || input[i] === '\n' || input[i] === '\t': the anchors are exactly the three code points U+0020, U+000A and U+0009, stored separately first, then only the remaining characters are shuffled and pasted back at their original indexes.
Input ab[SP]cd[LF]ef[TAB]gh[SP]ij Output ac[SP]dg[LF]bh[TAB]ji[SP]ef
The default setting consumes 9 random numbers to shuffle those 10 code units; untick it and line 138's input.split("") takes over, rewriting 3 of the 4 anchors. Line 113 also excludes \r: for the input aa[SP]bb[CR][LF]cc[SP]dd with the box checked, the carriage return still moves from index 5 to 11 while the LF stays at index 6, so the line ending is scattered; U+00A0 and U+3000 also take part in the shuffle. Line mode is a separate path: line 152 splits on LF only, so three lines including a blank one are 3 lines and 2 random numbers, and with remove blank lines before shuffling ticked, line 161 reports the filtered total lines=2. It reads only the control on line 149; changing the hidden ts-times to 50 has no effect.
The shuffle count field: the fallback rule is not what you would expect
Lines 105 and 149 both use parseInt(…, 10) || 1, which only catches 0, empty and NaN; negative numbers pass straight through:
| Value entered | Actual rounds | Random numbers | Stats display |
|---|---|---|---|
| 0 | 1 | 5 | shuffle count=1 |
| left blank | 1 | 5 | shuffle count=1 |
| 2.9 | 2 | 10 | shuffle count=2 |
| -5 | 0 | 0 | shuffle count=-5 |
| 1000 | 1000 | 9000 | shuffle count=1000 |
The random number column is t*(n-1), with 6 code units in the first four rows and 10 in the last. -5 is the awkward one: t < times on line 119 is simply false, so there are 0 rounds, 0 random numbers and the output equals the input, yet the UI still shows shuffle count=-5. The other end is unchecked too: max="100" on lines 28 and 35 is only an HTML attribute, with 0 range checks in the JS.
With no seed input, reproducing a result is impossible
seed, crypto, getRandomValues and localStorage appear on 0 lines of the template, and the only source of randomness is Math.random() on line 82. With the same input and the same settings, clicking again always changes the result: the button on line 49 just runs another times rounds, it does not change the seed. So the claim that shuffling characters adds randomness to a password does not hold: it only permutes the characters the user supplied and introduces no new entropy, and Math.random is xorshift128+, with 128 bits of state and 53 bits of output, which is not unpredictable.
Q: Why are two clicks the same or different? Can the result be reproduced?
When only one item can move, the result must be the same: a single character uses 0 random numbers, six identical characters still consume 5 while the output does not change, and three spaces are all anchors; with n=2 there is still only a 1/2 chance the order changes. Reproducing is not an option either, since there are 0 seed parameters.
Q: What does keep spaces and line breaks actually preserve?
The positions of spaces, line breaks and tabs, at the cost of fewer characters being shuffled; a carriage return, U+000D, is not on the list and is moved anyway, so saying every character is inaccurate.
Q: Is the uniformity verified by running it many times?
No, both sides are enumerated: one walks every draw sequence of line 82, the other every comparison branch, and the 100000 runs are only a cross-check.