# `$.random` — детерминированная случайность

Генератор случайных чисел с зерном («seed») и шум значений для рельефа,
биомов и дрожания. Аналог `RandomNumberGenerator` + `FastNoiseLite` из Godot 4.

Детерминизм — главное требование: одна и та же последовательность вызовов
после `$.random.seed(n)` даёт одну и ту же последовательность чисел. На этом
стоят юнит-тесты, повторные прогоны и режим `--fixed-dt`: два запуска с одним
`--seed` обязаны совпасть кадр в кадр.

База — `makeRandom()` из ядра (mulberry32), тот же генератор, которым уже
пользуются демки через `$.random.range/int/next/chance`. Подсистема расширяет
его, а не заменяет: последовательности существующих игр не меняются.

```js
$.random.seed(7);
$.random.int(1, 6);                       // кубик, 1..6 включительно
$.random.pick(['меч', 'щит', 'зелье']);
$.random.shuffle(deck);
$.random.chance(0.25);                    // четверть случаев — true
$.random.weighted([{ value: 'меч', weight: 1 }, { value: 'мусор', weight: 9 }]);
```

Проверка без движка:

```bash
build/_deps/quickjs-build/qjs tests/js/random_test.mjs
```

---

## 1. Генератор

### `$.random.seed(n) → $.random`; `$.random.seed() → number`

Ставит зерно и перезапускает последовательность с начала. Без аргумента
возвращает текущее зерно. `$.random.seed(7).next()` работает цепочкой.

Зерно 0 ядро подменяет константой `0x9e3779b9` — это по-прежнему
детерминированно, но если нужен именно «нулевой» отсчёт, берите `seed(1)`.

| Метод | Назначение |
|---|---|
| `next() → number` | Следующее число в `[0, 1)` |
| `range(a, b) → number` | Число в `[a, b)`; при `a > b` диапазон переворачивается |
| `int(a, b) → number` | Целое в `[a, b]` — **обе границы включительно** |
| `pick(list) → any` | Случайный элемент; пустой список → `undefined` |
| `chance(p) → bool` | `true` с вероятностью `p` (`chance(1)` — всегда, `chance(0)` — никогда) |
| `shuffle(list) → array` | Копия списка в случайном порядке (исходный не меняется) |
| `gaussian() → number` | Нормальное распределение: среднее 0, отклонение 1 |
| `weighted(list) → any` | Выбор с весами (форматы ниже) |
| `noise1D(x, seed?) → number` | Значение-шум 1D в `[0, 1)` |
| `noise2D(x, y, seed?) → number` | Значение-шум 2D в `[0, 1)` |

```js
// Волна врагов: состав и позиции воспроизводимы при одном --seed.
$.random.seed(level.seed);
const type = $.random.weighted([
    { value: 'goblin', weight: 10 },
    { value: 'orc', weight: 4 },
    { value: 'dragon', weight: 1 },
]);
const x = $.random.range(0, arena.w);
```

### `$.random.weighted(list)`

Форматы элемента списка:

| Запись | Значение | Вес |
|---|---|---|
| `{ value: 'меч', weight: 3 }` | `'меч'` | `3` |
| `{ v: 'меч', w: 3 }` | `'меч'` | `3` (короткая запись) |
| `['меч', 3]` | `'меч'` | `3` |
| `'меч'` | `'меч'` | `1` |

Веса `<= 0` не участвуют в выборе. Если сумма всех весов нулевая, выбор
равномерный — функция не делит на ноль и не падает. Пустой список даёт
`undefined`.

## 2. Шум значений

`noise1D`/`noise2D` — это **значение-шум** (value noise) на целочисленной
решётке со сглаживанием: гладкие холмы и биомы без таблиц и без внешних
файлов. Возвращают число в `[0, 1)`.

Важное свойство: шум — **чистая функция координаты**, он не зависит от
состояния `$.random` и от порядка вызовов. Иначе мир менялся бы от того,
сколько раз за кадр кто-то кинул кубик. Необязательный второй аргумент
(`seed`) сдвигает решётку — из него делают разные слои и «континенты».

```js
// Рельеф: высота клетки — сумма двух октав шума.
const h = 0.6 * $.random.noise2D(x / 64, y / 64)
        + 0.4 * $.random.noise2D(x / 16, y / 16, 777);

// Дрожание камеры на «шторме» — тоже шум, а не случайность.
cam.shake_x = ($.random.noise1D(time * 3) - 0.5) * 4;
```

## 3. Совместимость и советы

* Существующие вызовы `$.random.next()`, `.range()`, `.int()`, `.pick()`,
  `.chance()` продолжают работать без изменений — это методы того же
  генератора ядра.
* Один seed — одна последовательность: не подмешивайте `Math.random()` в
  игровую логику, иначе воспроизводимость теряется.
* Для независимых потоков случайности (например, генерация уровня и бой)
  заводите свои генераторы: `makeGenerator(seed)` экспортируется из
  `random.js` и не трогает общий `$.random`.
* Зерно по умолчанию берётся из `engine.seed` (его задаёт ключ `--seed`),
  без него — `12345`.

## 4. Установка

```js
import { installRandom, makeGenerator } from './random.js';
installRandom($);       // $.random = makeGenerator(engine.seed ?? 12345)
```

Чистые функции экспортируются наружу и проверяются qjs без движка:
`makeGenerator`, `shuffle`, `gaussian`, `weightedPick`, `hash01`, `noise1D`,
`noise2D`. `shuffle(list, rng)` и `weightedPick(list, rng)` принимают
генератор явно — так их можно проверить на фиксированном зерне.

## 5. Ограничения

| Чего нет | Почему / что делать |
|---|---|
| `Math.random()` как источник | Он невоспроизводим; для «настоящей» случайности берите `Date.now()` как зерно: `$.random.seed(Date.now())` |
| Симплекс-шума и fBm «из коробки» | Есть только value noise; октавы складывайте сами (см. пример) |
| Сохранения состояния ГПСЧ (`save`/`restore`) | Состояние не сериализуется: сохраняйте зерно и номер вызова либо генерируйте всё заранее |
| Гарантий криптостойкости | Генератор игровой (mulberry32), для паролей и токенов не годится |
| Взвешенного выбора без замены | `weighted` всегда с заменой; для «выдать 3 разных предмета» — `shuffle` + `weighted` по остатку |
