基于 fifteen_puzzle 模块源码,解读 15-Puzzle 数字推盘游戏的 C 语言实现与玩法机制
2026/9/24 1:39:44 网站建设 项目流程
  • 教程
  • 示例工程

【免费下载链接】cosmos

World's largest Contributor driven code dataset | Used in Quark Search Engine, @OpenGenus IQ, OpenGenus Visual Project

项目地址:https://gitcode.com/gh_mirrors/co/cosmos
点击查看免费下载

数字推盘(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)负责生成初始局面:

  1. d*d - 1开始逆序填充:第一行放d*d-1, d*d-2, ...,最后一行末尾自然落到 0(空位);
  2. 关键细节:当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)的实现分两步:

  1. 定位:两轮双重循环分别找出目标瓷砖tile的坐标(row, column)与空位(0)的坐标(row1, column1)
  2. 判定:依次检查空位的上、下、左、右四个相邻格,若目标瓷砖恰好在空位旁,则交换两者(空位移入原瓷砖位置、瓷砖移入空位)并返回 1;否则返回 0,主循环据此输出Illegal move.

值得注意的实现细节:move对四个方向依次判断board[row][column] == board[row1][column1 ± 1]board[row1 ± 1][column1],这里没有做边界下标防护。当空位位于棋盘边缘时,column1 + 1column1 - 1row1 - 1row1 + 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

项目地址:https://gitcode.com/gh_mirrors/co/cosmos
点击查看免费下载

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询