tower / files / src / tour / plan.ts
plan.ts
99a2821b 2026-09-29 Pavel Safronov | 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 | } |