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

时间:2016-11-24 21:16:41

标签: c# random functional-programming

我正在用C#编写一个基于骰子的游戏。我希望我的所有游戏逻辑都是纯粹的,所以我设计了一个像这样的骰子滚动生成器:

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

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

现在我可以在我的游戏逻辑中使用它:

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

问题是,下次我从diceRolls开始,我想跳过我已经采取的卷轴:

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

这已经非常难看并且容易出错。随着代码变得更加复杂,它不能很好地扩展。

这也意味着我必须在功能之间使用骰子滚动序列时要小心:

var x = DoSomeGameLogic(diceRolls);

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

我应该在这里使用一个好的设计模式吗?

请注意,由于同步和播放要求,我的功能必须保持纯净。

这个问题与正确初始化System.Random无关。请阅读我所写的内容,如果不清楚,请发表评论。

2 个答案:

答案 0 :(得分:2)

这是一个非常好的谜题。

由于操纵diceRolls的状态是不可能的(否则,我们会有你提到的那些同步和重放问题),我们需要一个操作,它返回(a)要消耗的值和(b)一个新的diceRolls可枚举,它在消耗的物品之后开始。

我的建议是使用(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);
}

用法:

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

DoSomeGameLogic将在内部使用Consume并返回剩余的卷。因此,需要按如下方式调用:

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

答案 1 :(得分:1)

&#34;经典&#34;实现纯随机生成器的方法是使用state monad的特殊形式(更多解释here),它包含了生成器当前状态的携带。所以,而不是实现(注意我的C#非常生疏,所以请将其视为伪代码):

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

你定义了这样的东西:

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);
    }
}

如果我做的一切都是正确的,哪些应该可用于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);

等,可能使用一些组合器。这里的诀窍是代表整个&#34;随机动作&#34;由当前输入状态参数化;但这也是问题所在,因为您需要NextRandom的良好实施。

如果您可以重用.NET Random实现的内部结构,那将会很好,但是看起来,您无法访问其内部状态。但是,我确信在互联网上有足够好的PRNG状态函数(this one看起来不错;您可能需要更改状态类型)。

monad的另一个缺点是,一旦你开始使用它们(即,在Random中构建东西),你需要&#34;随身携带#34;整个控制流程,直至最高级别,您应该一劳永逸地呼叫Run。这是人们需要使用的东西,并且在C#中比为这些东西优化的函数语言更乏味。