ShenQQQ的网络日志

[中英对译]趣味游戏:2048(CS61B-21sp)

上了一段时间班之后,发现自己不会盲打,打字从来都是六指禅。平时上班摸鱼聊天打打中文还凑活事,但最近在尝试学习 VIM 感觉手指实在不听使唤,再加上平时打英文单词错误率也是令人捉急。觉得这玩意还得是实践出真知。于是到网上找了个挺有意思的小项目,顺便把他们的文档翻译了一下。这个项目写起来不费什么时间,而且也很有意思,尤其单元测试部分是骨架中自带的,写的很好。

项目骨架代码地址:https://github.com/Berkeley-CS61B/skeleton-sp21/tree/master/proj0

原文档地址:https://sp21.datastructur.es/materials/proj/proj0/proj0


Introduction

A high level overview of this project can be found at https://youtu.be/Xzihuj_JZBI.

The intent of this project is to give you a chance to get familiar with Java and the various tools used in the course like the IntelliJ IDE and JUnit for writing and running unit tests. Though you’ll find many files and lots of code in the proj0 folder, your task only resides in Model.java and is constrained to just four methods.

这个项目的目的是给你们在课程中熟悉和使用 Java 和 比如 idea ,junit 这种工具来写代码和单元测试的机会。尽管你们在 proj0 的项目文件夹中会找到许多文件,但是你的任务只是修改 Model.java 文件,并且它被限制在了四个方法里

We will be grading solely on whether you manage to get your program to work (according to our tests) and to hand in the assigned pieces. There are no hidden tests. In future assignments we will also be grading you on style, but that isn’t the case with this project. We still recommend following our style61b guide as you’ll find that it helps create clean code, but you won’t be graded on it.

我们将仅仅依据您您是否设法让您的程序(依照我们的测试)来运行并提交指定的作品。这里并没有隐藏的测试。在未来的作业中,我们还会跟你的代码风格来给分,但是本次作业不会。我们依旧建议您按照我们的指南学习,你会找到能帮助你写出 clean code 的方法,但本次作业不会根据这个打分。

The spec for this assignment is quite long, and there is a lot of starter code. We recommend that you read the entire spec before you start doing any programming. It will probably feel overwhelming at first. You’ll probably need to reread sections of the spec several times to fully digest it, and some of the later parts might not make total sense until you’ve finished earlier parts of the project. Ultimately, we hope you leave this experience with a sense of empowerment that you were able to navigate such a large task.

本次作业的规范非常长,并且有很多启动代码。我们最推荐您在进行编程之前,先阅读 整个规范。它可能会让你觉得晕头转向。你可能需要反复阅读部分章节好几次才能彻底理解他,并且在你完成一些早期任务之前,一些比较晚的部分,你可能不是很好明白是什么意思。不管怎么说,我们希望你读完之后能充满力量,来克服这么一个艰难的工作。

The Game

You’ve probably seen and perhaps played the game “2048,” a single-player computer game written by Gabriele Cirulli, and based on an earlier game “1024” by Veewo Studio (see his on-line version of 2048).

你可能已经见过或者玩过这款叫做 2048 的游戏了。这是一款基于一位大哥写的更早的一款叫做 1024 的游戏的基础上,另外一位大哥写的单人电脑游戏。

In this project, you’ll be building the core logic of this game. That is, we’ve already put together all the GUI code, handle key-presses, and a ton of other scaffolding. Your job will be to do the most important and interesting part.

在这个项目里,你会写这个游戏的核心逻辑部分。为此,我们已经写了全部的 GUI 代码和键盘捕捉的部分,还有非常多的脚手架的部分。你的工作就是剩下的最重要也是最有意思的部分。

Specifically, you will fill out 4 methods in the Model.java file which governs what happens after certain key-presses from the user.

特别的,你将会填充 Model.java 文件当中用来控制用户按下键盘之后的四个方法。

The game itself is quite simple. It’s played on a $4\times4$ grid of squares, each of which can either be empty or contain a tile bearing an integer–a power of 2 greater than or equal to 2. Before the first move, the application adds a tile containing either 2 or 4 to a random square on the initially empty board. The choice of 2 or 4 is random, with a 75% chance of choosing 2 and a 25% chance of choosing 4.

游戏本身挺简单的。在一个 4*4 的方格里,每个方格要么为空,要么是一个包含大于 2 的 2的指数图块。在第一次移动之前,应用会添加一个 2 或者 4 的随机图块在初始空白板子上。是 2 还是 4 是随机的。75% 是 2 ,25% 是 4。

The player then chooses a direction via their arrow keys to tilt the board: north, south, east, or west. All tiles slide in that direction until there is no empty space left in the direction of motion (there might not be any to start with). A tile can possibly merge with another tile which earns the player points.

玩家通过方向键选择一个方向来倾斜板子。上下左右。所有图块都按照方向前进,直到在这个方向上没有运动的空间了(也可能运动不了)。一个图块可能被另外一个图块合并,并且能赚取玩家积分。

The below GIF is an example to see what the result of a few moves looks like.

2048 Examples

Here are the full rules for when merges occur that are shown in the image above.

在下面的图片里展示了当开始合并时候的全部规则

  1. Two tiles of the same value merge into one tile containing double the initial number.

两个等值的图块会被合并进一个双倍数值的图块当中

  1. A tile that is the result of a merge will not merge again on that tilt. For example, if we have [X, 2, 2, 4], where X represents an empty space, and we move the tiles to the left, we should end up with [4, 4, X, X], not [8, X, X, X]. This is because the leftmost 4 was already part of a merge so should not merge again.

一个作为结果的图块,不会同时被合并进另外一个与结果等值的图块当中。例如,我们有【 空,2,2,4 】 的一列,当我们向左移动这一列时,我们最终的结果应该是【 4,4,空,空 】,而不是【 8,空,空,空 】 。这是因为最左的 4 已经是合并的结果,不会再次合并。

  1. When three adjacent tiles in the direction of motion have the same number, then the leading two tiles in the direction of motion merge, and the trailing tile does not. For example, if we have [X, 2, 2, 2] and move tiles left, we should end up with [4, 2, X, X] not [2, 4, X, X].

当三个在一行的图块有相同的数值的时候,头部的两个图块会被合并,而在结尾的图块则不会。例如,比如我们有【 空,2,2,2 】之后向左移动,我们会得到【 4,2,空,空 】,而不是【2,4,空,空】

As a corollary of these rules, if there are four adjacent tiles with the same number in the direction of motion, they form two merged tiles. For example, if we have [4, 4, 4, 4], then if we move to the left, we end up with [8, 8, X, X]. This is because the leading two tiles will be merged as a result of rule 3, then the trailing two tiles will be merged, but because of rule 2 these merged tiles (8 in our example) will not merge themselves on that tilt. You’ll find applications of each of the 3 rules listed above in the animated GIF above, so watch through it a few times to get a good understanding of these rules.

作为这些规则的必然结果,如果在一个方向上有四个相连的同样数字的图块。他们会合并成两个图块。例如,我们有【 4,4,4,4】,之后如果我们向左移动,我们会得到【8,8,空,空】。这是因为按照规则三,开头的两个图块会被合并。然后后面的两个图块会被合并,然后因为规则2,两个8不会被再次合并。你会在上面的动图里找到全部的三个规则的应用场景。所以为了更好的理解规则,多看一会图片吧。

To test your understanding, you should complete this Google Form quiz. This quiz is not part of your 61B course grade.

如果你想测试一下你的理解,可以试试做做这组测验。

If the tilt did not change the board state, then no new tiles will be randomly generated. Otherwise, a single randomly generated tile will be added to the board on an empty square. Note: Your code will not be adding any new tiles! We’ve already done that part for you.

如果板子上的图块没有改变,那么也不会随机产生新的图块。另外,一个随机生成的新图块会被添加到板子的空白地方。注意:你的代码当中不会涉及到添加新方块的部分,我们已经帮你完成了这部分工作。

You might also notice that there is a field “Score” at the bottom of the screen that is being updated with each move. The score will not always change every move, but only when two tiles merge. Your code will need to update the score.

你可能也注意到了在屏幕底部又一个叫做分数的区域,随着你的移动而更新。这个分数不会每次移动都会发生改变,只有两个图块合并的时候才会。你的代码需要更新这个分数。

Each time two tiles merge to form a larger tile, the player earns the number of points on the new tile. The game ends when the current player has no available moves (no tilt can change the board), or a move forms a square containing 2048. Your code will be responsible for detecting when the game is over.

每次两个图块合并成一个更大的图块,玩家都会赚取新图块上的点数。游戏会在 玩家没法移动图块,或者有单个图块的分数变成 2048 时结束,你的代码在游戏结束时给出回应。

The “Max Score” is the maximum score the user has achieved in that game session. It isn’t updated until the game is over, so that is why it remains 0 throughout the animated GIF example.

最高分是在游戏会话中玩家达成的最高分。直到游戏结束之前,都不会更新,所以这是为什么在动图里是 0 。

Assignment Philosophy and Program Design

A video overview of this section of the spec can be found at https://youtu.be/3YbIOga6ZdQ.

In this project, we’re giving you a TON of starter code that uses many pieces of Java syntax that we have not covered yet, and even some syntax that we’ll never cover in our class.

在这个项目中,我们会给您非常多的启动代码,这里面可能有非常多的 Java 语法是我们还没涉及的,甚至一些语法我们永远也不会在课程中学到。

The idea here is that in the real world, you’ll often work with codebases that you don’t fully understand and will have to do some tinkering and experimentation to get the results you want. Don’t worry, when we get to project 1 next week, you’ll have a chance to start from scratch.

这里考虑的是,在真实世界中,你可能经常需要跟一些你并不能完全理解的代码库来打交道,并且你也需要不停的去刺探和实验直到你获得了你想要的结果。别担心,下周我们会开始实验1,你会有一个从头开始的机会。

Below, we describe some of the ideas behind the architecture of the given skeleton code, which was created by Paul Hilfinger. It is not important that you understand every detail, but you might find it interesting.

接下来,我们会描述一些我们提供的项目骨架当中的一些想法。这个项目骨架是一个叫保罗的大哥写的。了解这个项目的全部细节也许并不重要,但你可能会觉得这非常有意思。

The skeleton exhibits two design patterns in common use: the Model-View-Controller Pattern (MVC), and the Observer Pattern.

项目骨架展示了两种常用的设计模式:MVC 模式 和 观察者模式

The MVC pattern divides our problem into three parts:

MVC 模式把我们的问题拆分成了 三个部分:

  • The model represents the subject matter being represented and acted upon – in this case incorporating the state of a board game and the rules by which it may be modified. Our model resides in the Model, Side, Board, and Tile classes. The instance variables of Model fully determine what the state of the game is. Note: You’ll only be modifying the Model class.

“模型” 代表了 所代表的主题 和 采取的行动。在这个例子中,模型 结合了游戏的规则和被修改的板子游戏的状态。我们的模型分布在了,Model,Side,Board,Tile类里面。Model 类实例的值完全决定了游戏的状态。注意:你只需要修改 Model 类。

  • A view of the model, which displays the game state to the user. Our view resides in the GUI and BoardWidget classes.

视图是用来把游戏状态展示给用户的。GUI 和 BoardWidget 这两个类是视图层。

  • A controller for the game, which translates user actions into operations on the model. Our controller resides mainly in the Game class, although it also uses the GUI class to read keystrokes.

控制器是用来把用户操作转换到模型操作里面。在这个例子里,只有 Game 是控制层,尽管也使用到了 GUI 用来读取键盘输入。

The MVC pattern is not a topic of 61B, nor will you be expected to know or understand this design pattern on exams or future projects.

MVC 模式不是本课程的知识点,你不会在未来的考试或者项目中被要求了解这个设计模式。

The second pattern utilized is the “Observer pattern”. Basically this means that the model doesn’t actually report changes to the view. Instead, the view registers itself as an observer of the Model object. This is a somewhat advanced topic so we will provide no additional information here.

第二个被使用的设计模式是 观察者模式。简单的说,就是 模型 没有真正的把变化报告给 视图。而是,视图 作为 模型的观察者。这是一个有点高级的主题,所以我们不会在这里提供一些额外的信息。

We’ll now go over the different classes that you will interact with.

我们接下来会讲些你会实际交互的类。

Tile

This class represents numbered tiles on the board. If a variable of type Tile is null, it’s treated as an empty tile on the board. You will not need to create any of these objects, though you will need have an understanding of them since you will be using them in the Model class. The only method of this class you’ll need to use is .value() which returns the value of the given tile. For example if Tile t corresponds to a tile with the value 8, then t.value() will return 8.

这个类代表了板子上的数字图块。如果 Tile 对象的变量值 为 null,这会被在板子上当成一个空图块对待。你不需要创建任何这些对象,尽管你需要在使用 Model 类的时候理解这些。你唯一需要用到的方法是 values()方法,这个方法会返回图块的值。举个例子,如果 Tile t 对象对应的图块数值是 8,然后 t.values() 返回的值就是 8

Side

The Side class is a special type of class called an Enum. An enum is similar has restricted functionality. Specifically, enums may take on only one of a finite set of values. In this case, we have a value for each of the 4 sides: NORTH, SOUTH, EAST, and WEST. You will not need to use any of the methods of this class nor manipulate the instance variables.

Side 类是一个特别的类被称作 Enum。枚举功能受限。特别的,枚举只能获得有限的集合当中的值。因此,我们有四种 Side 对象:北,南,西,东。你不用使用这里面的任何方法,也不需要操作实例变量。

Enums can be assigned with syntax like Side s = Side.NORTH. Note that rather than using the new keyword, we simply set the Side value equal to one of the four values. Similarly if we have a function like public static void printSide(Side s), we can call this function as follows: printSide(Side.NORTH), which will pass the value NORTH to the function.

枚举可以用一种 Side s = Side.NORTH 的语法来赋值。注意,我们没有使用 new 关键字,我们只是简单的把 四个值当中的一个 赋给了 Side 类。

If you’re curous to learn more about Java enums, see https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html.

Model

This class represents the entire state of the game. A Model object represents a game of 2048. It has instance variables for the state of the board (i.e. where all the Tile objects are, what the score is, etc) as well as a variety of methods. One of the challenges when you get to the fourth final task of this project (writing the tilt method) will be to figure out which of these methods and instance variables are useful.

这个类代表了整体的游戏状态。Model 类代表了 2048 游戏。它是板子状态的实例化变量和一系列方法。当你开始这四个任务时其中一个挑战就是判断那些方法和变量是有用的。

Board

This class represents the board of tiles itself. It has three methods that you’ll use: setViewingPerspective, tile, move. Optionally, for experimentation, you can use getRandomNonNullTile.

**这个类代表了图块板子本身。这里有仨你需要使用的方法:setViewingPerspective, tile, move。可选的是,用于试验,你可以使用 getRandomNonNullTile 方法。

You will only edit the Model.java file in this assignment. Gradescope will only take your Model.java file and use the skeleton versions of the other files, so if you make an edit to Tile.java for example, it will not be recognized by Gradescope.

在本次任务中,你只需要修改 Model 对象 。打分系统会只拿你的 Model 文件,之后再用其他版本的骨架文件。所以如果你改了其他文件,也不会被打分系统收录。

Getting Started

Firstly, ensure you’ve completed lab 1. You will not be able to work on the project if haven’t completed all the necessary set up that you’re required to do in lab 1.

Getting the skeleton files

First, make sure that everything in your repository is properly updated and checked in. Before you start, the command when done in your sp21-s*** directory should report that the directory is clean and that there are no untracked files that should be added and committed. If there are, simply add and commit.

首先,确保你的仓库当中的所有文件已经更新并且签入了。在你开始之前,你的文件夹下面得是干净的,并且不存在应该被提交的文件还提交。这样的话,你就只需要简单的提交了。

Never start a new project without doing this.

请永远不要没检查这些东西之前就开始一个新项目。

To obtain the skeleton files, you should use the command

为了获取骨架文件,你需要使用 git clone 命令。

in your sp21-s*** directory. You’ll see a folder proj0 containing all of the skeleton code is now in your student repo.

在你的文件夹下面,你就会看到 proj0 文件夹包含所有骨架代码了

In the unlikely event that we must update the skeleton, you can use the same command to update your project with the same changes.

如果万一我们必须更新骨架,你可以使用相同的命令来更新骨架。

Getting restarted: Skeleton

Rather than trying to get your current code to work, you might find yourself wanting to just restart completely. That is possible with Git! Simply run this command in your sp21-s*** directory:

当你想彻底重来,用Git就能完全实现这一点。只要在你的文件夹下面跑这个命令就行。

git checkout skeleton/master -- proj0

Beware: this command will get rid of all changes to anything in the proj0 directory that you haven’t committed. So if you think you might want the code you currently have, simply make a commit before running this command, and then you can use a similar command to revert your proj0 directory to the state it was in the commit you just made.

当心:这个命令会清除所有未提交的改动。所以当你认为你可能需要你当前的代码时,请先提交,然后再使用 revert 命令来恢复你的仓库状态。

Your assignment

Your job for this project is to modify and complete the Model class, specifically the emptySpaceExists, maxTileExists, atLeastOneMoveExists and tilt methods. Everything else has been implemented for you. We recommend completing them in this order. The first two are relatively straightforward. The third (atLeastOneMoveExists) is harder, and the final method tilt will probably be quite difficult. We anticipate that tilt will take you 3 to 10 hours to complete. The first three methods will handle the game over conditions, and the final method tilt will modify the board after key-presses from the user. You can read the very short body of the checkGameOver method to get an idea of how your methods will be used to check if the game is over.

在这个项目当中,你的工作就是修改并完成 Model 类,特别是 emptySpaceExists, maxTileExists, atLeastoneMoveExists 和 tilt 方法。所有其他的都已经被实现好了。我们推荐你按照顺序来完成。前两个方法直接依赖,第三个方法比较难,最后一个方法确实挺难。我们估计你得用 4 到 10 个小时来完成中以后一个方法。第一个,第三个方法会处理游戏结束条件,tilt 方法是在用户操作之后修改板子的。你可以读读 checkGameOver 方法来想想怎么判断游戏是不是结束了。

Let’s start by looking at the first three methods:

让我们先看看前三个方法。

public static boolean emptySpaceExists(Board b)

This method should return true if any of the tiles in the given board are null. You should NOT modify the Board.java file in any way for this project. For this method, you’ll want to use the tile(int col, int row) and size() methods of the Board class. No other methods are necessary.

这个方法当板子上所有图块都是空的时候会返回 true 。在这个项目里,你不该用任何方法直接改 Board 文件。在这个文件里面,你会使用Board 类当中的 tile 和 size 方法。不需要其他的方法了。

Note: We’ve designed the Board class using a special keyword private that disallows you from using the instance variables of Board directly. For example, if you try to access b.values[0][0], this will not work. This is a good thing! It forces you to learn to use the tile method, which you’ll use throughout the rest of the project.

注意:我们实现的 Board 类使用了 private 关键字,并且不允许你直接使用 Board 当中的变量。例如,如果你尝试使用 b.values[0][0] , 这不会工作的。这是个好事,这强制你了解在接下里的项目里一直在使用的 tile 方法。

Try opening the TestEmptySpace.java folder. Run the tests. You should see that 6 of the tests fail and 2 of them pass. After you’ve correctly written the emptySpaceExists method, all 8 tests in TestEmptySpace should pass.

打开 TestEmptySpace 文件,跑下单测。你会发现两个通了, 六个没过。在你写完这个方法之后,八个都该过。

A quick overview of how to get started writing this method is provided in this video.

public static boolean maxTileExists(Board b)

This method should return true if any of the tiles in the board are equal to the winning tile value 2048. Note that rather than hard coding the constant 2048 into your code, you should use MAX_PIECE, which is a constant that is part of the Model class. In other words, you shouldn’t do if (x == 2048) but rather if (x == MAX_PIECE).

当板子上的任一图块等于胜利数值 2048 时,这个方法会返回 true。注意,你不应该在你的代码当中硬编码 2048,你应该使用 MAX_PIECE ,用Model类当中常量。另外,你也不应该 用 类似 if (x==2048) 这种方式,而是应该 if (x == MAX_PIECE)

Leaving in hard coded numbers like 2048 is a bad programming practice sometimes referred to as a “magic number”. The danger of such magic numbers is that if you change them in one part of your code but not another, you might get unexpected results. By using a variable like MAX_PIECE you can ensure they all get changed together.

留下 2048 这种 魔法值是一个写代码当中的坏习惯。魔法值的坏处在于,当你修改了其中一个而没改另外一个,你可能会得到难以预料的结果。使用统一的常量,你能确保你一下子把这些全改了。

After you’ve written the method, the tests in TestMaxTileExists.java should pass.

在你写完这个之后,TestMaxTileExists 的测试应该都能过了。

public static boolean atLeastOneMoveExists(Board b)

This method is more challenging. It should return true if there are any valid moves. By a “valid move”, we mean that if there is a button (UP, DOWN, LEFT, or RIGHT) that a user can press while playing 2048 that causes at least one tile to move, then such a keypress is considered a valid move.

这个方法写起来要难一点。当你能移动图块的时候,这个方法应该返回真。“能移动图块”,我们定义为,当玩家在玩2048按上下左右键的时候,起码有一个图块是得动的,这样就叫“能移动图块”。

There are two ways that there can be valid moves:

有两种方法来判断算不算“能移动图块”

  1. There is at least one empty space on the board.

板子里至少有一块空地

  1. There are two adjacent tiles with the same value.

两个挨着的图块有相同的数字。

For example, for the board below, we should return true because there is at least one empty space.

举个例子,下面的板子,因为有空地,所以应该返回true。

|   2|    |   2|    |
|   4|   4|   2|   2|
|    |   4|    |    |
|   2|   4|   4|   8|

For the board below, we should return false. No matter what button you press in 2048, nothing will happen, i.e. there are no two adjacent tiles with equal values.

下面的板子,因为怎么按上下左右都不会动,所以应该返回 false。

|   2|   4|   2|   4|
|  16|   2|   4|   2|
|   2|   4|   2|   4|
|   4|   2|   4|   2|

For the board below, we would return true since a move to the right or left would merge the two 64 tiles, and also a move up or down would merge the 32 tiles. Or in other words, there exist at least two adjacent tiles with equal values.

下面的板子,因为有两个挨着的图块值一样,所以应该返回真。

|   2|   4|  64|  64|
|  16|   2|   4|   8|
|   2|   4|   2|  32|
|   4|   2|   4|  32|

After you’ve written the method, the tests in TestAtLeastOneMoveExists.java should pass.

你写完这个方法之后,TestAtleastOneMoveExists 里的测试应该全能跑通了。

Main Task: Building the Game Logic

The fourth and final part of the assignment is to implement tilt. You should only start this method once you’re passing all the tests in TestEmptySpace, TestMaxTileExists and TestAtLeastOneMoveExists.

第四个也是最后一部分任务是实现 tilt 方法。你应该开始方法一次,之后就通过全部的 TestEmptySpace , TestMaxTileExists, TestAtLeastOneMoveExists 测试

Computer science is essentially about one thing: Managing complexity. Writing the tilt method is a rich experience that will give you a chance to try just that. I must warn you, this is probably going to be a frustrating experience. It is likely that you will attempt several approaches that will ultimately fail before you have to start back over.

计算机科学本质上是 管理复杂度 的学科。写 tilt 方法将会通过给你一个这样的尝试机会来提供丰富的经验。我必须警告你,这可能变成一次充满挫败感的经验。在你重新开始之前,你可能会尝试多种方法,但最终都失败了。

Before we start talking about how tilt should work, let’s try running the game.

在我们开始了解 tilt 怎么工作之前,让我们先跑一下这个游戏。

Open the Main class and click the run button. You should see the game pop up. Try pressing the arrow keys. You should see that nothing is happening. This is because you have not implemented the tilt method yet. When you’re done writing tilt, you’ll be able to play the game.

点开 Main 类并点击运行按钮。你会看到游戏弹出。这时候你按下方向键,应该没有任何反应。这是因为你还没实现 tilt 方法。当你实现了 tilt 方法之后,你就能开始玩游戏了。

public boolean tilt(Side side)

The tilt method does the work of actually moving all the tiles around. For example, if we have the board given by:

tilt 方法事实上是用来移动所有图块的。例如,如果我们有下面的板子,

|   2|    |   2|    |
|   4|   4|   2|   2|
|    |   4|    |    |
|   2|   4|   4|   8|

And press up, tilt will modify the board instance variable so that the state of the game is now:

按上,tilt 会修改板子的实例变量好让游戏的状态变为:

|   2|   8|   4|   2|
|   4|   4|   4|   8|
|   2|    |    |    |
|    |    |    |    |

In addition to modifying the board, two other things must happen:

除了改板子,还有两件事是必须发生的

  1. The score instance variable must be updated to reflect the total value of all tile merges (if any). For the example above, we merged two 4s into an 8, and two 2s into a 4, so the score should be incremented by 8 + 4 = 12.

**分数变量必须被更新以用来反应所有合并图块的总值。比如上面的例子,我们把两个4 合并成了8,把两个2 合并成了4 , 所以分数应该加 8+4 = 12 **

  1. If anything about the board changes, we must set the changed local variable to true. That’s because at the end of the skeleton code for tilt, you can see we call a setChanged() method: this informs the GUI that there is something to draw. You will not make any calls to setChanged yourself: only modifying the changed local variable.

如果板子上有任何变化,我们必须把 本地的 change 变量改成 true。这是因为,在为了 tilt 的骨架代码的最后,你能看到调用了 一个叫做 setChanged 的方法:这会通知 GUI , 这里有些东西需要被渲染。你不需要自己调用 setChange 方法:你只需要改变 changed 值就行。

All movements of tiles on the board must be completed using the move method provided by the Board class. All tiles of the board must be accessed using the tile method provided by the Board class. Due to some details in the GUI implementation, you should only call move on a given tile once per call to tilt. We’ll discuss this constraint further in the Tips section of this document.

所有移动图块的方法都必须完全使用 Board 类中提供的 move 方法。所有板子上的图块 都必须使用 Board 类中提供的 tile 方法访问。由于 GUI 的一些实现细节,你应该在每次调用 move 方法时,只调用一次 tilt。我们在 Tips 章节中讨论此限制。

A quick overview of how to get started writing this method is provided in this video.

Tips

We strongly recommend starting by thinking only about the up direction, i.e. when the provided side parameter is equal to Side.NORTH. To support you in this, we provide a TestUpOnly class that has four tests: testUpNoMerge, testUpBasicMerge, testUpTripleMerge, and testUpTrickyMerge. You’ll note that these tests involve only a single move up.

我们强烈推荐你通过思考 上 来开始。为了帮助你思考,我们提供了 TestUpOnly 类,这里面包含了四个测试:testupNoMerge, testUpBasicMerge, testUpTripleMerge, TestUpTrickyMerge. 你会注意到这些测试都只需要上。

When considering how to implement the up direction, consider the following:

当考虑怎么实现 上 的时候,你可以这么想:

In a given column, the piece on the top row (row 3) stays put. The piece on row 2 can move up if the space above it is empty, or it can move up one if the space above it has the same value as itself. In other words, when iterating over rows, it is safe to iterate starting from row 3 down, since there’s no way a tile will have to move again after moving once.

在给定的列里面, 最上面的行不变,如果第二行上方有空间,则可以往上移动,或者如果上面有一个等值的图块,则可以移动一格。另外一方面,从上往下迭代也是安全的,不会出现一个图块移动两次的问题。

While this sounds like it’s not going to be very hard, it really is. Be ready to bust out a notepad and work out a bunch of examples. Strive for elegant code, though elegance is hard to achieve with this problem. We strongly recommend the creation of one or more helper methods to keep your code clean. For example, you might have a helper function that processes a single column of the board, since each column is handled independently. Or you might have a helper function that can return a desired row value.

虽然这听起来不难,但确实如此。准备好拿出笔记本,再写一堆例子吧。努力写出优雅的代码吧,尽管很难优雅的解决这个问题。我们强烈推荐你使用一个或者多个辅助方法来帮助你的代码保持整洁。例如,你可以写一个处理单列的辅助方法,因为每一列都是独立处理的。或者你可以写一个辅助方法计算应该返回的行值。

Reminder: You should only call move on a given tile once. In other words, suppose you have the board below and press up.

提醒:你应该在一个图块上只调用一次 move 方法。

|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
|    |    |    |   2|

One way we could accomplish this would be as follows:

一种实现的方法是

Tile t = board.tile(3, 0)
board.move(3, 1, t);
board.move(3, 2, t);
board.move(3, 3, t);
setChanged();
return true;

However, the GUI will get confused because the same tile is not supposed to move multiple times with only one call to setChanged. Instead, you’ll need to complete the entire move with one call to move, e.g.

然而,GUI会非常困惑,因为同一个图块不支持移动多次,并只调用一次 setChange 方法。相反,你应该在一次调用 move 中完成全部的移动。

Tile t = board.tile(3, 0)
board.move(3, 3, t);

In a sense, the hard part is figuring out which row each tile should end up on.

一份比较难的点在于怎么判断每个图块应该停在那个行上。

To test your understanding, you should complete this Google Form quiz. This quiz (and the following quizzes) are completely optional (i.e. no graded) but highly suggested as it’ll find any conceptual misunderstandings you might have about the game mechanics. You may attempt this quiz as many times as you’d like.

为了测试您的理解,你应该完成这个。这个问卷不计入成绩,但是非常建议因为它会发现你对于游戏的概念性误解。你可以根据需要多次完成这个问卷。

To know when you should update the score, note that the board.move(c, r, t) method returns true if moving the tile t to column c and row r would replace an existing tile (i.e. you have a merge operation).

为了让你知道什么时候应该更新分数,请注意,如果将图块t 移动到 c 列 r 行,将替换现有图块(合并操作),则 board.move(c,r,t)将返回 true。

To make matters seemingly much worse, even after you get tilt working for the up direction, you’ll have to do the same thing for the other three directions. If you do so naively, you’ll get a lot of repeated, slightly modified code, with ample opportunity to introduce obscure errors.

更糟的是,当你完成了 上 的tilt 工作之后,你需要对另外的三个方向也做相同的事情。如果你这么天真的去做了,那么你会得到一大堆略有不同的重复代码,并且非常多的机会引入晦涩的错误。

For this problem, we’ve given away a clean solution. This will allow you to handle the other three directions with only two additional lines of code! Specifically, the Board class has a setViewingPerspective(Side s) function that will change the behavior of the tile and move classes so that they behave as if the given side was NORTH.

为了解决这个问题,我们提供了一个干净的解决办法。这会帮助你在只加两行代码的前提下处理另外三个方向。特别的,Board类里面又一个 setViewingPerspective 的方法,可以让 tile 和 move 方法像 向上 一样 工作。

For example, consider the board below:

例如,考虑下面这个例子。

|    |    |    |    |
|  16|    |  16|    |
|    |    |    |    |
|    |    |    |   2|

If we call board.tile(0, 2), we’ll get 16, since 16 is in column 0, row 2. If we call board.setViewingPerspective(s) where s is WEST, then the board will behave as if WEST was NORTH, i.e. you had your head turned 90 degrees to the left, as shown below:

如果我们调用 0 列 2 行的图块,我们会得到 16。如果我们这时候调用 setViewperspective 方法, 并把方向设置为左,之后板子会让左表现的向上一样。(你可以把脑袋向左转 90 度)

|    |    |  16|    |
|    |    |    |    |
|    |    |  16|    |
|   2|    |    |    |

In other words, the 16 we had before would be at board.tile(2, 3). If we were to call board.tilt(Side.NORTH) with a properly implemented tilt, the board would become:

换句话说,这个图块的位置就变成了 2 列 3 行。如果我们再调用 tilt ,这个时候会变成

|   2|    |  32|    |
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |

To get the board to go back to the original viewing perspective, we simply call board.setViewingPerspective(Side.NORTH), which will make the board behave as if NORTH was NORTH. If we do this, the board will now behave as if it were:

再把板子转回原来的角度。

|    |    |    |    |
|  32|    |    |    |
|    |    |    |    |
|   2|    |    |    |

**Observe that this is the same thing as if you’d slid the tiles of the original board to the WEST.

你会观察到,这其实跟往左是一样的

Important: Make sure to use board.setViewingPerpsective to set the perspective back to Side.NORTH before you finish your call to tilt, otherwise weird stuff will happen.

重要:确保在你调用完 tilt 方法之后,转回 上 方向,否则会发生奇怪的事情。

To test your understanding, try this third and final Google Form quiz. You may attempt this quiz as many times as you’d like.

Testing

While in the future we expect you to be able to test your own programs, for this project we’ve given you the full test suite.

在之后,我们希望你能使用自己的程序进行测试, 这个项目,我们提供了全套的测试

The tests are split over 5 files: TestEmptySpace, TestMaxTileExists, TestAtLeastOneMoveExists, TestUpOnly, and TestModel. Each file tests a specific portion of the code with the exception of TestModel which tests all the things you write in coordination with each other. Such a test is called an integration test and are incredibly important in testing. While unit tests run things in isolation, integration tests run things all together and are designed to catch obscure bugs that occur as a result of the interaction between different functions you’ve written.

这个测试有 5 个文件。每个文件的测试都会有测试代码的特定部分,但是 TestModel 除外,它会测试您相互协调编写的所有内容。这样的测试被叫做集成测试,在测试过程中非常重要。

So do not attempt to debug TestModel until you’re passing the rest of the tests! In fact, the order in which we discuss the tests is the order you should attempt them in.

所以直到你解决了剩余的所有问题之前,先别弄 TestModel。事实上,你应该按照我们讨论的测试的顺序来进行排查。

We’ll now take a look at each of these tests and show you how to read the error messages.

我们现在来看看每个测试并告诉你怎么阅读错误信息。

TestEmptySpace

These tests will check the correctness of your emptySpaceExists method. Here is what the error message would look like if you failed one of the tests:

这些测试会测试 emptySpaceExists 方法的正确。这里是如果你没过测试可能会出现的错误信息。

TestEmptySpace all fail

On the left-hand side, you’ll see the list of all tests that were run. The yellow X means we failed a test while the green check means we passed it. On the right, you’ll see some useful error messages. To look at a single test and its error message in isolation, click the test on the left-hand side. For example, let’s say we want to look at the testCompletelyEmpty test.

在左边,你会看到所有运行的测试列表。黄叉意味着没过,绿勾意味着过了。在右边,你会看到一些有用的错误信息。点击左边的错误,你就能看到每一个单个测试的单独的错误信息。例如,你想看 testCompletelyEmpty 的错误信息。

testCompletelyEmpty

The right-hand side is now the isolated error message for this test. The top line has a useful message: "Board is full of empty space" followed by a String representation of the board. You’ll see that it’s clearly empty, yet our emptySpaceExists method is returning false and causing this test to fail. The javadoc comment at the top of the code for the test also has some useful information in case you’re failing a test.

右边现在是单独的错误信息。最上面一行是有用的信息,说板子全是空的,并且搭配了字符串样子的板子。你会看到上面确实全是空的,然后我们写的 emptySpaceExists 方法返回了 false 因为测试失败了。测试代码最上方的 javadoc 为了防止您失败,写了一些有用的信息。

TestMaxTileExists

These tests will check the correctness of your maxTileExists method. The error messages will be similar to those for TestEmptySpace, and you can still click on each individual test to look at them in isolation. Remember that your maxTileExists method should only look for the max tile and not anything else (i.e. shouldn’t look for empty space). If yours does, you will not pass all of these tests.

这些测试将会检查你的 maxTileExists 方法的正确性。错误信息和上一个测试基本一致,然后你依然能点击每个独立的测试来看看单个测试的情况。记住你的 maxTileExists 方法应该只找到最大的图块,并不检查其他的。如果你这么做了,你就没发通过全部的测试了。

TestAtLeastOneMoveExists

These tests will check the correctness of your atLeastOneMoveExists method. The error messages are similar to the above two. Since the atLeastOneMoveExists method depends on the emptySpaceExists method, you shouldn’t expect to pass these tests until you are passing all of the tests in TestEmptySpace.

这些测试是用来测试 你的 atLeastOneMoveExists 方法的正确性。错误信息跟上面两个相似。 atLeastOneMoveExists 方法依赖于 empthSpaceExists 方法。你应该把上面的测试都过了在看这个测试。

TestUpOnly

These tests will check the correctness of your tilt method, but only in the up (Side.NORTH) direction. The error messages for these are different, so let’s look at one. Say we run all the tests, notice we’re failing the testUpTrickyMerge test. After clicking that test, we’ll see this:

这个测试是用来检测 tilt 方法的正确性的,但只检查 上 方向。错误信息也有些不同,让我们看看其中一个。我们运行全部测试,注意到我们失败了一个叫做 testUpTrickyMerge 的测试。在点完测试之后,我们发现

testUpTrickyMerge Error Message

The first line tells us the direction that was tilted (for these tests it’ll always be North), then what your board looked like before the tilt, then what we expected the board to look like, and finally what your board actually looked like.

第一行告诉我们方向,然后是执行之前你的板子的样子,和最后你的板子的样子。

You’ll see that we’re merging a tile twice on a single call to tilt which results in a single tile with value 8 instead of two tiles both with value 4. As a result, our score is also incorrect as you can see in the bottom of the representation of the board.

你会看到我们在同一个调用中合并了一个图块两次,这让结果在一个图块中展示了 8 ,而不是两个 4 的图块。同时也能看到在板子最下边的分数也是错的。

For other tests it might be difficult to notice the difference between the expected and actual boards right away; for those, you can click the blue “Click to see difference” text at the very bottom of the error message to get a side-by-side comparison of the expected (on the left) and actual (on the right) boards in a separate window. Here is what it looks like for this test:

对于不同的测试,也许很难正确的注意到期望和实际的不同。为此,你能点击错误信息下面蓝色的 “Click to see difference” 文本来在分割的窗口中,看看比较期望和实际结果的不同。下面是它看起来的样子

testUpTrickyMerge Comparison

Debugging these can be a bit tricky because it’s hard to tell what you’re doing wrong. First, you should identify which of the 3 rules listed above you’re violating. In this case, we can see that it’s rule 2 since a tile is merging more than once. The javadoc comments on these methods are good resources for this as they specifically lay out what rule/configuration they’re testing. You might also be able to figure out what rule you’re violating by just looking at the before and after boards. Then, comes the tricky party: refactoring your existing code to properly account for that rule. We suggest writing out on pen and paper the steps your code takes so you can first understand why your board looks the way it does, then coming up with a fix. These tests only call tilt once, so you don’t need to worry about debugging multiple calls to tilt.

调试这些 有一点棘手,因为很难来告诉你你做的是错的。首先,你应该确定你违反了上面列出的 3 条规则当中的那一条。这个例子,我们可以看到是 规则2 因为一个图块被合并了超过一次。 这些方法的 Javadoc 是不错的资源,他们专门能列出了他们正在测试的规则。你也许能通过前后对比图就能判断你违反了那一条规则。然后,来到了棘手的部分。重构现有代码以考虑正确规则。我们建议你用笔写下你的代码的步骤,好让你能先了解你的板子为什么会成为现在这样。然后再来修改。这些测试只会调用 tilt 一次。所以你不用担心调试程序多次调用了 tilt

TestModel

These tests will check the correctness of everything together. The majority of these tests are similar to the tests in TestUpOnly as they only call tilt once, but we also have tests for gameOver (which test all of your emptySpaceExists, maxTileExists, and atLeastOneMoveExists methods together) and tests that make many calls to tilt in a sequence.

这些测试会检查所有东西是否正确。这些测试跟 测试上 的测试很像,只不过,测试上的测试只调用了 tilt 方法一次。但我们还有对于游戏结束的测试,还有这些测试会在一个序列里调用多次 tilt 方法。

The error messages for these look exactly the same as those in TestUpOnly, and the javadoc comments are similarly useful in figuring out what the test is testing.

这些错误信息跟上一个测试看起来差不多。Javadoc 评论对于分辨那个测试正在运行非常有帮助。

Don’t worry about the actual code for the tests: you’re not required to understand or modify any of these, though you’re welcome to read through and get an idea for how test writing works and even add some of your own if you are feeling really ambitious.

别担心测试的实际代码:你不需要理解或者修改这些代码,但欢迎您通过阅读并了解测试编写的工作原理,并且如果您有兴趣,也可以添加一些你自己的东西进来。

Grading

A full scoring project will pass all of the unit tests that we provide. Remember that there are no hidden tests, so if you’re passing all of these tests then you have a full scoring project!

完整的打分程序会在通过全部测试后提供。记住这里没有隐藏的测试。所以如果你通过了全部的测试,你在这个项目里就能拿到满分。

Some tests are weighted slightly differently than others on Gradescope, so if you’re not passing some fraction of the tests, your score as a percent will be some different fraction (likely higher).

一些测试的权重会和其他测试略有不同。所以,你没有通过其中的一些测试,你的分数的百分比可能不同。

That’s because some parts of this project are harder than others, and we know this is many students’ first time using Java, so we’ve weighted each portion of the project with that in mind.

**这是因为其中一些测试比其他的测试要更难一些。并且我们知道一些同学是第一次用 Java,所以,我们会考虑到这一点来分配权重。 **

Here is a breakdown of what percent you’d earn on this project with varying levels of completing:

  1. Only implementing emptySpaceExists or maxTileExists: ~27%
  2. Implementing everything except tilt: ~47%
  3. Implementing everything, except tilt only works in the Up direction: ~68%
  4. Implementing everything, except merging: ~64%
  5. Implementing everything, except rule 2 of merging: ~93%

You’ll note that getting the rule 2 of merging is only ~7% of the project. That is because it’s a difficult rule to handle that accounts for a very small portion of the game itself, so we’ve weighted it accordingly.

你会注意到 规则2(合并)只占到了 7% 的权重。这是因为这个有点难去处理,并且只占了游戏的一小部分。所以我们对其进行了加权。