简体   繁体   中英

Functionally pure dice rolls in C#

I am writing a dice-based game in 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:

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 . 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.

My suggestion would be to use the return value for (a) and an out parameter for (b):

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. 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. So, instead of implementing (note that my C# is quite rusty, so please consider this as pseudocode ):

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:

// 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 .

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. 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).

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. This is something one needs to get use to, and is more tedious in C# than functional languages optimized for such things.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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