- 教程
- 示例工程
【免费下载链接】cosmos
World's largest Contributor driven code dataset | Used in Quark Search Engine, @OpenGenus IQ, OpenGenus Visual Project
数字推盘(15-puzzle,又称 Gem Puzzle、Boss Puzzle、Game of Fifteen、Mystic Square 等)是一款经典的滑块谜题:一个方框内装有若干带数字的方形瓷砖,随机排列,且恰好缺一块,玩家通过滑动瓷砖利用唯一的空位将它们按顺序排好。在 cosmos 仓库的 fifteen_puzzle 模块中,这一经典游戏被实现为一个可推广到任意 d×d 棋盘的终端程序(fifteen.c)。阅读本文,你将掌握 15-puzzle 的规则体系与命名约定、C 语言版核心数据结构和五个关键函数(init / draw / move / won / clear)的逐行逻辑,并能直接编译运行、调试与验证该实现。
一、从文档出发:什么是 15-puzzle 及其家族
1.1 核心规则
根据模块 readme.md 的定义:15-puzzle 是一种滑动拼图,由随机顺序排列的编号方形瓷砖和一个空位组成。游戏目标是:通过利用空位的滑动移动,将瓷砖按顺序放置到位。
1.2 命名与尺寸约定
文档明确指出:拼图存在多种尺寸,特别是更小的 8-puzzle:
- 3×3 瓷砖 → 称为8-puzzle 或 9-puzzle
- 4×4 瓷砖 → 称为15-puzzle 或 16-puzzle
这里数字分别对应瓷砖数量和空位数量(即格位总数):8-puzzle 有 8 块编号瓷砖 + 1 个空位 = 9 格;15-puzzle 有 15 块编号瓷砖 + 1 个空位 = 16 格。这是理解后面代码中d*d - 1这个计数的关键。
二、源码级纵览:一个可推广到 d×d 的通用实现
仓库中的实现没有局限于 4×4,而是将棋盘尺寸参数化为d(fifteen.c):
- 维度
d的合法范围由 DIM_MIN / DIM_MAX 常量 限定为 3~9:#define DIM_MIN 3 #define DIM_MAX 9 - 棋盘使用全局二维数组
int board[DIM_MAX][DIM_MAX](L29)承载,最大可容纳 9×9 的棋盘,因此一次编译即可覆盖 8-puzzle、15-puzzle 乃至 80-puzzle(9×9)的全部形态。
程序对外暴露 5 个核心函数(L35-L40 原型声明):clear(清屏)、greet(欢迎)、init(初始化棋盘)、draw(绘制棋盘)、move(移动瓷砖)、won(胜负判定)。
三、命令行入口与主循环:fifteen d
3.1 参数校验
main函数(L42-L134)要求且仅接受一个命令行参数:
./fifteen d- 参数个数不为 2 时,输出
Usage: fifteen d并返回退出码 1; d不在[DIM_MIN, DIM_MAX]即 [3, 9] 范围内时,提示Board must be between 3 x 3 and 9 x 9, inclusive.并返回退出码 2;- 日志文件打开失败时返回退出码 3。
3.2 主循环
while (1) { clear(); // 清屏 draw(); // 绘制当前棋盘 /* 将当前棋盘状态写入 log.txt */ if (won()) { // 胜利判定 printf("ftw!\n"); break; } printf("Tile to move: (0)exit "); scanf("%d", &tile); if (tile == 0) break; // 输入 0 退出(便于自动化测试) if (!move(tile)) { // 非法移动 printf("\nIllegal move.\n"); usleep(500000); } usleep(500000); // 动画节奏 }这个循环精确对应文档所述的“通过滑动利用空位”的过程:每一回合绘制棋盘 → 判定胜负 → 提示玩家输入要移动的瓷砖编号 → 执行移动或报告非法。胜利时打印ftw!(for the win)并退出。
3.3 动画与日志:usleep 与 log.txt
- 代码注释(L11-L12)说明:
usleep虽已废弃,但比sleep粒度更细,比nanosleep更简单。greet显示欢迎语后暂停 2 秒(usleep(2000000)),每回合移动后暂停 0.5 秒,非法移动时额外停顿,营造逐帧动画效果。 - 程序在启动时打开
log.txt(L61),每回合将棋盘以|分隔的行格式写入并fflush刷盘(L83-L95),用于自动化测试与回溯。仓库中已有的 log.txt 正是 3×3 棋盘的一局真实运行记录,可以看到从初始状态到逐步移动的完整轨迹:
8|7|6 5|4|3 2|1|0 8|7|6 5|4|3 2|0|1 ...四、初始化:init() 与可解性处理
init函数(L159-L178)负责生成初始局面:
- 从
d*d - 1开始逆序填充:第一行放d*d-1, d*d-2, ...,最后一行末尾自然落到 0(空位); - 关键细节:当
d为偶数时,交换倒数第二块与倒数第三块瓷砖(即board[d-1][d-3]与board[d-1][d-2]):if (d % 2 == 0) { int temp = board[d-1][d-3]; board[d-1][d-3] = board[d-1][d-2]; board[d-1][d-2] = temp; }
从滑块谜题的数学性质可以推断:奇偶性不同的初始排列拥有不同的可解性。若直接按 1..15 逆序平铺,偶数尺寸棋盘会得到一个不可解的排列;代码在偶数维度时交换最后两块,正是为了把初始局面调整为可解状态——这与 15-puzzle 经典的"逆序数 + 空格行号"可解性判据相呼应。对 3×3 棋盘(d=3,奇数),代码不做交换,初始为:
8 7 6 5 4 3 2 1 _这与 log.txt 首帧完全一致。
五、棋盘绘制:draw() 与 clear() 的 ANSI 清屏
draw(L182-L200)逐行打印当前棋盘:
- 空位(值为 0)显示为
_; - 其他瓷砖以
%2i右对齐两位打印,保证棋盘视觉对齐。
clear(L139-L143)使用 ANSI 转义序列完成清屏:
printf("\033[2J"); // 清空整个屏幕 printf("\033[%d;%dH", 0, 0); // 光标移动到 (0,0)这意味着程序依赖支持 ANSI 转义的终端(Linux / macOS 终端均可,Windows 需借助 Windows Terminal 或 WSL 等环境)。
六、移动判定:move() 的边界问题与合法移动
move(L206-L267)的实现分两步:
- 定位:两轮双重循环分别找出目标瓷砖
tile的坐标(row, column)与空位(0)的坐标(row1, column1); - 判定:依次检查空位的上、下、左、右四个相邻格,若目标瓷砖恰好在空位旁,则交换两者(空位移入原瓷砖位置、瓷砖移入空位)并返回 1;否则返回 0,主循环据此输出
Illegal move.。
值得注意的实现细节:move对四个方向依次判断board[row][column] == board[row1][column1 ± 1]或board[row1 ± 1][column1],这里没有做边界下标防护。当空位位于棋盘边缘时,column1 + 1、column1 - 1、row1 - 1、row1 + 1可能越界访问。从源码结构看,这一版本依赖"非法越界匹配必然失败"的巧合来返回 0,实际存在未定义行为的隐患;move的函数注释也以// TODO标记其未完成状态。在真实棋盘(3×3 及以上、空位在角落)上,move会越界读取相邻内存,可能导致非法移动被误判为合法。这是该实现留给读者的一个改进点:更稳妥的做法是先检查row1/column1的边界再访问相邻元素。
七、胜利判定:won()
won(L273-L292)按行主序遍历棋盘:
int counter = 0; for (int i = 0; i < d; i++) for (int j = 0; j < d; j++) if (++counter != (d * d) && board[i][j] != counter) return 0; return 1;- 期望每个格子依次为 1, 2, 3, ..., d*d-1,最后一块(
counter == d*d,即右下角)应为空位 0,因此跳过对它的比较; - 只要出现一处错位即返回 0;全部符合则返回 1,主循环打印
ftw!并结束游戏。
以 3×3 为例,胜利局面为:
1 2 3 4 5 6 7 8 _八、编译、运行与测试
模块自带的 makefile 提供了标准构建流程:
fifteen: fifteen.c clang -ggdb3 -O0 -std=c11 -Wall -Werror -o fifteen fifteen.c -lcs50 -lm clean: rm -f *.o a.out core fifteen log.txt- 编译命令使用
clang,开启-Wall -Werror(所有警告视为错误)、-ggdb3(完整调试信息)、-std=c11; - 注意链接了
-lcs50(CS50 库,实为与哈佛 CS50 课程配套的库)与-lm(数学库)。如果你的环境没有安装libcs50,需要将命令简化为clang -ggdb3 -O0 -std=c11 -Wall -Werror -o fifteen fifteen.c后编译运行; make clean会清除产物以及运行产生的log.txt。
运行 4×4 版本(经典 15-puzzle):
make ./fifteen 4启动后终端显示欢迎语与初始棋盘,随后每回合输入要移动的瓷砖编号(输入 0 退出)。移动过程中 log.txt 会持续记录每一步的棋盘快照,可用于脚本化验证:例如对 3×3 依次输入 1、2、3……若每一步都合法,则最终可收敛到胜利局面并打印ftw!。
九、总结与延伸
本文从 readme.md 的规则定义出发,结合 fifteen.c、makefile 与 log.txt 逐层拆解了 15-puzzle 的完整实现:从 d×d 棋盘抽象、逆序初始化与偶数棋盘可解性处理,到 ANSI 清屏、输入输出主循环、移动合法性判定和顺序胜利检查。该模块是理解滑块谜题状态空间、数组操作与终端交互的优秀范例;若想继续深挖,可在仓库的 divide_conquer/src/inversion_count 中找到逆序数计数实现——它正是 15-puzzle 可解性判据背后的数学工具。
- 教程
- 示例工程
【免费下载链接】cosmos
World's largest Contributor driven code dataset | Used in Quark Search Engine, @OpenGenus IQ, OpenGenus Visual Project
相关推荐
Flipper Zero 15 拼图游戏源码解析:Game 15 的玩法、存档与渲染实现
Flipper Zero 15 拼图游戏源码解析:Game 15 的玩法、存档与渲染实现 导读 本文基于本仓库中收录的 Game 15(经典数字华容道 / 15
示例工程Flipper Zero 数字华容道 Game 15:玩法、界面与 C 源码实现全解析
Flipper Zero 数字华容道 Game 15:玩法、界面与 C 源码实现全解析 Game "15"(十五数字华容道)是运行在 Flipper Zero
示例工程SpacetimeDB 模块开发速查表:Rust / C / TypeScript / C++ 四语言语法对照与源码解读
SpacetimeDB 模块开发速查表:Rust / C / TypeScript / C++ 四语言语法对照与源码解读 本指南是 SpacetimeDB 服务
数据库关系型数据库后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考