简体   繁体   English

功能纯粹的骰子在C#中滚动

[英]Functionally pure dice rolls in C#

I am writing a dice-based game in C#. 我正在用C#编写一个基于骰子的游戏。 I want all of my game-logic to be pure, so I have devised a dice-roll generator like this: 我希望我的所有游戏逻辑都是纯粹的,所以我设计了一个像这样的骰子滚动生成器:

public static IEnumerable<int> CreateDiceStream(int seed)
{
    var random = new Random(seed);

    while (true)
    {
        yield return 1 + random.Next(5);
    }
}

Now I can use this in my game logic: 现在我可以在我的游戏逻辑中使用它:

var playerRolls = players.Zip(diceRolls, (player, roll) => Tuple.Create(player, roll));

The problem is that the next time I take from diceRolls I want to skip the rolls that I have already taken: 问题是,下次我从diceRolls我想跳过我已经采取过的卷:

var secondPlayerRolls = players.Zip(
    diceRolls.Skip(playerRolls.Count()), 
    (player, roll) => Tuple.Create(player, roll));

This is already quite ugly and error prone. 这已经非常丑陋并且容易出错。 It doesn't scale well as the code becomes more complex. 随着代码变得更加复杂,它不能很好地扩展。

It also means that I have to be careful when using a dice roll sequence between functions: 这也意味着在函数之间使用骰子滚动序列时必须小心:

var x = DoSomeGameLogic(diceRolls);

var nextRoll = diceRolls.Skip(x.NumberOfDiceRollsUsed).First();

Is there a good design pattern that I should be using here? 我应该在这里使用一个好的设计模式吗?

Note that it is important that my functions remain pure due to syncronisation and play-back requirements. 请注意,由于同步和播放要求,我的功能保持纯净非常重要。


This question is not about correctly initializing System.Random . 这个问题与正确初始化System.Random无关。 Please read what I have written, and leave a comment if it is unclear. 请阅读我所写的内容,如果不清楚,请发表评论。

That's a very nice puzzle. 这是一个非常好的谜题。

Since manipulating diceRolls 's state is out of the question (otherwise, we'd have those sync and replaying issues you mentioned), we need an operation which returns both (a) the values to be consumed and (b) a new diceRolls enumerable which starts after the consumed items. 由于操纵diceRolls的状态是diceRolls的(否则,我们会有你提到的那些同步和重放问题),我们需要一个操作,它返回(a)要消耗的值和(b)可枚举的新diceRolls在消耗的物品之后开始。

My suggestion would be to use the return value for (a) and an out parameter for (b): 我的建议是使用(a)的返回值和(b)的out参数:

static IEnumerable<int> Consume(this IEnumerable<int> rolls, int count, out IEnumerable<int> remainder)
{
    remainder = rolls.Skip(count);
    return rolls.Take(count);
}

Usage: 用法:

var firstRolls = diceRolls.Consume(players.Count(), out diceRolls);
var secondRolls = diceRolls.Consume(players.Count(), out diceRolls);

DoSomeGameLogic would use Consume internally and return the remaining rolls. DoSomeGameLogic将在内部使用Consume并返回剩余的卷。 Thus, it would need to be called as follows: 因此,需要调用如下:

var x = DoSomeGameLogic(diceRolls, out diceRolls);
// or
var x = DoSomeGameLogic(ref diceRolls);
// or
x = DoSomeGameLogic(diceRolls);
diceRolls = x.RemainingDiceRolls;

The "classic" way to implement pure random generators is to use a specialized form of a state monad (more explanation here ), which wraps the carrying around of the current state of the generator. 实现纯随机生成器的“经典”方法是使用状态monad的特殊形式( 这里有更多解释),它包含了生成器当前状态的携带。 So, instead of implementing (note that my C# is quite rusty, so please consider this as pseudocode ): 所以,而不是实现(请注意我的C#非常生疏,所以请将其视为伪代码 ):

Int Next() {
    nextState, nextValue = NextRandom(globalState);
    globalState = nextState;
    return nextValue;
} 

you define something like this: 你定义这样的东西:

class Random<T> {
    private Func<Int, Tuple<Int, T>> transition;

    private Tuple<Int, Int> NextRandom(Int state) { ... whatever, see below ... }

    public static Random<A> Unit<A>(A a) { 
        return new Random<A>(s => Tuple(s, a)); 
    }

    public static Random<Int> GetRandom() {
        return new Random<Int>(s => nextRandom(s));
    }

    public Random<U> SelectMany(Func<T, Random<U>> f) {
        return new Random(s => {
            nextS, a = this.transition(s);
            return f(a).transition(nextS);
        }
    }

    public T Run(Int seed) {
        return this.transition(seed);
    }
}

Which should be usable with LINQ, if I did everything right: 如果我做的一切都正确,哪些应该可用于LINQ:

// player1 = bla, player2 = blub, ...
Random<Tuple<Player, Int>> playerOneRoll = from roll in GetRandom()
                                           select Tuple(player1, roll);
Random<Tuple<Player, Int>> playerTwoRoll = from roll in GetRandom()
                                           select Tuple(player2, roll);
Random<List<Tuple<Player, Int>>> randomRolls = from t1 in playerOneRoll
                                               from t2 in playerTwoRoll
                                               select List(t1, t2);

var actualRolls = randomRolls.Run(234324);

etc., possibly using some combinators. 等,可能使用一些组合器。 The trick here is to represent the whole "random action" parametrized by the current input state; 这里的技巧是表示当前输入状态参数化的整个“随机动作”; but this is also the problem, since you'd need a good implementation of NextRandom . 但这也是问题,因为你需要一个很好的NextRandom实现。

It would be nice if you could just reuse the internals of the .NET Random implementation, but as it seems, you cannot access its internal state. 如果你可以重用.NET Random实现的内部结构会很好,但是看起来,你无法访问其内部状态。 However, I'm sure there are enough sufficiently good PRNG state functions around on the internet ( this one looks good; you might have to change the state type). 但是,我确信在互联网上有足够好的PRNG状态函数( 这个看起来很好;你可能需要改变状态类型)。

Another disadvantage of monads is that once you start working in them (ie, construct things in Random ), you need to "carry that though" the whole control flow, up to the top level, at which you should call Run once and for all. monad的另一个缺点是,一旦你开始使用它们(即,构建Random东西),你需要“携带那个”整个控制流程,直到最高级别,你应该在那里调用Run一劳永逸。 This is something one needs to get use to, and is more tedious in C# than functional languages optimized for such things. 这是人们需要使用的东西,并且在C#中比为这些东西优化的函数语言更乏味。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM