Случайные числа: seed, состояние и взвешенный выбор
Воспроизводимое поле, независимые генераторы, восстановление State и проверка весов.
Повторить генерацию
Добавьте RandomField2D.cs к корню Entity2D отдельного C#-уровня 1.0.0. Задайте окно/viewport 640×420, соберите и нажмите F6. Поле из 96 маркеров повторяется при Seed = 42 и том же порядке вызовов. Смена Seed меняет поле. Это выбор точек, не создание физических игровых объектов.
RandomField2D.cs
using System;
using Reckfell;
public partial class RandomField2D : Entity2D
{
[Export] public ulong Seed { get; set; } = 42;
[Export] public int Count { get; set; } = 96;
public int PointCount => _points.Length;
public ulong FinalState { get; private set; }
private Vector2[] _points = [];
private int[] _kinds = [];
private Label? _status;
public Vector2 GetPoint(int index) => _points[index];
public int GetKind(int index) => _kinds[index];
public static bool ValidWeights(float[]? weights)
{
if (weights is null || weights.Length == 0) return false;
float sum = 0;
foreach (float weight in weights)
{
if (!float.IsFinite(weight) || weight < 0) return false;
sum += weight;
}
return float.IsFinite(sum) && sum > 0;
}
public static int Choose(RandomNumberGenerator rng, float[] weights)
{
if (!ValidWeights(weights)) throw new ArgumentException("Weights require a finite positive sum", nameof(weights));
return checked((int)rng.RandWeighted(weights));
}
public Error Generate(ulong seed, int count)
{
if (count < 1 || count > 256) return Error.InvalidParameter;
Vector2[] points = new Vector2[count];
int[] kinds = new int[count];
using var rng = new RandomNumberGenerator { Seed = seed };
float[] weights = [1, 3, 6];
for (int i = 0; i < count; i++)
{
points[i] = new Vector2(rng.RandfRange(40, 600), rng.RandfRange(125, 380));
kinds[i] = Choose(rng, weights);
}
FinalState = rng.State;
_points = points;
_kinds = kinds;
Seed = seed;
Count = count;
if (_status is not null) _status.Text = "Seed = " + seed + "; n = " + count;
QueueRedraw();
return Error.Ok;
}
public override void _Ready()
{
bool ru = TranslationServer.GetLocale().StartsWith("ru");
var title = new Label { Position = new Vector2(24, 20), Text = ru
? "Один seed и порядок вызовов — одно поле" : "Same seed and call order, same field" };
title.AddThemeFontSizeOverride("font_size", 22); AddChild(title);
_status = new Label { Position = new Vector2(24, 58) }; AddChild(_status);
AddChild(new Label { Position = new Vector2(24, 85), Text = ru
? "Размеры маркеров: веса 1 : 3 : 6; это не точные количества."
: "Marker sizes use weights 1 : 3 : 6, not exact counts." });
if (Generate(Seed, Count) != Error.Ok) ReckfellApi.PrintErr("RandomField2D: count must be 1..256");
}
public override void _Draw()
{
DrawRect(new Rect2(0, 0, 640, 420), new Color(0.035f, 0.04f, 0.05f));
for (int i = 0; i < _points.Length; i++)
DrawCircle(_points[i], 2 + 2 * _kinds[i], _kinds[i] == 0 ? Colors.White : new Color(0.25f, 0.46f, 1));
}
}У каждого RandomNumberGenerator своё состояние. Конструктор вызывает Randomize; пример сразу присваивает Seed, чтобы старт был определённым. Вызовы чужого экземпляра не меняют последовательность этого генератора. Для независимых игровых систем удобно иметь отдельные экземпляры и контролировать порядок их использования. Глобальная случайная последовательность может измениться от случайного вызова в другом месте проекта.
Seed и State решают разные задачи
Seed запускает последовательность с начала. State продолжает её с сохранённого места. Сохраните State после нужного вызова, получите два числа, верните State и повторите эти же два вызова — значения должны совпасть. Этот сценарий проверен в движке.
Если сохраняете оба значения, при восстановлении сначала установите Seed, затем State: присваивание Seed сбрасывает внутреннее состояние. Не подменяйте State произвольным seed. Для повторения нужны та же реализация генератора, параметры, порядок вызовов и ветвления. Совпавший seed сам по себе не доказывает синхронность сетевой игры или одинаковую последовательность на будущих версиях.
Диапазоны и веса
| Метод | Значение |
|---|---|
| RandiRange(a, b) | Целое с включёнными границами a и b |
| Randf / RandfRange | float в заданном диапазоне; не используйте как готовый индекс массива |
| Randfn(mean, deviation) | Нормальное распределение, не ограниченный диапазон |
| RandWeighted(weights) | Индекс выбранного веса, не сам вес |
Для случайного индекса непустого массива используйте RandiRange(0, Length − 1). Randfn может выйти за mean ± deviation; ограничение Clamp меняет распределение. Для весов 1 : 3 : 6 относительные вероятности — 10%, 30%, 60%, а не точные количества в каждой сотне.
ValidWeights примера отвергает пустой массив, отрицательные/нефинитные веса, переполнение суммы и сумму 0. Нативный RandWeighted при пустом/отрицательном массиве сообщает ошибку и возвращает −1; при нулевой сумме выбора нет. Не используйте −1 как индекс. Нулевой вес в валидном наборе исключает этот вариант.
Генератор предназначен для игровой случайности. Для секретов и токенов нужен криптографический генератор. Проверки документации подтверждают диапазоны, повторение и отдельные случаи весов; они не являются сертификацией статистического качества или гарантией межплатформенного совпадения битов.