278字节打造完整DOS象棋程序AttoChess
速览
AttoChess是一个专为16位x86 DOS系统设计的完整可玩国际象棋程序,整个程序仅占用278字节。它展示了极致的代码压缩技术,实现了完整的象棋规则和交互界面。这一成果体现了编程挑战与复古计算文化的融合,是技术爱好者对极限编程的致敬。
AI 深度解读
背景
AttoChess 是一个完整的、可对弈的国际象棋程序,专为 16 位 x86 DOS 环境设计,整个程序仅占用 278 字节。它源自一个更小的象棋程序,但通过一系列精妙的优化,在保持核心搜索算法不变的前提下,将体积压缩到了极致。这篇文章来自 Hacker News,详细拆解了 AttoChess 在显示、启动、输入、搜索循环和兵走法五个方面的优化技巧,展示了在极限空间下如何用最少指令实现完整功能。
核心内容
01 · 显示
显示部分的优化贡献了最大的体积节省。原版程序将棋盘渲染到一个独立缓冲区:遍历棋盘,将每个格子转换成可打印字符,存储,追加 $ 终止符,然后通过 DOS 功能 09h(int 21h)输出整个字符串。这种做法需要设置缓冲区指针、执行拷贝循环的存储、写入终止符,并且因为缓冲区位于棋盘数据之后,还需要在镜像中保留一个固定的棋盘数组。
AttoChess 完全删除了这些。它将棋盘边界的列布局为 CR、LF、CR、LF(0Dh、0Ah、0Dh、0Ah),而不是之前的填充符 08h。CR 和 LF 的 bit 3 都被置位,因此所有现有的边界/颜色掩码测试与之前完全一致,但原始棋盘字节本身已经变成了可打印的框架。显示循环通过 int 29h(DOS 快速控制台输出)将每个字节直接流式输出到控制台:边界变成换行符,空格变成 NUL,只有真正的棋子才进行 ASCII 转换。具体代码中,主循环从 board_db + 24 开始,读取 98 个字节(8 行棋盘加上最后的 CR/LF),对每个字节判断是否是棋子,若是则进行转换(inc ax 使王对齐,and al, 27h 提取棋子类型和颜色位,add al, 4Bh 映射到 K、N、B、P、Q、R 及其大小写),否则直接输出原始字节。这样一次去掉了:渲染缓冲区、缓冲区指针设置、$ 终止符、int 21h/09h 字符串打印以及文件中保留的棋盘数组。
02 · 启动
原版程序一开始调用 int 10h 强制设置 BIOS 显示模式 0。AttoChess 去掉这个调用,而是明确声明两个合理的入口假设:cld 清除方向标志(DF 在进入时不确定),mov cx, 13 设置行数(CX 在进入时不确定)。由于通过 int 29h 流式输出不需要特定视频模式,所以模式设置并非必需,去掉后体积更小,且无论程序如何启动都能正确工作。
03 · 输入
读取一步棋意味着将两个输入字符(文件和行)转换为棋盘地址。原版分步处理:读取文件字符并相加,然后读取行字符,用 and al, 0Fh 掩码,将 12 加载到 ah,用 mul 得到行偏移,最后相减。AttoChess 将算术运算折叠起来:把 ASCII 偏置常量直接预折叠到基地址中,利用 16 位指针数学在 64K 内自动回绕。归一化步骤和单独的乘法设置都消失了。具体代码中,基地址设为 board_db + 123 + 0CE0h,这样文件字符的加法直接抵消了 ASCII 偏移(add di, ax,其中 AX = 0100h + file char),然后读取行字符,用 imul ax, 12(80186 立即数乘法,替代了 mov ah,12 + mul ah)计算行偏移,最后相减得到目标格子。回绕基地址使显式的 and al, 0Fh 输入掩码变得多余。
04 · 搜索
在递归搜索内部,原版用一个计数循环(mov cl, 92 ... loop src_loop)扫描候选源方格。这复用了 CX 作为循环计数器,但 CX 同时也保存着搜索深度,因此每次递归调用时必须从栈帧中重新读取深度(mov cx, [si + 32])再递减。AttoChess 改为通过比较指针与棋盘末尾来遍历源方格:cmp bp, offset board_db + 120,jnz src_loop。这样 CX 从未被触及,始终作为活跃的深度计数器。递归调用点直接执行 dec cx,完全去掉了栈上的深度重载。
05 · 兵
原版的兵逻辑将向量符号位分离出来,移位到与颜色位对齐的位置,然后与当前走子方颜色异或,再根据奇偶性分支,涉及多条比特操作指令。AttoChess 将前进/后退测试直接折叠到颜色位 bit 5 中,用一条 xor al, dh 完成,并利用向量奇偶性(奇数偏移 = 对角线)来区分吃子和推兵。具体代码中,先 xor al, dh 使得 bit 5 等于“向量符号 XOR 走子方颜色”,然后 test al, 20h 判断是否向前(对于当前走子方颜色)。如果向后则拒绝。再 test al, 1 判断是否为对角线偏移(+/-11 或 +/-13),若是则必须吃子;否则是直走(+/-12),此时将目标颜色反转(xor ah, 30h)用于空格子测试。最后通过 test ah, dl 判断目标格子是否可走。由于方向测试基于走子方颜色而不是绝对符号,同一个代码路径同时正确处理了黑白双方的兵。
关键要点
- 显示循环:利用边界字符的 CR/LF 位模式直接作为可打印框架,去掉缓冲区、终止符和 DOS 字符串输出函数,用
int 29h逐字节输出,节省了缓冲区指针设置、拷贝循环、终止符写入和预留的棋盘数组。 - 启动:去掉 BIOS 模式设置,只设置必要的入口假设(方向标志和行数),因为流式输出不依赖特定视频模式。
- 输入解码:将 ASCII 偏置常量预折叠到基地址中,利用 16 位指针回绕消除显式掩码,并用 80186 立即数乘法替代寄存器+乘法指令。
- 搜索循环:用指针比较替代计数循环,释放 CX 寄存器,使其保持搜索深度,从而消除递归调用中的栈深度重载。
- 兵走法:用一条
xor al, dh将方向测试与颜色位结合,同时利用偏移奇偶性区分对角线(吃子)和直线(推兵),统一了黑白双方的兵逻辑。
意义与影响
AttoChess 展示了在极端资源约束下(仅 278 字节)如何通过重新思考数据布局
