3

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.

sdgfsdh
  • 33,689
  • 26
  • 132
  • 245
  • Dice rolls can and do repeat...sounds like you are looking for a shuffle – Ňɏssa Pøngjǣrdenlarp Nov 24 '16 at 21:21
  • Of course they do. The problem is that calling `diceRolls.First()` multiple times always gives the same result. I need an elegant way to skip results that I have already used. – sdgfsdh Nov 24 '16 at 21:25
  • Can you just use separate roll stream for each player? If not, maybe you can use single enumerator (now you create new enumetator each time so it starts all over again). – Evk Nov 24 '16 at 21:28
  • 1
    Possible duplicate of [Random number generator only generating one random number](http://stackoverflow.com/questions/767999/random-number-generator-only-generating-one-random-number) – Ňɏssa Pøngjǣrdenlarp Nov 24 '16 at 21:30
  • 2
    Can you give some more context on why you even have a `secondPlayerRolls` variable? I mean wouldn't the second turn just be another call of the same function, rather than its own code with its own variables? – sepp2k Nov 24 '16 at 21:30
  • It is just an example. There are lots of games where the players roll in several rounds (e.g. in Monopoly each player rolls at the start of their turn) – sdgfsdh Nov 24 '16 at 21:32
  • 1
    @sdgfsdh Yes, and I'm saying when programming such a game there'd never be such a thing as a `secondRolls` variable, so the problem you described just wouldn't occur. – sepp2k Nov 24 '16 at 21:33
  • I don't understand what you mean. There are games where all of the players roll a die and then all of the players roll again. – sdgfsdh Nov 24 '16 at 21:34
  • What do you mean by "pure", since the act of rolling once changes the next roll then that method is hardly pure is it? – Lasse V. Karlsen Nov 24 '16 at 21:35
  • 1
    @Plutonix this question is not related to what you marked as duplicate at all. – Evk Nov 24 '16 at 21:35
  • @sdgfsdh Yes, so you have a function `turn` where every placer rolls the die. And then you call that function multiple times and that way there are multiple turns. If you have a `firstTurn` and a `secondTurn` variable and so, you can only have a fixed number of turns. That doesn't make sense for most games. And even if your game does only have a fixed number of turns, it still makes more sense to call one function multiple times rather than just repeating the same code. – sepp2k Nov 24 '16 at 21:36
  • 1
    I mean "pure" in the sense of functional purity. A function is pure when it has no side-effects and the result is fully determined by the arguments. – sdgfsdh Nov 24 '16 at 21:37
  • @sdgfsdh: What you probably mean by "pure" is that "diceRolls" should stay immutable, right? – Heinzi Nov 24 '16 at 21:39
  • That is correct @Heinzi. – sdgfsdh Nov 24 '16 at 21:41
  • Random is Random. it doesn't make sense to skip that enumerator. you get random numbers anyway. you should understand how enumerators work. when you call MoveNext it will return value at `yield return`. next time you call MoveNext from same function it will continue execution from previous position i.e previous yield return. – M.kazem Akhgary Nov 24 '16 at 21:54
  • 1
    @M.kazemAkhgary It's an enumerable, not an enumerator. He's not calling `MoveNext` on anything (nor should he if he wants to maintain purity). – sepp2k Nov 24 '16 at 21:58
  • Am I missing something, or wouldn't this be impure by definition (since the roll depends on both the argument and on the previous rolls)? – EJoshuaS - Stand with Ukraine Nov 24 '16 at 23:02
  • No, it can be pure if the dice rolls are determined by the `seed` argument. – sdgfsdh Nov 24 '16 at 23:18

2 Answers2

2

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;
Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • Isn't basically the same Skip OP already uses? Generating all numbers up to N to get Nth roll feels not very good. – Evk Nov 24 '16 at 21:45
  • I think that should be `return rolls.Take(remainder);` right? – Evan Trimboli Nov 24 '16 at 21:49
  • @Evk: The OPs solution requires the number of first rolls in the code line creating the second rolls, which is ugly and error prone. My solution fixed that (by requiring the number of first rolls only in the code line creating the first rolls). I did not do anything to enhance performance or fix the "Schlemiel the Painter" issue, which you correctly identified. – Heinzi Nov 24 '16 at 22:07
  • @EvanTrimboli: Nope, Take takes an integer. – Heinzi Nov 24 '16 at 22:08
  • Do you think there is a way to not enumerate sequence over and over again from the start and still leave code "pure"? I suppose if you will reuse the same enumerator it will become not pure any more? – Evk Nov 25 '16 at 09:28
  • @Evk: To be pure, we'd need an immutable object which always returns the random sequence initialized with seed x starting with the y-th element. We can't use the same enumerator, since the enumerator is not immutable - replaying wouldn't be possible. Since the .NET framework does not expose such an object, I don't think there's an easy or elegant solution for this. – Heinzi Nov 25 '16 at 09:50
1

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.

Community
  • 1
  • 1
phipsgabler
  • 20,535
  • 4
  • 40
  • 60