
Dice:模拟扔到五个六的终端小游戏
之前看到有人直播扔骰子,一次扔五颗,直到五颗都是六点才结束。我有点好奇:如果让程序一直扔,会扔多少次才中?
这个概率可以直接计算,但我想通过模拟观察实际需要的投掷次数,于是写了 Dice。后来加上 TUI 配置、运行统计和命中动画,变成了一个终端小游戏。

配置界面:选择五个六,预览单次命中率和期望次数。
配置投掷目标
每轮重新扔全部五颗骰子,只有同一轮出现五个六才算命中,不保留上一轮的六。
省略界面和统计后,核心循环如下:
let rolls = 0;
let dice: number[];
do {
dice = Array.from({ length: 5 }, () => Math.floor(Math.random() * 6) + 1);
rolls += 1;
} while (!dice.every(value => value === 6));
console.log({ rolls, dice });
命中条件支持配置。除了五个六,还可以指定总点数为 30,或者不计顺序的 1、2、3、4、5,不必为每种目标修改循环。
命中条件还可以带通配符。[6,6,6,*,*] 表示至少三个六,剩下两颗随意;[X,X,X,X,X,*] 表示六颗骰子里有五颗点数相同,具体是几点都行。这里的 X 要取同一个值。
TUI 配置界面用于选择目标、骰子数量和运行方式,确认后再开始,省去记忆命令行参数的过程。
按批次推进模拟
只输出最终次数很简单,但这个项目也想保留持续投掷和等待命中的过程。我当时提过一个要求:目标很难时尽量快,目标容易时又不希望几秒内就结束。
所以模拟次数和界面刷新频率分开控制。每次刷新之间可以推进一批投掷,界面只展示这批结束时的状态,不需要每次投掷都重新渲染。
低期望次数的目标直接在主线程运行,根据期望次数和所选节奏估算批次大小。批次内部仍然逐次判断,命中立即停止,不会为了凑展示时长忽略结果。因此,这种节奏控制不保证最低运行时长,运气好时仍然可能提前结束。
需要大量投掷时,循环放到 worker 中,主线程接收进度并处理界面、按键,也可以配置多个 worker 并行运行。
节奏选项主要控制主线程模拟。worker 模式尽量快速计算,按另一个间隔向界面更新进度。配置界面显示的耗时是估计值,不是实测保证。

投掷到 2394 次时的界面,显示当前骰面、点数分布与最近记录。
命中后播放庆祝动画,再显示结束报告,也保留了跳过动画的操作。
五个六的概率
配置界面显示理论概率和期望次数,运行时可以与实际投掷次数对照。
假设五颗骰子公平,各颗骰子与每次投掷都相互独立,单次五个六的概率是:
p = (1/6)^5 = 1/7776
期望投掷次数 = 1/p = 7776
7776 并不是扔到这个次数就该中了。按照这个模型,扔过 7776 次仍然没中,概率大约还有 36.8%。前面已经失败多少次,也不会提高下一次出现五个六的概率。
因此,实际次数与期望次数的比值只表示这一局已经投掷了多少。超过 100% 仍未命中是正常结果,不能把期望次数当成倒计时。
程序另有会人为改变命中规则的黑箱玩法。观察普通随机投掷时需要关闭它,否则结果不能直接按上述概率模型解释。
配置校验
假设目标总和是 30,骰子数量是 5,这个目标可以达到。把骰子数量改成 4 后,最大总和只有 24,目标 30 就失效了。
当时的问题是,内部已经判定目标无效,显示的 30 却还是普通状态。修改后保留这个输入值,直接显示非法状态并阻止开始,由用户选择修改目标或恢复骰子数量,不自动替换参数。
目标概率和校验状态都随配置变化,开始前就能看到参数是否有效。
评论
评论加载中…