经过几周的期待,你和朋友们终于聚在一起,开始你们的第一局 Dungeons & Dragons(D&D)。 因为这是游戏的第一场,每位玩家都要生成一个角色来玩。 角色的能力由掷 6 面骰子决定,可骰子_到底_在哪儿呢? 你震惊地发现,朋友们正等着_你_来准备骰子,毕竟玩 D&D 可是你出的主意! 慌乱之中,你意识到自己忘了带骰子,那就意味着没法玩 D&D 了。 好在你有一些基本的编程技能,很快就想出了一个解决办法:写一个程序来模拟掷骰子。
在 Dungeons & Dragons 游戏中,每位玩家首先要生成一个可以扮演的角色。 这个角色除了其他特征之外,还有 6 项属性:力量、敏捷、体质、智力、感知和魅力。 这 6 项属性的数值是随机确定的。 具体做法是掷 4 颗六面骰子,记下最大的 3 颗骰子的点数之和。 每项属性掷 1 次,一共重复 6 次。
你角色的初始生命值是 10 加上你角色的体质调整值。 用你角色的体质减去 10,再除以 2 并向下取整,就得到你角色的体质调整值。
编写一个遵循上述规则的随机角色生成器。
例如,这 6 次掷 4 颗骰子的结果可能如下:
因为体质是 3,所以体质调整值是 -4,生命值是 6。
大多数编程语言都有(伪)随机数生成器,但很少有编程语言是专门为掷骰子设计的。 其中一种就是 Troll。
在函数式语言中,大多数函数都是_纯函数_。 这意味着,对于同一组实参,它们总是返回相同的输出,除此之外什么都不做。 换句话说,纯函数是确定性的。
然而,顾名思义,随机值是不可预测的,也是非确定性的。 即使是伪随机性,也就是返回一串_看起来_随机的数字,在纯函数的语境里也没那么容易实现。 因为伪随机数生成器(PRNG)需要跟踪内部状态,才能返回序列中的下一个数字。
Lean 提供了两种解决这个问题的思路:
使用一种 monad,让你可以改变 PRNG 的内部状态。
有一个现成的函数 IO.rand 正好能做这件事。
使用纯函数,把生成器作为实参传入,返回值不仅有生成的伪随机值,还有一个更新后的生成器,它已经“准备好”产出序列中的下一个数字。
本练习采用第二种方式。 每个需要生成伪随机值的函数都要接收一个生成器,并且要同时返回这个值和更新后的生成器。
注意,给定的生成器是确定性的,也就是说,它_总是_产出相同的值。 想要生成序列中的下一个伪随机值,就必须使用更新后的生成器。
在本练习中,随机性通过卡方检验来检查,显著性水平为 p < 0.0001。
这意味着,如果按照说明生成属性值,一个正确的实现因随机因素而无法通过测试的概率不到 0.01%。
注意,按照说明,属性值_是四次投掷无偏 d6(六面骰)中最高的三个结果之和_。