Project 0:2048¶
截止时间:2021 年 1 月 29 日¶
简介¶
本项目的高层概览视频位于:https://youtu.be/Xzihuj_JZBI。
本项目的目的是让你有机会熟悉 Java,以及课程使用的各种工具,例如 IntelliJ IDE,以及用于编写和运行单元测试的 JUnit。虽然你会在 proj0 文件夹中看到许多文件和大量代码,但任务只位于 Model.java 中,而且仅限于四个方法。
评分只取决于程序是否能够按照测试要求正确工作,以及你是否提交了指定内容。本项目没有隐藏测试。之后的作业还会对代码风格进行评分,但本项目不会。不过,我们仍然建议遵循课程的 style61b 指南,因为它会帮助你编写整洁的代码,只是本项目不会据此扣分。
这份作业规格相当长,而且起始代码很多。我们建议你在开始编程之前完整阅读规格。刚开始时,它很可能会让你觉得不知所措。为了彻底理解内容,你可能需要多次重新阅读规格中的某些部分;而且在完成项目较早的部分之前,后面的某些内容可能不会完全说得通。
最终,我们希望这段经历能让你获得一种力量感:你成功驾驭了这样一项庞大的任务。
游戏¶
你很可能见过,甚至玩过 “2048”。这是 Gabriele Cirulli 编写的一款单人电脑游戏,它又基于 Veewo Studio 更早的游戏 “1024”(可以查看他的 2048 在线版本)。
在本项目中,你将构建这个游戏的核心逻辑。也就是说,我们已经完成了所有 GUI 代码、按键处理以及大量其他脚手架。你的工作将是完成其中最重要、也最有趣的部分。
具体来说,你将填写 Model.java 文件中的 4 个方法,它们控制用户按下某些按键之后会发生什么。
游戏本身相当简单。游戏在一个 \(4\times4\) 的方格网格上进行,每个方格可以为空,也可以包含一块写有某个整数的方块;这个整数是大于等于 2 的 2 的幂。在第一次移动之前,应用程序会在最初为空的棋盘上随机选择一个方格,并添加一块值为 2 或 4 的方块。2 或 4 的选择是随机的:选择 2 的概率为 75%,选择 4 的概率为 25%。
然后,玩家通过方向键选择一个方向来倾斜棋盘:北、南、东或西。所有方块都会沿该方向滑动,直到运动方向上不再有空位(也可能一开始就没有空位)。一个方块可能会与另一个方块合并,从而为玩家获得分数。
下面的 GIF 展示了进行几步移动后的效果。

下面是上图所展示的、发生合并时需要遵循的完整规则。
- 两块数值相同的方块会合并成一块数值为原来两倍的方块。
- 一次倾斜中,由合并产生的方块不会再次合并。例如,如果有
[X, 2, 2, 4],其中 X 表示空位,并把方块向左移动,结果应当是[4, 4, X, X],而不是[8, X, X, X]。这是因为最左侧的 4 已经参与过一次合并,所以不应当再次合并。 - 如果沿运动方向有三个相邻方块具有相同数值,那么运动方向上靠前的两块会合并,而后面的那块不会。例如,如果有
[X, 2, 2, 2]并向左移动,结果应当是[4, 2, X, X],而不是[2, 4, X, X]。
作为这些规则的推论,如果沿运动方向有四块相邻方块具有相同数值,它们会形成两块合并后的方块。例如,如果有 [4, 4, 4, 4] 并向左移动,结果是 [8, 8, X, X]。这是因为根据规则 3,靠前的两块会合并,之后后面的两块也会合并;但根据规则 2,这两块新合并出的方块(本例中的 8)不会在这次倾斜中继续彼此合并。
上面的动画 GIF 中包含了上述三条规则各自的应用,因此请多看几遍,确保充分理解这些规则。
为了测试理解,你应当完成课程提供的 Google Form 小测。这个小测不计入 61B 课程成绩。
如果一次倾斜没有改变棋盘状态,就不会随机生成新方块。否则,程序会在一个空方格上添加一块随机生成的方块。注意:你的代码不会负责添加任何新方块!这一部分我们已经替你完成。
你可能还会注意到屏幕底部有一个 “Score” 字段,它会随着移动而更新。分数不会每一步都发生变化,而只会在两块方块合并时变化。你的代码需要更新分数。
每当两块方块合并成一块更大的方块,玩家会获得等于新方块数值的分数。当当前玩家没有任何可用移动(任何方向的倾斜都无法改变棋盘),或者一次移动形成了值为 2048 的方块时,游戏结束。你的代码负责检测游戏何时结束。
“Max Score” 是用户在当前游戏会话中取得过的最高分。只有游戏结束时它才会更新,因此在上面的动画 GIF 示例中,它始终保持为 0。
作业理念与程序设计¶
本节规格的视频概览位于:https://youtu.be/3YbIOga6ZdQ。
在这个项目中,我们提供了大量起始代码,其中使用了许多尚未讲解的 Java 语法,甚至还有一些本课程永远不会讲到的语法。
这里的想法是:在真实世界中,你经常会面对自己并未完全理解的代码库,需要通过试验和摸索来获得想要的结果。不要担心;下周进入 Project 1 时,你会有机会从头开始编写代码。
下面将说明 Paul Hilfinger 创建的骨架代码背后的一些架构思想。你不需要理解每一个细节,不过这些内容可能会很有意思。
骨架代码体现了两种常见设计模式:Model-View-Controller 模式(MVC)和 Observer 模式。
MVC 模式把问题划分为三个部分:
- Model 表示正在被描述和操作的主题;在本项目中,它包含棋盘游戏的状态以及修改状态的规则。我们的 Model 位于
Model、Side、Board和Tile类中。Model的实例变量可以完全确定游戏状态。注意:你只会修改Model类。 - View 是模型的视图,它向用户显示游戏状态。我们的 View 位于
GUI和BoardWidget类中。 - Controller 是游戏控制器,它把用户操作转换为对模型的操作。我们的 Controller 主要位于
Game类中,不过它也会使用 GUI 类读取按键。
MVC 模式不是 61B 的课程主题,考试或未来项目中也不会要求你了解或理解这种设计模式。
骨架使用的第二种模式是 “Observer 模式”。简单地说,这意味着 Model 实际上不会主动把变化报告给 View。相反,View 会把自己注册为 Model 对象的观察者。这是一个略微高级的话题,因此这里不提供更多信息。
现在介绍你将接触的各个类。
Tile¶
这个类表示棋盘上带数字的方块。如果一个 Tile 类型的变量为 null,它会被视为棋盘上的空方格。你不需要创建任何 Tile 对象,不过由于会在 Model 类中使用它们,你需要理解它们。你需要使用这个类的唯一方法是 .value(),它会返回给定方块的数值。例如,如果 Tile t 对应一个数值为 8 的方块,那么 t.value() 会返回 8。
Side¶
Side 类是一种特殊类型的类,称为 Enum。枚举与普通类类似,但功能受到限制。具体来说,枚举只能取有限集合中的某个值。在这里,四个方向各自对应一个值:NORTH、SOUTH、EAST 和 WEST。你不需要使用这个类的任何方法,也不需要操作它的实例变量。
枚举可以使用类似 Side s = Side.NORTH 的语法赋值。注意,我们不是使用 new 关键字,而是直接把 Side 值设为四个值之一。类似地,假如有函数 public static void printSide(Side s),可以用 printSide(Side.NORTH) 调用它,把值 NORTH 传给函数。
如果想进一步了解 Java 枚举,请查看 Java Enum 教程。
Model¶
这个类表示游戏的完整状态。一个 Model 对象表示一局 2048。它拥有表示棋盘状态的实例变量(例如所有 Tile 对象的位置、分数等),还拥有多种方法。当你做到项目第四个也是最后一个任务(编写 tilt 方法)时,一项挑战就是判断哪些方法和实例变量有用。
Board¶
这个类表示方块棋盘本身。你将使用它的三个方法:setViewingPerspective、tile 和 move。作为可选试验,也可以使用 getRandomNonNullTile。
本作业中,你只会编辑 Model.java 文件。Gradescope 只会获取你的 Model.java,并使用其他文件的骨架版本;因此,如果修改了 Tile.java,Gradescope 不会识别这些修改。
开始项目¶
首先,确保已经完成 Lab 1。如果没有完成 Lab 1 要求的全部必要配置,就无法进行这个项目。
获取骨架文件¶
首先,确保仓库中的所有内容都已正确更新并提交。开始之前,在 sp21-s*** 目录中运行:
命令应当报告目录是干净的,而且没有任何需要添加和提交的未跟踪文件。如果存在,就把它们添加并提交。
永远不要在没有完成这件事的情况下开始一个新项目。
要获取骨架文件,请在 sp21-s*** 目录中使用:
现在,你会在学生仓库中看到一个 proj0 文件夹,其中包含所有骨架代码。
在极少数我们必须更新骨架的情况下,可以使用同一条命令,把相同的更新合并到项目中。
重新开始:Skeleton¶
与其努力让当前代码工作,你有时可能想完全重新开始。通过 Git 可以做到这一点!只需在 sp21-s*** 目录中运行:
注意:这条命令会清除 proj0 目录中所有尚未提交的更改。因此,如果认为当前代码以后可能还有用,请先创建一个提交,再运行这条命令。之后,你可以使用类似命令把 proj0 目录恢复为刚才创建的提交中的状态。
IntelliJ 配置¶
现在在 IntelliJ 中打开文件。首先启动 IntelliJ。它会显示最近项目列表,但由于尚未开始这项作业,项目不会出现在其中。要打开项目,请单击应用窗口右上角的 “Open” 按钮,这会打开操作系统的文件浏览器。进入学生仓库中的 proj0 文件夹,然后单击打开:


屏幕左上角会显示 proj0 目录中的文件和文件夹列表。如果没有看到,请单击 proj0 文件夹旁的向下箭头来展开。它应当如下所示:

.idea 文件夹由 IntelliJ 生成,用于存储各种设置,可以忽略。
game2048 文件夹是所有 Java 文件的源目录。你需要完成的所有工作都位于这里。
javalib 文件夹是一个 Java 库。它包含三个 .jar 文件,即我们想使用的预编译文件。这三个文件让我们能够运行 JUnit,还提供一些 UC Berkeley 特有的功能,以便在漂亮的窗口中渲染游戏。
IntelliJ 通常足够聪明,能够自动完成其余配置;不过,如果你的 IntelliJ 遇到困难,下面会逐步说明配置过程。
首先,告诉 IntelliJ 我们使用 Java 15。进入 File > Project Structure:

单击 “Edit” 按钮左侧的方框,从中选择 JDK,也就是 Java 版本。

接下来,告诉 IntelliJ 我们要使用 javalib 文件夹中的 .jar 文件。仍然位于 Project Structure 中时,在左侧单击 Project Settings 下的 “Library”。如果已经看到 javalib 被添加,就不需要做任何事。否则,单击 “+” 按钮,再选择 “Java”,这会打开操作系统的文件浏览器;然后选择 javalib 文件夹。最后,单击屏幕右下角的 “Apply”,再单击蓝色 “OK” 按钮。
整个配置过程大致如下(抱歉,图片比较模糊):

为了确保配置正常,打开 game2048 文件夹,并右键单击 Java 文件 Main。你会看到若干选项,我们关心的是绿色的 “Run Main.main()” 按钮。它应当如下图所示:

单击它启动 2048 游戏。一个带有空棋盘的新窗口会出现。现在先关闭窗口;到规格中 “主要任务:构建游戏逻辑” 一节时,我们还会回来。
如果什么都没有出现,说明配置不正确。重新完成上面的步骤,确保没有遗漏,不过不要在这件事上花超过 10 分钟。配置问题最好在 TA 帮助下解决,也就是说,应当在 Ed 上发帖或前往 Office Hours。如果在 Ed 上发帖,需要告诉我们已经完成和尝试过的全部操作,以便清楚了解错误。请附上所有内容的截图,尤其是收到的错误消息。
你可能遇到一个奇怪现象:代码能够正确编译和运行,但 IntelliJ 仍然显示红色下划线。进入 Model 类,找到 addTile 方法。这个方法由我们提供,但你可能看到 tile 变量下面有红线,并出现如下错误消息:

不过我们清楚地知道它是正确的,因为 1)代码能够运行,2)这是起始代码!IntelliJ 虽然非常强大,但有时也会像这样判断错误。要修复它,进入 File > Invalidate / Restart,再在出现的窗口中单击 “Invalidate and Restart”。

IntelliJ 会重新索引 JDK,并从头配置项目,这需要一两分钟。完成后,源文件中应当不再有红色下划线。
只有上述配置正常,才能完成项目,因此请把尽快完成配置作为首要任务。
你的任务¶
本项目的工作是修改并完成 Model 类,具体包括 emptySpaceExists、maxTileExists、atLeastOneMoveExists 和 tilt 方法。其他所有内容已经为你实现。我们建议按这个顺序完成。前两个相对直接,第三个(atLeastOneMoveExists)更困难,最后的 tilt 方法可能会相当困难。我们预计 tilt 需要 3 到 10 小时完成。
前三个方法处理游戏结束条件,最后的 tilt 方法会在用户按键后修改棋盘。阅读非常短的 checkGameOver 方法体,可以了解这些方法如何用于判断游戏是否结束。
先看前三个方法。
public static boolean emptySpaceExists(Board b)¶
如果给定棋盘中任何方块为 null,这个方法应当返回 true。本项目中绝对不要以任何方式修改 Board.java。在这个方法中,你会需要使用 Board 类的 tile(int col, int row) 和 size() 方法,不需要其他方法。
注意:我们在设计 Board 类时使用了特殊关键字 private,它不允许你直接访问 Board 的实例变量。例如,如果尝试访问 b.values[0][0],代码不会工作。这是一件好事!它会迫使你学习使用 tile 方法,而整个项目的其余部分也会一直使用这个方法。
尝试打开 TestEmptySpace.java 并运行测试。你应该会看到 6 个测试失败、2 个测试通过。正确编写 emptySpaceExists 后,TestEmptySpace 中全部 8 个测试都应当通过。
有关怎样开始编写这个方法的快速概览,请查看课程提供的视频。
public static boolean maxTileExists(Board b)¶
如果棋盘中任何方块的数值等于获胜方块值 2048,这个方法应当返回 true。注意,不要在代码中硬编码常量 2048,而应当使用 MAX_PIECE,它是 Model 类的一部分。换句话说,不要写 if (x == 2048),而应当写 if (x == MAX_PIECE)。
在代码中保留 2048 这样的硬编码数字是一种不良编程实践,有时称为“魔法数字”。魔法数字的危险在于,如果修改了代码中某处的数字,却没有修改另一处,可能会得到意料之外的结果。使用 MAX_PIECE 这样的变量,可以确保所有位置一起改变。
编写方法后,TestMaxTileExists.java 中的测试应当通过。
public static boolean atLeastOneMoveExists(Board b)¶
这个方法更有挑战性。如果存在任何有效移动,它应当返回 true。“有效移动”是指:在玩 2048 时,如果用户能够按下某个按键(UP、DOWN、LEFT 或 RIGHT),并使至少一块方块移动,那么这次按键就是有效移动。
存在有效移动的情况有两种:
- 棋盘上至少有一个空位。
- 有两块相邻方块具有相同数值。
例如,对于下面的棋盘,应当返回 true,因为至少有一个空位。
对于下面的棋盘,应当返回 false。无论在 2048 中按下哪个按钮,什么都不会发生;也就是说,没有两块相邻方块具有相同数值。
对于下面的棋盘,应当返回 true,因为向左或向右移动会合并两块 64,而向上或向下移动会合并两块 32。换句话说,至少存在两块数值相同的相邻方块。
编写方法后,TestAtLeastOneMoveExists.java 中的测试应当通过。
主要任务:构建游戏逻辑¶
作业的第四个也是最后一个部分是实现 tilt。只有在 TestEmptySpace、TestMaxTileExists 和 TestAtLeastOneMoveExists 的全部测试都通过之后,才应当开始这个方法。
计算机科学本质上只关乎一件事:管理复杂性。编写 tilt 方法是一段丰富的经历,会让你有机会亲自尝试管理复杂性。我必须警告你,这很可能会是一次令人沮丧的经历。你可能会尝试若干种最终失败的方法,然后不得不重新开始。
在讨论 tilt 应当怎样工作之前,先尝试运行游戏。
打开 Main 类并单击运行按钮。游戏窗口应当出现。尝试按方向键,你会看到什么都没有发生。这是因为尚未实现 tilt 方法。完成 tilt 后,就可以玩游戏了。
public boolean tilt(Side side)¶
tilt 方法负责真正移动棋盘上的所有方块。例如,如果棋盘是:
并按下向上键,tilt 会修改 board 实例变量,使游戏状态变成:
除了修改棋盘,还有两件事必须发生:
- 必须更新
score实例变量,使其反映所有方块合并的总价值(如果有)。在上面的例子中,两块 4 合并为 8,两块 2 合并为 4,因此分数应当增加 8 + 4 = 12。 - 如果棋盘有任何变化,必须把局部变量
changed设为true。这是因为在tilt的骨架代码末尾可以看到,我们调用了setChanged()方法;它会通知 GUI 有内容需要绘制。你自己不会调用setChanged,只需要修改局部变量changed。
棋盘上的所有方块移动都必须使用 Board 类提供的 move 方法完成。访问棋盘上的所有方块都必须使用 Board 类提供的 tile 方法。由于 GUI 实现中的一些细节,在一次 tilt 调用中,对给定方块应当只调用一次 move。本文的 Tips 部分会进一步讨论这个限制。
课程视频中提供了怎样开始编写这个方法的快速概览。
提示¶
我们强烈建议一开始只考虑向上方向,也就是参数 side 等于 Side.NORTH 的情况。为了支持这一过程,我们提供了 TestUpOnly 类,其中包含四个测试:testUpNoMerge、testUpBasicMerge、testUpTripleMerge 和 testUpTrickyMerge。你会注意到,这些测试都只涉及一次向上移动。
考虑怎样实现向上移动时,请注意以下内容。
对于给定的一列,顶行(第 3 行)的方块保持原位。如果第 2 行上方的位置为空,它可以向上移动;如果上方方块与它数值相同,它也可以向上移动一格。换句话说,遍历各行时,从第 3 行开始向下遍历是安全的,因为一个方块移动一次之后,不可能还需要再次移动。
这听起来可能不会太难,但实际上真的很难。准备好拿出记事本,推导许多例子。努力编写优雅的代码,尽管在这个问题中很难做到优雅。我们强烈建议创建一个或多个辅助方法来保持代码整洁。例如,可以编写一个辅助函数处理棋盘中的一列,因为每一列彼此独立。也可以编写一个能够返回目标行数值的辅助函数。
提醒:对给定方块只能调用一次 move。换句话说,假设棋盘如下,并按下向上键:
一种实现方式可能如下:
Tile t = board.tile(3, 0)
board.move(3, 1, t);
board.move(3, 2, t);
board.move(3, 3, t);
setChanged();
return true;
不过,GUI 会被弄糊涂,因为同一块方块不应当在只调用一次 setChanged 的情况下移动多次。相反,需要通过一次 move 调用完成整个移动,例如:
从某种意义上说,困难的部分在于判断每块方块最终应当位于哪一行。
为了测试理解,你应当完成课程提供的 Google Form 小测。这个小测(以及后面的其他小测)完全可选,不计分,但我们强烈建议完成,因为它可以发现你对游戏机制的概念误解。可以任意多次尝试。
要判断何时更新分数,请注意:如果把方块 t 移动到列 c、行 r 会替换一块已存在的方块(也就是发生合并),board.move(c, r, t) 方法会返回 true。
看起来更糟糕的是,即使让向上方向的 tilt 工作,仍然需要为另外三个方向完成相同的事情。如果采用朴素方法,会得到大量重复、只有少量修改的代码,并产生许多引入隐蔽错误的机会。
对于这个问题,我们直接提供了一个干净解法的关键思想。它会让你只增加两行代码,就能处理另外三个方向!具体来说,Board 类拥有 setViewingPerspective(Side s) 函数,它会改变 tile 和 move 方法的行为,让它们表现得就像给定方向是 NORTH。
例如,考虑下面的棋盘:
如果调用 board.tile(0, 2),会得到 16,因为 16 位于第 0 列、第 2 行。如果调用 board.setViewingPerspective(s),其中 s 是 WEST,棋盘会表现得就像 WEST 是 NORTH;也就是说,就像你把头向左转了 90 度,如下所示:
换句话说,之前的 16 现在会位于 board.tile(2, 3)。如果拥有正确实现的 tilt,并调用 board.tilt(Side.NORTH),棋盘会变成:
要让棋盘恢复原始观察视角,只需调用 board.setViewingPerspective(Side.NORTH),这会让棋盘重新表现为 NORTH 就是 NORTH。执行后,棋盘的行为就像它是:
可以看到,这与把原始棋盘中的方块滑向 WEST 完全相同。
重要:结束 tilt 调用之前,务必使用 board.setViewingPerspective 把观察视角设回 Side.NORTH,否则会发生奇怪的事情。
为了测试理解,请尝试第三个也是最后一个 Google Form 小测。可以任意多次尝试。
测试¶
虽然未来我们会要求你能够测试自己的程序,但本项目提供了完整测试套件。
测试分布在 5 个文件中:TestEmptySpace、TestMaxTileExists、TestAtLeastOneMoveExists、TestUpOnly 和 TestModel。每个文件测试代码中的某个特定部分,只有 TestModel 例外,它会协调测试你编写的全部内容。这种测试称为集成测试,在测试中非常重要。
单元测试会隔离运行各部分,而集成测试会把所有部分一起运行,用于捕获由不同函数相互作用导致的隐蔽 bug。
因此,在通过其余测试之前,不要尝试调试 TestModel!事实上,下面讨论测试的顺序就是你应当尝试它们的顺序。
接下来逐个查看这些测试,并说明怎样阅读错误消息。
TestEmptySpace¶
这些测试检查 emptySpaceExists 方法的正确性。如果某个测试失败,错误消息大致如下:

左侧会看到所有运行过的测试列表。黄色 X 表示测试失败,绿色对勾表示测试通过。右侧会看到一些有用的错误消息。要单独查看某个测试及其错误消息,请单击左侧的测试。例如,假设我们想查看 testCompletelyEmpty 测试。

右侧现在只显示这个测试的错误消息。第一行包含一条有用信息:"Board is full of empty space",后面跟着棋盘的 String 表示。可以清楚看到棋盘为空,但 emptySpaceExists 方法返回了 false,导致测试失败。如果某个测试失败,测试代码顶部的 Javadoc 注释也包含一些有用信息。
TestMaxTileExists¶
这些测试检查 maxTileExists 方法的正确性。错误消息与 TestEmptySpace 类似,仍然可以单击每个测试来单独查看。请记住,maxTileExists 方法应当只寻找最大方块,不应当检查任何其他内容(例如不应当寻找空位)。如果你的方法这样做,就无法通过所有测试。
TestAtLeastOneMoveExists¶
这些测试检查 atLeastOneMoveExists 方法的正确性。错误消息与上面两个测试类似。由于 atLeastOneMoveExists 依赖 emptySpaceExists,在 TestEmptySpace 的全部测试通过之前,不应当期待这些测试能够通过。
TestUpOnly¶
这些测试检查 tilt 方法的正确性,但只检查向上(Side.NORTH)方向。这些测试的错误消息不同,来看一个例子。假设运行全部测试后发现 testUpTrickyMerge 失败。单击这个测试后,会看到:

第一行会说明倾斜方向(在这些测试中始终是 North),之后显示倾斜前你的棋盘是什么样子、预期棋盘是什么样子,最后显示你的棋盘实际是什么样子。
你会看到,在一次 tilt 调用中,我们让同一块方块合并了两次,结果得到一块数值为 8 的方块,而不是两块数值均为 4 的方块。因此,棋盘表示底部所显示的 score 也不正确。
对于其他测试,可能很难立刻看出预期棋盘与实际棋盘之间的区别。对于这些测试,可以单击错误消息最底部蓝色的 “Click to see difference” 文本,在一个单独窗口中并排比较预期棋盘(左侧)与实际棋盘(右侧)。对于这个测试,结果如下:

调试这些问题可能有些棘手,因为很难判断自己做错了什么。首先,应当判断违反了上面列出的三条规则中的哪一条。在这个例子中,可以看到违反的是规则 2,因为一块方块合并了多次。这些测试方法的 Javadoc 注释是很好的资源,因为它们会明确说明正在测试哪条规则或哪种配置。也可以通过查看移动前后的棋盘来判断违反了哪条规则。
之后才是棘手的部分:重构现有代码,让它正确处理这条规则。我们建议用纸笔写下代码执行的步骤,先理解棋盘为什么会变成当前样子,再设计修复方法。这些测试只调用一次 tilt,因此不必担心调试多次 tilt 调用。
TestModel¶
这些测试会一起检查所有内容的正确性。大多数测试与 TestUpOnly 中的测试类似,只调用一次 tilt;但它们还包含 gameOver 测试(会一起测试 emptySpaceExists、maxTileExists 和 atLeastOneMoveExists),以及连续多次调用 tilt 的测试。
这些测试的错误消息与 TestUpOnly 完全相同,Javadoc 注释同样有助于判断测试正在检查什么。
不需要担心测试的实际代码;你不必理解或修改任何测试,不过欢迎阅读它们,了解测试是怎样编写的。如果非常有雄心,也可以添加自己的测试。
评分¶
满分项目会通过我们提供的全部单元测试。请记住,本项目没有隐藏测试,因此,如果通过了所有这些测试,你就拥有一个满分项目!
Gradescope 上某些测试的权重略有不同,因此,如果通过了某个比例的测试,你的百分制分数可能会是另一个比例(很可能更高)。
这是因为项目中的某些部分比其他部分更困难,而且我们知道这是许多学生第一次使用 Java,因此据此设置了各部分权重。
下面列出不同完成程度大约能够获得的百分比:
- 只实现
emptySpaceExists或maxTileExists:约 27%。 - 实现除
tilt之外的全部内容:约 47%。 - 实现全部内容,但
tilt只支持 Up 方向:约 68%。 - 实现全部内容,但不支持合并:约 64%。
- 实现全部内容,但没有实现合并规则 2:约 93%。
可以看到,实现合并规则 2 只占项目的约 7%。这是因为它是一条很难处理的规则,却只涉及游戏中很小一部分情况,所以我们相应地设置了权重。
额外加分¶
在周四晚上 11:59 之前(即项目截止前一天)把最终提交交到 Gradescope 的学生会获得 2 分额外加分。“最终提交”是 Gradescope 上被激活的提交,因此截止时间后仍然可以继续提交,但只有在周四 11:59 之前的提交被设为 active 时,才会获得这 2 分。额外分会在截止时间后应用,因此项目截止之前不会显示。
提交与版本控制¶
以较高频率把工作提交到仓库非常重要。当你把某些内容弄坏,或者你的狗吃掉项目时,版本控制是拯救自己的强大工具;但只有经常使用它,它才有用。可以每 15 分钟提交一次。尽管 Git 的行为像是为整个项目创建快照,它实际上只保存发生变化的内容。
git status 命令会告诉你从上次提交以来修改、删除或可能添加了哪些文件,还会告诉你有多少内容尚未发送到 GitHub 仓库。
典型命令大致如下:
git status # To see what needs to be added or committed.
git add <filepath> # To add, or stage, any modified files.
git commit -m "Commit message" # To commit changes.
git push
之后可以继续项目,直到再次准备提交和推送,再重复上面的过程。养成频繁提交并编写有信息量的提交消息的习惯,符合你的最佳利益。这样,如果需要恢复到旧代码版本,不但可以做到,而且很容易。我们建议每当添加了一段重要代码或达到某个里程碑(例如通过一个新测试)时都创建提交。
把代码推送到 GitHub(也就是运行 git push)后,可以进入 Gradescope,找到 proj0 作业并提交代码。请注意,Gradescope 使用的是最近一次推送的提交;如果在 Gradescope 提交前没有运行 git push,测试的会是旧代码,而不是计算机上的最新代码。
获取帮助¶
少量挣扎和调试是正常的,甚至是健康的;但如果已经按照上面的所有建议尝试,仍然卡住数小时毫无进展,请向工作人员求助!
在 CS 61B 中,获得帮助的两种方式是参加 Office Hours,或者在 Ed 上发帖,让 TA 或其他学生帮助你摆脱困境。
请记住,在 Office Hours 中,TA 对每位学生最多花 10 分钟。为了加快进度,我们要求你带着一个能够清楚向 TA 表达的问题,而不是只说某个测试没有通过。例如,如果某个测试没有通过,先判断具体是哪一部分没有通过,以及为什么存在差异。也许 score 没有正确更新,或者合并没有按应有方式发生。这样会加快帮助过程,甚至可能让你自己找到 bug。
如果在 Ed 上发帖,请阅读课程的 Ed 政策,确保没有意外发布部分解法并违反学术诚信政策。除此之外,欢迎在 megathread 中进行有建设性的讨论。发帖前请先搜索问题,因为许多学生会遇到非常相似的 bug!
原始页面:https://sp21.datastructur.es/materials/proj/proj0/proj0