news 2026/9/24 13:47:56

DIGITS 数字猜测游戏:从 1978 年 BASIC 版到多语言移植的模式识别算法剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DIGITS 数字猜测游戏:从 1978 年 BASIC 版到多语言移植的模式识别算法剖析
  • 示例工程

【免费下载链接】basic-computer-games

An updated version of the classic "Basic Computer Games" book, with well-written examples in a variety of common MEMORY SAFE, SCRIPTING programming languages. See https://coding-horror.github.io/basic-computer-games/

项目地址:https://gitcode.com/gh_mirrors/ba/basic-computer-games
点击查看免费下载

导读

本文基于 34_Digits/README.md 及其所在仓库的完整源码,深入剖析 DIGITS 这款经典猜数字游戏:玩家随机写下 30 个 0/1/2 数字,计算机则借助三个"记忆矩阵"和一条加权求和方程,实时统计你的数字序列特征并逐位预测下一个数字。你将了解到游戏规则、猜测方程的逐行数学拆解、状态变量的滚动更新机制、原始代码中"神秘常量"与A恒为 0 的历史怪癖,以及同一算法在 BASIC、Python、Java、C#、JavaScript、Perl 中的移植实现差异与运行方式。

一、游戏是什么:规则与玩法

DIGITS 是一个"人机对抗的序列预测"游戏,玩法极其简单:

  1. 玩家先取一张纸,随机写下 30 个数字,每个数字只能是 0、1 或 2,排成三行、每行 10 个;
  2. 计算机分三轮向玩家索要数字,每轮 10 个
  3. 对每个数字,计算机总是先猜测,再查看玩家给出的真实数字,判断自己是否猜中;
  4. 连续 30 次预测之后,统计猜中次数,判定胜负。

游戏的核心悬念在于:如果纯粹靠运气(0/1/2 各占 1/3),计算机 30 次预测的期望命中数是10 次(即 1/3)。但正如 README 所述:"It is uncanny how much better it generally does than that!"——由于它会对玩家刚输入的数字序列做统计学习,实际表现往往远超 10 次,这正是本游戏的乐趣所在。

README 同时记载了该程序的出处:程序起源于达特茅斯学院(Dartmouth),原作者不详;它作为《Basic Computer Games》(1978)中的经典程序之一被收录,原版程序由 Vintage Basic 网站流传而来。完整规则说明也原样保留在各移植版的指令文本中,例如 34_Digits/csharp/Resources/Instructions.txt。

二、核心算法:猜测方程的逐行拆解

整个游戏的核心只有一条方程,位于原始 BASIC 源码 34_Digits/digits.bas 的第 700 行:

700 S1=A*K(Z2,J)+B*L(Z1,J)+C*M(Z,J)

它对候选数字J(J = 0、1、2)分别计算一个加权得分S1,得分最高的候选即为计算机的猜测。三个加权项分别来自三个"记忆矩阵":

系数矩阵行索引含义
A*K(Z2,J)A = 0K(2,2)Z2 = 上一个真实数字按"上一数字"统计的历史频率
B*L(Z1,J)B = 1L(8,2)Z1 = 最近两位组合编码按"最近两位组合"统计的历史频率
C*M(Z,J)C = 3M(26,2)Z = 两位历史三进制状态按"历史状态"统计的历史频率

系数 A = 0、B = 1、C = 3 在 BASIC 中通过DATA 0,1,3读取(digits.bas)。注意:C 的权重(3)远高于 B(1),即"两位历史状态" M 矩阵对猜测的贡献最大,这暗示原作者的意图是捕捉数字序列中的二阶(bigram)统计规律

猜测的选择逻辑(digits.bas):

690 FOR J=0 TO 2 700 S1=A*K(Z2,J)+B*L(Z1,J)+C*M(Z,J) 710 IF S>S1 THEN 760 720 IF S<S1 THEN 740 730 IF RND(1)<.5 THEN 760 740 S=S1: G=J 760 NEXT J

即:初始S=0,遍历 J 后取S1最大的 J 作为猜测G;若两个候选得分相等(平局),则抛一枚硬币(RND(1)<.5,50% 概率)随机改选——这是一个让行为不至于死板的随机扰动机制。所有移植版都忠实保留了这一逻辑,例如 Python 版 34_Digits/python/Digits.py:

if s < s1: s = s1 my_guess = j elif s1 == s and random.random() >= 0.5: my_guess = j

三、三个记忆矩阵与状态变量

3.1 矩阵初始化

BASIC 中三个矩阵的初始化(digits.bas):

400 FOR I=0 TO 26: FOR J=0 TO 2: M(I,J)=1: NEXT J: NEXT I ' M 全部填 1 410 FOR I=0 TO 2: FOR J=0 TO 2: K(I,J)=9: NEXT J: NEXT I ' K 全部填 9 420 FOR I=0 TO 8: FOR J=0 TO 2: L(I,J)=3: NEXT J: NEXT I ' L 全部填 3 450 L(0,0)=2: L(4,1)=2: L(8,2)=2 ' 但对角线三项改为 2
  • M(27×3):27 行对应 27 个"两位历史状态"(下文详述),每行 3 列对应候选数字 0/1/2,初值全部为 1;
  • L(9×3):9 行对应两位组合的 9 种编码,初值为 3,但L(0,0)=L(4,1)=L(8,2)=2三个对角线位置被压低到 2——同样属于 README 所说的"看似随意的神秘常量";
  • K(3×3):3 行对应上一数字,初值全部为 9。

3.2 状态变量 Z、Z1、Z2 的滚动更新

三个矩阵的行索引不是固定的,而是随游戏进程滚动变化的:

480 Z=26: Z1=8: Z2=2 ' 初始状态 ... 830 M(Z,N)=M(Z,N)+1 840 L(Z1,N)=L(Z1,N)+1 850 K(Z2,N)=K(Z2,N)+1 860 Z=Z-INT(Z/9)*9 ' 等价于 Z = Z MOD 9 870 Z=3*Z+N(U) ' 三进制左移并入新数字 ... 880 Z1=Z-INT(Z/9)*9 ' Z1 = Z MOD 9 890 Z2=N(U) ' Z2 = 当前真实数字

从源码结构可以推断:Z 是一个以 3 为底的"最近两位数字"状态编码Z = 3*a + ba对应倒数第二个数字、b对应最近一个数字,取值 0~26;每当猜中并观察到新数字N后,先取Z MOD 9丢弃最高位,再3*Z+N并入新数字,从而滚动记录最新的两位组合。Z1 = Z MOD 9是两位组合的 0~8 编码,用作 L 矩阵的行索引;Z2就是当前真实数字,用作 K 矩阵的行索引。

因此三个矩阵实际构成了不同粒度的"条件计数表":

  • M[Z][j]:在历史状态 Z(最近两位数字)之后出现 j 的次数(二阶统计);
  • L[Z1][j]:在两位组合 Z1 之后出现 j 的次数(一阶/二阶折中);
  • K[Z2][j]:在数字 Z2 之后出现 j 的次数(一阶统计)。

得分方程S1 = 0*K + 1*L + 3*M实质是对三个粒度的条件频率做加权投票,权重偏向更精细的二阶状态。而权重最高的 M 矩阵之所以初值填 1、L 填 3、K 填 9,是一种"拉普拉斯平滑"式的先验设计——初值越大,冷启动阶段对某种数字的倾向越保守。

3.3 面向对象版:C# 的实现印证

C# 移植版将这套逻辑清晰地对象化,见 34_Digits/csharp/Memory.cs 与 34_Digits/csharp/Matrix.cs:

_matrices = new[] { new Matrix(27, 3, (_, _) => 1), // M:27 行,权重 3,全 1 new Matrix(9, 1, (i, j) => i == 4 * j ? 2 : 3), // L:9 行,权重 1,对角线 2 new Matrix(3, 0, (_, _) => 9) // K:3 行,权重 0,全 9 };

C# 版把原来散落在 BASIC 全局变量里的系数 A/B/C内嵌为 Matrix 对象的weight字段(3、1、0 分别对应 M、L、K),每次猜测时对三个矩阵的加权值求和(34_Digits/csharp/Guesser.cs),学习更新则集中在ObserveDigit中:三个矩阵各自IncrementValue,并同步滚动Index(对应 BASIC 的 Z/Z1/Z2)。这与 BASIC 原版的逐行逻辑一一对应,可以作为理解原始方程的最佳"带注释版本"。

四、输入校验与游戏流程

每轮输入 10 个数字时,程序会强制校验取值范围。BASIC 的写法相当精妙(digits.bas):

570 W=N(I)-1 580 IF W=SGN(W) THEN 620 ' 只有 N=0、1、2 时 W 才等于 SGN(W) 590 PRINT "ONLY USE THE DIGITS '0', '1', OR '2'." 600 PRINT "LET'S TRY AGAIN.":GOTO 530

即对每个输入数字减 1 后与符号函数比较,巧妙地排除了 0/1/2 以外的任何整数;非法的整轮输入都会被驳回重输。移植版则改用更直白的范围判断,如 Python 版 34_Digits/python/Digits.py 的if number < 0 or number > 2,并在读取时对非数字输入提示!NUMBER EXPECTED - RETRY INPUT LINE

三轮共 30 个数字猜测完毕后,进入胜负判定(digits.bas):

  • X > 10I GUESSED MORE THAN 1/3 OF YOUR NUMBERS. I WIN.(计算机赢,BASIC 还会连响 10 声铃CHR$(7));
  • X == 10I GUESSED EXACTLY 1/3 OF YOUR NUMBERS. IT'S A TIE GAME.(平局,恰等于纯随机的期望值);
  • X < 10I GUESSED LESS THAN 1/3 OF YOUR NUMBERS. YOU BEAT ME. CONGRATULATIONS *****(玩家赢)。

随后询问是否再来一局(DO YOU WANT TO TRY AGAIN (1 FOR YES, 0 FOR NO)),选择继续则重新初始化三个矩阵和状态变量(回到第 400 行),保证每局之间互不影响。整个游戏主循环在 C# 版中被拆分为GameSeries.Play()Game.Play()(34_Digits/csharp/Game.cs),GameSeries负责"介绍 → 指令 → 循环开局 → 告别",Game负责单局的三轮预测,结构比原始 BASIC 的 GOTO 网清晰得多。

五、移植注意事项:神秘常量与 A=0 的怪癖

README 的 Porting Notes 部分给出了两个重要的移植警示,这两点在源码中都能得到印证:

  1. The program contains a lot of mysterious and seemingly arbitrary constants. It's not clear there is any logic or rationality behind it.
  2. The key equation involved in the guess (line 700) involves a factor ofA, butAis always 0, making that term meaningless. As a result, all the work to build and update array K and value Z2 appear to be meaningless, too.

其一,神秘常量遍地:矩阵初值 1/9/3、L 的三个对角线值 2、Z/Z1/Z2 的初始 26/8/2、系数 0/1/3……这些数值从算法层面难以完全解释其设计动机,移植时照搬即可,不必强行赋予意义。

其二,A 恒为 0 导致 K 矩阵与 Z2 的更新成为"死代码":猜测方程第 700 行S1=A*K(Z2,J)+B*L(Z1,J)+C*M(Z,J)中,A通过DATA 0,1,3恒定为 0,因此A*K(Z2,J)这一项对最终得分永远没有贡献。相应地,第 850 行K(Z2,N)=K(Z2,N)+1对 K 矩阵的更新、以及第 890 行对 Z2 的维护,从结果上看都属于无用功。Python 移植版的作者在源码中直接以注释标注了这一发现(34_Digits/python/Digits.py):

# What did the original author have in mind ? # The first expression always results in 0 because a is always 0

有趣的是,C# 版把 A=0 这个怪癖"固化"进了设计:K 矩阵的weight参数正是 0(34_Digits/csharp/Memory.cs),同时在ObserveDigit中仍然忠实执行 K 矩阵的更新与 Z2 的滚动——即移植版完整保留了原始代码的结构与行为,包括其"无效"的部分,这对理解原始程序、保持跨语言行为一致而言,反而是值得肯定的移植态度。这也解释了为何 README 建议移植者"照原样翻译",而不是"自作聪明地删掉 K 矩阵"。

六、多语言移植与运行方式

DIGITS 是该仓库中移植语言最丰富的游戏之一,除原始 BASIC 外至少包含 6 种实现,且各版本逻辑逐行对应:

语言文件特点
BASIC34_Digits/digits.bas原始版,GOTO 结构,RND/CHR$(7)等经典语法
Python34_Digits/python/Digits.py函数化拆分,附"原作者意图"注释
Java34_Digits/java/Digits.java单文件Digits类,printf对齐表格输出
C#34_Digits/csharp/Program.csOO 重构,Guesser/Memory/Matrix分层,文本资源外置
JavaScript34_Digits/javascript/digits.js浏览器交互版,async/await模拟输入
Perl34_Digits/perl/digits.pl1-based 数组技巧("0 " . $Answer占位)

各版本的运行方式:

  • BASIC:使用 Vintage BASIC 等兼容解释器加载 34_Digits/digits.bas 后RUN
  • Pythonpython3 34_Digits/python/Digits.py(需要 Python 3,使用了typingrandom标准库);
  • Java:编译并运行 34_Digits/java/Digits.java(javac+java Digits,依赖java.util.Scanner);
  • C#dotnet run --project 34_Digits/csharp/Digits.csproj,该项目面向 .NET 6.0(见 34_Digits/csharp/Digits.csproj),并引用仓库公共库00_Common/dotnet/Games.Common
  • JavaScript:直接用浏览器打开 34_Digits/javascript/digits.html,在页面中交互;
  • Perlperl 34_Digits/perl/digits.pl(依赖use strict; use warnings;)。

七、小结

DIGITS 是一个"以简驭繁"的经典案例:30 行规则 + 1 条加权方程 + 3 个计数矩阵,就让一台 1978 年的计算机具备了超越纯随机的"模式识别"能力。透过 34_Digits/README.md 的规则说明与移植笔记,再对照 34_Digits/digits.bas 及各语言移植版,可以清晰看到:

  • 猜测的本质是对一阶/二阶条件频率的加权投票,系数 0/1/3 与矩阵初值构成隐式的先验平滑;
  • Z/Z1/Z2是一个滚动更新的三进制历史状态机,是整条算法的"记忆指针";
  • 原版代码中A恒为 0、K 矩阵与 Z2 更新无意义,属于忠实移植时必须知晓的历史遗留怪癖;
  • 各移植版(尤其 C# 的对象化重构)为理解这套算法提供了极佳的"可读性增强版本"。

如果你对"让计算机学会猜你"的统计游戏感兴趣,DIGITS 是一个小而完整的入门样例——既适合作为学习马尔可夫式条件计数思想的练手项目,也适合作为跨语言移植对比的基准程序。

  • 示例工程

【免费下载链接】basic-computer-games

An updated version of the classic "Basic Computer Games" book, with well-written examples in a variety of common MEMORY SAFE, SCRIPTING programming languages. See https://coding-horror.github.io/basic-computer-games/

项目地址:https://gitcode.com/gh_mirrors/ba/basic-computer-games
点击查看免费下载
上一篇:深度解析BsMax插件架构:3ds Max工作流迁移的3大技术实现优势
下一篇:FluentValidation 终极指南:避免常见错误的15个实用技巧

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 13:45:38

小型以太网组建实战:线序、IP配置与故障排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 13:42:27

douyin-downloader 实操指南:抖音作品批量下载去水印完整教程

douyin-downloader 实操指南&#xff1a;抖音作品批量下载去水印完整教程 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallbac…

作者头像 李华
网站建设 2026/9/24 13:38:08

推荐开源项目:Eventyay 支持FAQ平台

推荐开源项目&#xff1a;Eventyay 支持FAQ平台 【免费下载链接】open-event-documentation Archived documentation 项目地址: https://gitcode.com/gh_mirrors/su/open-event-documentation 项目介绍 Eventyay 支持FAQ是一个全面的资源库&#xff0c;为活动组织者、参…

作者头像 李华
网站建设 2026/9/24 13:34:51

私人牙科诊所管理系统

私人牙科诊所管理系统选题背景与意义随着我国居民健康意识的不断提升以及口腔健康问题日益受到重视&#xff0c;私人牙科诊所的数量呈现快速增长趋势。相较于大型公立医院&#xff0c;私人牙科诊所具有服务灵活、环境舒适、个性化程度高等优势&#xff0c;逐渐成为民众获取口腔…

作者头像 李华