172 lines
8.0 KiB
C#
172 lines
8.0 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace SLSUtilities.General
|
||
{
|
||
/// <summary>
|
||
/// 可复现的种子随机数管理器。
|
||
/// 从一个主种子 (master seed) 派生出各子系统独立的 <see cref="Random"/>,
|
||
/// 确保同一种子下所有随机结果完全可复现,且各子系统之间互不干扰。
|
||
/// <para/>
|
||
/// <b>核心概念:</b>
|
||
/// <list type="bullet">
|
||
/// <item>
|
||
/// <term>Channel</term>
|
||
/// <description>
|
||
/// 通过字符串 domain 派生的固定 RNG,适合生命周期与 Randomizer 相同的子系统。
|
||
/// 同一 domain 始终返回同一个 <see cref="Random"/> 实例。
|
||
/// </description>
|
||
/// </item>
|
||
/// <item>
|
||
/// <term>Fork</term>
|
||
/// <description>
|
||
/// 通过任意 key 派生的一次性 RNG,每次调用都返回新实例。
|
||
/// 适合与特定数据绑定(坐标、ID 等),同一 key 始终产生相同序列。
|
||
/// </description>
|
||
/// </item>
|
||
/// <item>
|
||
/// <term>Next</term>
|
||
/// <description>
|
||
/// 自增计数器派生的 RNG,每次调用都返回不同的实例。
|
||
/// 适合无特定绑定数据的通用场景。
|
||
/// </description>
|
||
/// </item>
|
||
/// </list>
|
||
///
|
||
/// <para><b>使用示例:</b></para>
|
||
///
|
||
/// <code>
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// // 1. 创建 Randomizer(通常在一局 Run 开始时)
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// //
|
||
/// // 传入种子字符串,为空则自动生成
|
||
/// var randomizer = new Randomizer("my-seed-123");
|
||
/// // randomizer.Seed → "my-seed-123"
|
||
///
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// // 2. Channel — 固定子系统 RNG(地图生成、战利品表等)
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// //
|
||
/// // 首次调用创建,后续调用返回同一实例
|
||
/// Random mapRng = randomizer.Channel("Map");
|
||
/// Random lootRng = randomizer.Channel("Loot");
|
||
/// // randomizer.Channel("Map") == mapRng → true(同一引用)
|
||
///
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// // 3. Fork — 按 key 派生一次性 RNG(节点坐标、房间 ID 等)
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// //
|
||
/// // 机械台/商店:同一坐标 → 同一随机序列
|
||
/// Random nodeRng = randomizer.Fork(nodePosition.x, nodePosition.y);
|
||
/// mechanicalTable.Setup(nodeRng);
|
||
///
|
||
/// // 也可以用字符串 key
|
||
/// Random bossRng = randomizer.Fork("BossRoom", floorIndex);
|
||
///
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// // 4. Next — 自增通用 RNG(无特定绑定的临时随机)
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// //
|
||
/// // 每次调用返回不同实例,但同一 seed 下第 N 次调用结果一致
|
||
/// Random rng1 = randomizer.Next(); // 第 1 次
|
||
/// Random rng2 = randomizer.Next(); // 第 2 次,与 rng1 不同
|
||
///
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// // 5. 实际游戏场景举例
|
||
/// // ═══════════════════════════════════════════════════════════
|
||
/// //
|
||
/// // Roguelite Run 开始:
|
||
/// // var runRng = new Randomizer(MainGameManager.Seed);
|
||
/// // RunMapData map = MapGenerator.Generate(config, runRng.Channel("Map"));
|
||
/// //
|
||
/// // 进入机械台节点 (3, 5):
|
||
/// // mechanicalTable.Setup(runRng.Fork(3, 5));
|
||
/// // → 稀有度 Roll + 装备 Roll 都由该 Fork 驱动
|
||
/// //
|
||
/// // 进入商店节点 (2, 4):
|
||
/// // logisticsCenter.Setup(runRng.Fork(2, 4));
|
||
/// // → 商品列表 + 价格 Roll 都由该 Fork 驱动
|
||
/// //
|
||
/// // 战斗中敌人掉落:
|
||
/// // Random dropRng = runRng.Channel("EnemyDrop");
|
||
/// // → 所有敌人掉落共享一个 Channel,保持掉落顺序一致
|
||
/// //
|
||
/// // 临时需要一个 RNG(不关心绑定):
|
||
/// // Random tempRng = runRng.Next();
|
||
/// </code>
|
||
/// </summary>
|
||
public sealed class Randomizer
|
||
{
|
||
/// <summary>本实例使用的种子字符串。</summary>
|
||
public string Seed { get; }
|
||
|
||
private readonly int _masterSeed;
|
||
private int _counter;
|
||
private readonly Dictionary<string, Random> _channels = new Dictionary<string, Random>();
|
||
|
||
/// <summary>
|
||
/// 使用指定种子创建。传入 null 或空字符串则自动生成 8 位随机种子。
|
||
/// </summary>
|
||
public Randomizer(string seed = null)
|
||
{
|
||
if (string.IsNullOrEmpty(seed))
|
||
{
|
||
seed = Guid.NewGuid().ToString("N")[..8];
|
||
}
|
||
|
||
Seed = seed;
|
||
_masterSeed = seed.GetHashCode();
|
||
}
|
||
|
||
// ─────────────────── Channel ───────────────────
|
||
|
||
/// <summary>
|
||
/// 获取或创建指定 domain 的固定 Channel RNG。
|
||
/// 同一 domain 始终返回同一个 <see cref="Random"/> 实例。
|
||
/// </summary>
|
||
/// <param name="domain">子系统标识,如 "Map"、"Loot"、"EnemyDrop"。</param>
|
||
public Random Channel(string domain)
|
||
{
|
||
if (_channels.TryGetValue(domain, out Random existing))
|
||
{
|
||
return existing;
|
||
}
|
||
|
||
Random rng = new Random(HashCode.Combine(_masterSeed, domain));
|
||
_channels[domain] = rng;
|
||
return rng;
|
||
}
|
||
|
||
// ─────────────────── Fork ───────────────────
|
||
|
||
/// <summary>
|
||
/// 通过任意数量的 key 派生一次性 RNG。
|
||
/// 同一组 key 始终产生相同的随机序列。
|
||
/// </summary>
|
||
/// <param name="keys">用于派生种子的 key(坐标、ID、字符串等)。</param>
|
||
public Random Fork(params object[] keys)
|
||
{
|
||
int derived = _masterSeed;
|
||
foreach (object key in keys)
|
||
{
|
||
derived = HashCode.Combine(derived, key);
|
||
}
|
||
|
||
return new Random(derived);
|
||
}
|
||
|
||
// ─────────────────── Next ───────────────────
|
||
|
||
/// <summary>
|
||
/// 创建一个自增计数器派生的通用 RNG。
|
||
/// 每次调用返回不同实例,适用于无特定绑定的通用场景。
|
||
/// </summary>
|
||
public Random Next()
|
||
{
|
||
_counter++;
|
||
return new Random(HashCode.Combine(_masterSeed, _counter));
|
||
}
|
||
}
|
||
}
|