如何在 C# 中保存随机生成器的状态

本文关键字:状态 随机 保存 | 更新日期: 2023-09-27 18:36:18

出于测试目的,我正在用给定的种子创建随机数(即不基于当前时间)。

因此,整个程序是确定性的。

如果发生什么事,我希望能够在事件发生前不久快速恢复一个点。

因此,我需要能够将System.Random恢复到以前的状态。

有没有办法提取可用于重新创建随机生成器的种子?

如何在 C# 中保存随机生成器的状态

根据这里给出的答案,我写了一个小类来帮助保存和恢复状态。

void Main()
{
    var r = new Random();
    Enumerable.Range(1, 5).Select(idx => r.Next()).Dump("before save");
    var s = r.Save();
    Enumerable.Range(1, 5).Select(idx => r.Next()).Dump("after save");
    r = s.Restore();
    Enumerable.Range(1, 5).Select(idx => r.Next()).Dump("after restore");
    s.Dump();
}
public static class RandomExtensions
{
    public static RandomState Save(this Random random)
    {
        var binaryFormatter = new BinaryFormatter();
        using (var temp = new MemoryStream())
        {
            binaryFormatter.Serialize(temp, random);
            return new RandomState(temp.ToArray());
        }
    }
    public static Random Restore(this RandomState state)
    {
        var binaryFormatter = new BinaryFormatter();
        using (var temp = new MemoryStream(state.State))
        {
            return (Random)binaryFormatter.Deserialize(temp);
        }
    }
}
public struct RandomState
{
    public readonly byte[] State;
    public RandomState(byte[] state)
    {
        State = state;
    }
}

您可以在 LINQPad 中测试此代码。

这是我想到的:

基本上,它提取私有种子数组。您只需要小心恢复"未共享"阵列即可。

var first = new Random(100);
// gain access to private seed array of Random
var seedArrayInfo = typeof(Random).GetField("SeedArray", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var seedArray = seedArrayInfo.GetValue(first) as int[];
var other = new Random(200); // seed doesn't matter!
var seedArrayCopy = seedArray.ToArray(); // we need to copy since otherwise they share the array!
seedArrayInfo.SetValue(other, seedArrayCopy);

for (var i = 10; i < 1000; ++i)
{
    var v1 = first.Next(i);
    var v2 = other.Next(i);
    Debug.Assert(v1 == v2);
}
我知道

这个问题已经得到了回答,但是,我想提供我自己的实现,目前正用于我正在创建的游戏。本质上,我使用 .NET的随机.cs。我不仅添加了更多功能,而且还添加了一种将当前生成器状态保存和加载到仅包含 59 个索引的数组中和从中加载的方法。最好这样做,而不是其他一些评论建议"迭代 x 次以手动恢复状态。这是一个坏主意,因为在RNG重型游戏中,您的随机生成器状态理论上可能会进入数十亿次调用,这意味着根据他们的说法,您需要迭代十亿次才能在每次启动期间恢复上次游戏会话的状态。当然,这可能仍然只需要一秒钟,但在我看来它仍然太脏了,特别是当您可以简单地提取随机生成器的当前状态并在需要时重新加载它时,并且只占用 1 个数组(59 个内存索引)。

这只是一个想法,所以从我的代码中获取你想要的。

这是完整的来源,太大了,无法在这里发布:

格里莫尔随机.cs

对于任何只想实现该问题的人,我将在此处发布。

        public int[] GetState()
        {
            int[] state = new int[59];
            state[0] = _seed;
            state[1] = _inext;
            state[2] = _inextp;
            for (int i = 3; i < this._seedArray.Length; i++)
            {
                state[i] = _seedArray[i - 3];
            }
            return state;
        }
        public void LoadState(int[] saveState)
        {
            if (saveState.Length != 59)
            {
                throw new Exception("GrimoireRandom state was corrupted!");
            }
            _seed = saveState[0];
            _inext = saveState[1];
            _inextp = saveState[2];
            _seedArray = new int[59];
            for (int i = 3; i < this._seedArray.Length; i++)
            {
                _seedArray[i - 3] = saveState[i];
            }
        }

我的代码是完全独立的,除了DiceType枚举和OpenTK Vector3结构。这两个功能都可以删除,它将为您工作。

System.Random不是

密封的,它的方法都是虚拟的,因此您可以创建一个类来计算生成的数字数以跟踪状态,如下所示:

class StateRandom : System.Random
{
    Int32 _numberOfInvokes;
    public Int32 NumberOfInvokes { get { return _numberOfInvokes; } }
    public StateRandom(int Seed, int forward = 0) : base(Seed)
    {
        for(int i = 0; i < forward; ++i)
            Next(0);
    }
    public override Int32 Next(Int32 maxValue)
    {
        _numberOfInvokes += 1;
        return base.Next(maxValue);
    }
}

用法示例:

void Main()
{
    var a = new StateRandom(123);
    a.Next(100);
    a.Next(100);
    a.Next(100);
    var state = a.NumberOfInvokes;
    Console.WriteLine(a.Next(100));
    Console.WriteLine(a.Next(100));
    Console.WriteLine(a.Next(100));
    // use 'state - 1' to be in the previous state instead
    var b = new StateRandom(123, state);
    Console.WriteLine(b.Next(100));
    Console.WriteLine(b.Next(100));
    Console.WriteLine(b.Next(100));
}

输出:

81
73
4
81
73
4

还有另一种解决方案,即 (1) 避免需要记住所有以前生成的数字;(2)不涉及访问随机的私有字段;(3)不需要序列化;(4) 不需要像调用的那样多次循环回随机;(5) 不需要创建内置 Random 类的替代品。

诀窍是通过生成随机数,然后将随机数生成器重新设定为此值来获取状态。然后,在将来,始终可以通过将随机数生成器重新设定为此值来返回到此状态。换句话说,我们"刻录"随机数序列中的一个数字,以保存状态和重新播种。

实现如下。请注意,将访问 Generator 属性以实际生成数字。

public class RestorableRandom
{
    public Random Generator { get; private set; }
    public RestorableRandom()
    {
        Generator = new Random();
    }
    public RestorableRandom(int seed)
    {
        Generator = new Random(seed);
    }
    public int GetState()
    {
        int state = Generator.Next();
        Generator = new Random(state);
        return state;
    }
    public void RestoreState(int state)
    {
        Generator = new Random(state);
    }
}

这是一个简单的测试:

[Fact]
public void RestorableRandomWorks()
{
    RestorableRandom r = new RestorableRandom();
    double firstValueInSequence = r.Generator.NextDouble();
    int state = r.GetState();
    double secondValueInSequence = r.Generator.NextDouble();
    double thirdValueInSequence = r.Generator.NextDouble();
    r.RestoreState(state);
    r.Generator.NextDouble().Should().Be(secondValueInSequence);
    r.Generator.NextDouble().Should().Be(thirdValueInSequence);
}

这是一个从这里的一些答案中挑选出来的精致版本,只需将其添加到您的项目中即可。

public class RandomState
    {
        private static Lazy<System.Reflection.FieldInfo> _seedArrayInfo = new Lazy<System.Reflection.FieldInfo>(typeof(System.Random).GetField("_seedArray", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static));
        private static Lazy<System.Reflection.FieldInfo> _inextInfo = new Lazy<System.Reflection.FieldInfo>(typeof(System.Random).GetField("_inext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static));
        private static Lazy<System.Reflection.FieldInfo> _inextpInfo = new Lazy<System.Reflection.FieldInfo>(typeof(System.Random).GetField("_inextp", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static));
        private static System.Reflection.FieldInfo seedArrayInfo {get { return _seedArrayInfo.Value; }}
        private static System.Reflection.FieldInfo inextInfo { get { return _inextInfo.Value; } }
        private static System.Reflection.FieldInfo inextpInfo { get { return _inextpInfo.Value; } }
        private int[] seedState;
        private int inext;
        private int inextp;
        public static RandomState GetState(Random random)
        {
            var state = new RandomState() { seedState = ((int[])seedArrayInfo.GetValue(random)).ToArray(), inext = (int)inextInfo.GetValue(random), inextp = (int)inextpInfo.GetValue(random) };
            return state;
        }
        public static void SetState(Random random, RandomState state)
        {
            seedArrayInfo.SetValue(random, state.seedState.ToArray());
            inextInfo.SetValue(random, state.inext);
            inextpInfo.SetValue(random, state.inextp);
        }
    }
    public static class RandomExtensions
    {
        public static RandomState GetState (this System.Random random)
        {
            return RandomState.GetState(random);
        }
        public static void ApplyState (this System.Random random, RandomState state)
        {
            RandomState.SetState(random, state);
        }
    }

使用它的示例,尝试复制它。

    public class Program
    {
        public static void Main (string[] args)
        {
            System.Random rnd = new System.Random (255);
            var firststate = rnd.GetState();
            Console.WriteLine("Saved initial state...");
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom("Step ", rnd);
            var oldState = rnd.GetState();
            Console.WriteLine("Saved second state....");
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom("Step ", rnd);
            PrintRandom("Step ", rnd);
            PrintRandom("Step ", rnd);
            rnd.ApplyState(oldState);
            Console.WriteLine("Re-applied second state state....");
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
            rnd.ApplyState(firststate);
            Console.WriteLine("Re-applied initial state state....");
            PrintRandom("Step ", rnd);
            PrintRandom ("Step ", rnd);
            PrintRandom ("Step ", rnd);
        }
        static void PrintRandom (string label, Random rnd)
        {
            System.Console.WriteLine(string.Format ("{0} - RandomValue {1}", label, rnd.Next (1, 100)));
        }
    }

输出:

Saved initial state...
Step  - RandomValue 94
Step  - RandomValue 64
Step  - RandomValue 1
Saved second state....
Step  - RandomValue 98
Step  - RandomValue 34
Step  - RandomValue 40
Step  - RandomValue 16
Step  - RandomValue 37
Re-applied second state state....
Step  - RandomValue 98
Step  - RandomValue 34
Step  - RandomValue 40
Step  - RandomValue 16
Step  - RandomValue 37
Re-applied initial state state....
Step  - RandomValue 94
Step  - RandomValue 64
Step  - RandomValue 1

存储随机数生成器像Xi Huan写的那样运行的次数。

然后只需循环即可恢复旧状态。

Random rand= new Random();
int oldRNGState = 439394;
for(int i = 1; i < oldRNGState-1; i++) {
    rand.Next(1)
}

现在就做

int lastOldRNGValue = rand.Next(whateverValue);

没有办法解决这个问题,你必须循环回到你离开的地方。