tower / files / src / tour / plan.ts
plan.ts
| 1 | /** |
| 2 | * The order the tour flies its legs in, and how long each lasts. See world-generation.md §19. |
| 3 | * |
| 4 | * Variety is the point (D-085), so the tour is drawn like cards from a shuffled deck: every |
| 5 | * leg is flown once before any is flown twice, no two in a row are the same kind of craft, |
| 6 | * and each lasts its own twelve to eighteen seconds. When the deck runs out it is shuffled |
| 7 | * again, and the new deck never opens with the craft the old one closed on. |
| 8 | */ |
| 9 | import { mulberry32 } from '../terrain/noise.js' |
| 10 | |
| 11 | export const LEG_MIN_S = 12 |
| 12 | export const LEG_MAX_S = 18 |
| 13 | |
| 14 | /** A random number source: a fresh one per tour, or a fixed one for the screenshots. */ |
| 15 | export const tourRng = (seed: number): (() => number) => mulberry32(seed >>> 0) |
| 16 | |
| 17 | /** How long a leg lasts, from a draw in [0, 1). */ |
| 18 | export const legSeconds = (u: number): number => LEG_MIN_S + (LEG_MAX_S - LEG_MIN_S) * u |
| 19 | |
| 20 | /** |
| 21 | * A shuffled order of `items`, with no two neighbours of the same kind where that can be |
| 22 | * helped — nor of the same `theme`, so two legs about one waterfall are not flown back to |
| 23 | * back — and the first not of kind `after`. At each step the next card is the first of the |
| 24 | * shuffled deck that differs from the last one laid, unless one kind has so many cards left |
| 25 | * that it must be laid now or end up laid twice running, and then it is. |
| 26 | */ |
| 27 | export function deal<T>(items: readonly T[], kind: (x: T) => string, rng: () => number, after?: string, |
| 28 | theme: (x: T) => string = () => ''): T[] { |
| 29 | const deck = [...items] |
| 30 | for (let i = deck.length - 1; i > 0; i--) { |
| 31 | const j = Math.floor(rng() * (i + 1)) |
| 32 | ;[deck[i], deck[j]] = [deck[j]!, deck[i]!] |
| 33 | } |
| 34 | const out: T[] = [] |
| 35 | let last = after |
| 36 | let lastTheme: string | null = null |
| 37 | while (deck.length) { |
| 38 | const counts = new Map<string, number>() |
| 39 | for (const x of deck) counts.set(kind(x), (counts.get(kind(x)) ?? 0) + 1) |
| 40 | let must: string | null = null |
| 41 | for (const [k, c] of counts) if (k !== last && c > (deck.length - 1) / 2) must = k |
| 42 | const fits = (x: T) => (must !== null ? kind(x) === must : kind(x) !== last) |
| 43 | let i = deck.findIndex((x) => fits(x) && (!theme(x) || theme(x) !== lastTheme)) |
| 44 | if (i < 0) i = deck.findIndex(fits) |
| 45 | if (i < 0) i = 0 |
| 46 | const [x] = deck.splice(i, 1) |
| 47 | out.push(x!) |
| 48 | last = kind(x!) |
| 49 | lastTheme = theme(x!) |
| 50 | } |
| 51 | return out |
| 52 | } |