Live model · Spotify shuffle

See how it actually works. Then change one thing.

Built for people who want to understand, not memorize. Every visual here is real code running.

StopSeed0x3F2ASongs16
  1. D1
  2. A3
  3. B4
  4. A1
  5. B3
  6. B1
  7. A4
  8. A6
  9. D2
  10. C1
  11. C2
  12. A2
  13. D3
  14. B2
  15. A5
  16. C3
  • CH1 Artist A
  • CH2 Artist B
  • CH3 Artist C
  • CH4 Artist D
Same artist back-to-back3

fisherYates() on 16 songs, seed 0x3F2A. Done: the same artist plays back to back 3 times. Outlined pairs are the repeats.

Shuffle
New seed
2,000 shuffles of this playlistPure random avg 3.37Spread out avg 0.47
Table
2,000 shuffles of this playlist
Same artist back to back, per shufflePure randomSpread out
01.5% (30)57.8% (1155)
19.1% (182)38.0% (759)
218.9% (378)4.3% (86)
326.4% (528)0.0% (0)
422.2% (444)0.0% (0)
513.1% (262)0.0% (0)
65.8% (116)0.0% (0)
72.4% (47)0.0% (0)
8+0.7% (13)0.0% (0)

The rule

We didn't animate it. We ran it.

That play order came from the function below. The page calls it every time you press Shuffle again or move the slider. Nobody drew it, and every lesson on codebrew works this way: the frames are what the code produced.

Every change to it is checked against a plain-Python copy of the same algorithm, on hundreds of random playlists. They have to agree on every order.

The codeThis is the function that shuffled the playlist.

The runNow it runs, one line at a time.

The resultSame order the hero showed. Nobody drew it.

StopSeed0x3F2AStep31/31
  1. D1
  2. A3
  3. B4
  4. A1
  5. B3
  6. B1
  7. A4
  8. A6
  9. D2
  10. C1
  11. C2
  12. A2
  13. D3
  14. B2
  15. A5
  16. C3
  • CH1 Artist A
  • CH2 Artist B
  • CH3 Artist C
  • CH4 Artist D
Same artist back-to-back3

Done: the same artist plays back to back 3 times.

src/sim/shuffle/fisher-yates.ts
function fisherYates<T>(
  songs: readonly T[],
  rand: () => number,
  trace?: Trace<T>
): T[] {
  const order = songs.slice()
  for (let i = order.length - 1; i > 0; i--) {
    const j = Math.floor(rand() * (i + 1))
    trace?.("pick", i, j, order) // to the stage
    ;[order[i], order[j]] = [order[j]!, order[i]!]
    trace?.("swap", i, j, order) // to the stage
  }
  return order
}
Printed from the file that ran
Spread out runs spreadShuffle()
src/sim/shuffle/spread-shuffle.ts
function spreadShuffle<T extends { artist: string }>(
  songs: readonly T[],
  rand: () => number,
  trace?: (artist: string, m: number, offset: number, placed: readonly { song: T; pos: number }[]) => void
): T[] {
  // Space each artist's songs evenly across the playlist,
  // starting at a random offset, with a little jitter.
  const byArtist = new Map<string, T[]>()
  for (const song of songs) {
    if (!byArtist.has(song.artist)) byArtist.set(song.artist, [])
    byArtist.get(song.artist)!.push(song)
  }

  const placed: { song: T; pos: number }[] = []
  for (const [artist, mine] of byArtist) {
    const m = mine.length
    const order = fisherYates(mine, rand)
    const offset = rand() / m
    order.forEach((song, i) => {
      const jitter = (rand() - 0.5) * (0.2 / m)
      placed.push({ song, pos: offset + i / m + jitter })
    })
    trace?.(artist, m, offset, placed) // report the step to the stage
  }
  placed.sort((a, b) => a.pos - b.pos)
  return placed.map((p) => p.song)
}

Requests

What should we explain next?

Name a thing you use and wonder about. The most-requested ones get built as models, checked, and published.

Prepping for a coding interview?

The same lessons, organized into tracks and company plans.