fuzzyworld

tower / files / src / tour / plan.ts

plan.ts

2.3 KB · history · blame

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 */
9import { mulberry32 } from '../terrain/noise.js'
10
11export const LEG_MIN_S = 12
12export const LEG_MAX_S = 18
13
14/** A random number source: a fresh one per tour, or a fixed one for the screenshots. */
15export const tourRng = (seed: number): (() => number) => mulberry32(seed >>> 0)
16
17/** How long a leg lasts, from a draw in [0, 1). */
18export 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 */
27export 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}