我正在用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
无关。请阅读我所写的内容,如果不清楚,请发表评论。
答案 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#中比为这些东西优化的函数语言更乏味。