Files
2026-05-10 11:47:55 -04:00

172 lines
8.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}
}