21.y — 第 21 章项目

向读者 Avtem 致敬,感谢他构思并合作完成了这个项目。

项目时间

让我们来实现经典游戏15 拼图

在 15 拼图中,你开始时是一个随机的 4×4 格子,其中有 15 个瓷砖编号从 1 到 15,缺少一个瓷砖。

例如

     15   1   4
  2   5   9  12
  7   8  11  14
 10  13   6   3

在这个拼图中,缺失的瓷砖位于左上角。

在游戏的每一回合中,你都会选择一个与缺失瓷砖相邻的瓷砖,并将其滑入缺失瓷砖所在的位置。

游戏的目标是滑动瓷砖,直到它们按数字顺序排列,缺失的瓷砖位于右下角

  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15   

你可以在这个网站上玩几回合。这将帮助你理解这个游戏是如何运作的以及如何实现它。

在我们的游戏中,每一回合用户将输入一个字母命令。有 5 个有效命令

  • w - 向上滑动瓷砖
  • a - 向左滑动瓷砖
  • s - 向下滑动瓷砖
  • d - 向右滑动瓷砖
  • q - 退出游戏

由于这将是一个更长的程序,我们将分阶段开发它。

还有一点:在每个步骤中,我们将呈现两件事:一个目标任务。目标定义了该步骤试图实现的结果,以及任何其他相关信息。任务提供了有关如何实现目标的详细信息和提示。

任务最初将隐藏起来,以鼓励你尝试仅使用目标和示例输出或示例程序来完成每个步骤。如果你不确定如何开始,或者感到卡住,可以取消隐藏任务。它们应该能帮助你前进。

> 步骤 #1

由于这将是一个更大的程序,让我们从设计练习开始。

作者注

如果你在程序预先设计方面经验不足,你可能会觉得这有点困难。这是预料之中的。重要的是你参与并学习,而不是你是否做得对。

我们将在后续步骤中详细介绍所有这些项目,因此如果你感到完全不知所措,请随意跳过此步骤。

目标:记录此程序的主要需求,并从高层次规划程序结构。我们将分三部分完成此操作。

A) 你的程序需要做哪些顶层事情?这里有一些可以帮助你入门

棋盘相关

  • 显示游戏棋盘

用户相关

  • 从用户获取命令

显示答案

B) 你将使用哪些主要类或命名空间来实现步骤 1 中列出的项目?另外,你的 main() 函数将做什么?

您可以创建一张图表,或者使用两个这样的表格

主要类/命名空间/主函数实现顶层项目成员
类 Board显示游戏棋盘
函数 main主游戏逻辑循环

显示答案

C) (额外加分) 你能想到任何有助于使上述实现更容易或更具凝聚力的辅助类或功能吗?

显示答案

如果你觉得这个练习很难,没关系。这里的目标主要是让你在开始动手之前思考一下你要做什么。

现在,是时候开始实施了!

> 步骤 #2

目标:能够在屏幕上显示单个瓷砖。

我们的游戏棋盘是一个 4×4 的瓷砖网格,可以滑动。因此,拥有一个表示我们 4×4 网格上的编号瓷砖或缺失瓷砖的 Tile 类将很有用。每个瓷砖都应该能够

  • 给定一个数字或设置为缺失的瓷砖
  • 确定它是否是缺失的瓷砖。
  • 以适当的间距绘制到控制台(以便在显示棋盘时瓷砖能够对齐)。请参阅下面的示例输出,了解瓷砖应如何间隔的示例。

显示任务

以下代码应该能够编译并在代码下方产生您可以看到的输出结果

int main()
{
    Tile tile1{ 10 };
    Tile tile2{ 8 };
    Tile tile3{ 0 }; // the missing tile
    Tile tile4{ 1 };

    std::cout << "0123456789ABCDEF\n"; // to make it easy to see how many spaces are in the next line
    std::cout << tile1 << tile2 << tile3 << tile4 << '\n';
    
    std::cout << std::boolalpha << tile1.isEmpty() << ' ' << tile3.isEmpty() << '\n';
    std::cout << "Tile 2 has number: " << tile2.getNum() << "\nTile 4 has number: " << tile4.getNum() << '\n';
    
    return 0;
}

预期输出(注意空格)

0123456789ABCDEF
 10   8       1 
false true
Tile 2 has number: 8
Tile 4 has number: 1

显示答案

> 步骤 #3

目标:创建一个已解决的棋盘(4×4 瓷砖网格)并将其显示在屏幕上。

定义一个表示 4×4 瓷砖网格的 `Board` 类。新创建的 `Board` 对象应处于已解决状态。要显示棋盘,首先打印 `g_consoleLines`(在下面的代码片段中定义)空行,然后打印棋盘本身。这样做将确保任何先前的输出都被推出视野,以便只有当前棋盘在控制台上可见。

为什么要以已解决状态启动棋盘?当你购买这些拼图的实体版本时,拼图通常以已解决状态开始——你必须手动将它们打乱(通过滑动瓷砖)才能尝试解决它们。我们将在程序中模仿这个过程(我们将在未来的步骤中进行打乱)。

显示任务

以下程序应该运行

// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };

// Your code goes here

int main()
{
    Board board{};
    std::cout << board;

    return 0;
}

并输出以下内容

























  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15

显示答案

> 第4步

目标:在此步骤中,我们将允许用户重复输入游戏命令,处理无效输入,并实现退出游戏命令。

以下是我们的游戏将支持的 5 个命令(每个命令都将作为单个字符输入)

  • ‘w’ - 向上滑动瓷砖
  • ‘a’ - 向左滑动瓷砖
  • ‘s’ - 向下滑动瓷砖
  • ‘d’ - 向右滑动瓷砖
  • ‘q’ - 退出游戏

当用户运行游戏时,将发生以下情况

  • (已解决的)棋盘应打印到控制台。
  • 程序应反复从用户获取有效的游戏命令。如果用户输入无效命令或冗余输入,则忽略它。

对于每个有效的游戏命令

  • 打印 "有效命令: " 和用户输入的字符。
  • 如果命令是退出命令,则同时打印 "\n\n再见!\n\n",然后退出应用程序。

由于我们的用户输入例程不需要维护任何状态,请将它们实现在名为 UserInput 的命名空间中。

显示任务

程序的输出应与以下内容匹配

























  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15
w
Valid command: w
a
Valid command: a
s
Valid command: s
d
Valid command: d
f
g
h
Valid command: q


Bye!


显示答案

> 步骤 #5

目标:实现一个辅助类,使我们更容易处理方向命令。

在完成上一步之后,我们可以接受用户的命令(以字符“w”、“a”、“s”、“d”和“q”的形式)。这些字符在我们的代码中本质上是魔法数字。虽然在我们的 UserInput 命名空间和函数 main() 中处理这些命令是可行的,但我们不希望将它们传播到整个程序中。例如,Board 类不应该知道“s”是什么意思。

实现一个名为 Direction 的辅助类,它将允许我们创建表示基本方向(上、左、下或右)的对象。operator- 应返回相反的方向,operator<< 应将方向打印到控制台。我们还需要一个成员函数,它将返回一个包含随机方向的 Direction 对象。最后,向 UserInput 命名空间添加一个函数,用于将方向性游戏命令(“w”、“a”、“s”或“d”)转换为 Direction 对象。

我们越能使用 Direction 而不是方向性游戏命令,我们的代码就越容易阅读和理解。

显示任务

最后,修改您在上一步中编写的程序,使其输出与以下内容匹配

























  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15
Generating random direction... up
Generating random direction... down
Generating random direction... up
Generating random direction... left

Enter a command: w
You entered direction: up
a
You entered direction: left
s
You entered direction: down
d
You entered direction: right
q


Bye!


显示答案

> 步骤 #6

目标:实现一个辅助类,使我们更容易索引游戏棋盘中的瓷砖。

我们的游戏棋盘是一个 4×4 的 Tile 网格,我们将其存储在 Board 类的二维数组成员 m_tiles 中。我们将使用其 {x, y} 坐标访问给定的瓷砖。例如,左上角的瓷砖坐标为 {0, 0}。其右侧的瓷砖坐标为 {1, 0}(x 变为 1,y 保持 0)。其下方一个的瓷砖坐标为 {1, 1}。

由于我们将大量使用坐标,因此创建一个名为 Point 的辅助类,用于存储 {x, y} 坐标对。我们应该能够比较两个 Point 对象是否相等和不相等。还要实现一个名为 getAdjacentPoint 的成员函数,它将 Direction 对象作为参数并返回该方向的 Point。例如,Point{1, 1}.getAdjacentPoint(Direction::right) == Point{2, 1}

显示任务

保存您上一步的 main() 函数,您将在下一步中再次用到它。

以下代码应该运行并为每个测试用例打印 true

// Your code goes here

// Note: save your main() from the prior step, as you'll need it again in the next step
int main()
{
    std::cout << std::boolalpha;
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::up)    == Point{ 1, 0 }) << '\n';
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::down)  == Point{ 1, 2 }) << '\n';
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::left)  == Point{ 0, 1 }) << '\n';
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::right) == Point{ 2, 1 }) << '\n';
    std::cout << (Point{ 1, 1 } != Point{ 2, 1 }) << '\n';
    std::cout << (Point{ 1, 1 } != Point{ 1, 2 }) << '\n';
    std::cout << !(Point{ 1, 1 } != Point{ 1, 1 }) << '\n';

    return 0;
}

显示答案

> 步骤 #7

目标:添加玩家在棋盘上滑动瓷砖的功能。

首先,我们应该更仔细地了解滑动瓷砖是如何实际运作的

给定一个看起来像这样的拼图状态

     15   1   4
  2   5   9  12
  7   8  11  14
 10  13   6   3

当用户在键盘上输入“w”时,唯一可以向上移动的瓷砖是瓷砖 2

移动瓷砖后,棋盘看起来像这样

  2  15   1   4
      5   9  12
  7   8  11  14
 10  13   6   3

所以,本质上发生的是我们将空瓷砖与瓷砖 2 交换了。

让我们将这个过程概括一下。当用户输入一个方向命令时,我们需要

  • 找到空的瓷砖。
  • 从空瓷砖开始,找到与用户输入方向相反的相邻瓷砖。
  • 如果相邻瓷砖有效(没有超出网格),则交换空瓷砖和相邻瓷砖。
  • 如果相邻瓷砖无效,则不执行任何操作。

通过向 Board 类添加一个成员函数 moveTile(Direction) 来实现此功能。将其添加到步骤 5 的游戏循环中。如果用户成功滑动了瓷砖,游戏应重新绘制更新后的棋盘。

显示任务

显示答案

> 步骤 #8

目标:在此步骤中,我们将完成游戏。随机化游戏棋盘的初始状态。另外,检测用户何时获胜,之后我们可以打印胜利消息并退出游戏。

我们需要注意如何随机化我们的拼图,因为并非每个拼图都可解。例如,这个拼图无法解开

  1   2   3   4 
  5   6   7   8
  9  10  11  12
 13  15  14

如果我们只是盲目地随机化拼图中的数字,则有可能生成一个不可解的拼图。对于实体版本的拼图,我们会通过随机滑动瓷砖直到瓷砖充分混合来随机化拼图。这种随机化拼图的解决方案是向与最初随机化时滑动的方向相反的方向滑动每个瓷砖。因此,以这种方式随机化拼图总是会生成一个可解的拼图。

我们可以让我们的程序以相同的方式随机化棋盘。

一旦用户解决了拼图,程序应打印 "\n\n你赢了!\n\n",然后正常退出。

显示任务

这是我们 15 拼图游戏的完整解决方案

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
137 评论
最新
最早 最多投票
内联反馈
查看所有评论