代码解读部分
本项目是实现一个基本的2048游戏,本项目提供了大量的初始代码以供参考,设计者本身使用的是MVC模式构建基本代码,可以让我们专注于逻辑设计,忽略大量的高级Java知识和技术
Tile类
tile 类主要在项目中使用到的是tile类下的value方法,提供标准坐标系下(即以北边为最远的边的坐标系)(这种说法还会在下面的side类中提到)的某个点的值
Side 类
side 类主要在项目中起到重定向坐标系的作用,这里主要有四个属性,分别是col0, row0, dcol, drow,其中col0和row0分别用于重定向坐标原点,比如当在以北边为最远的坐标系下,最左下角的点的坐标就是(0, 0), 当以最东边为最远时,最左下角的坐标就是(size, 0), 上述的坐标都是相对于标准坐标系而言的(即以北边为最远的坐标系);而dcol, drow 的作用就是在每个重定向的坐标系下,如果还想像标准坐标系那样向北边移动一个单位,需要对行和列分别加上(dcol, drow)
public enum Side {
NORTH(0, 0, 0, 1), EAST(0, 1, 1, 0), SOUTH(1, 1, 0, -1),
WEST(1, 0, -1, 0);
}
接着对重定向的坐标采用坐标变换就可以得到标准坐标系下的坐标
/** Return the standard column number for square (C, R) on a board
* of size SIZE oriented with this Side on top. */
public int col(int c, int r, int size) {
return col0 * (size - 1) + c * drow + r * dcol;
}
/** Return the standard row number for square (C, R) on a board
* of size SIZE oriented with this Side on top. */
public int row(int c, int r, int size) {
return row0 * (size - 1) - c * dcol + r * drow;
}
至于这样的坐标变换到底怎么得来,可能需要极好的数学基础,不过我们可以感性理解下,如果当前重定向的坐标系是以最东边为最远的,并且板子长度为5,重定向的坐标为(2, 1),根据上述公式可以算出标准坐标系下的点是(1, 2),画图验证发现成立。
Board 类
board类主要是用来对tile进行管理的,可以让我们从更高的层次改变board的状态,并且可以用于封装,我们所需要用到的方法如下:
move方法可以将tile移动到重定向后的坐标(col, row),注意这里的(col, row)是重定向的坐标
/** Places the Tile TILE at column COL, row ROW where COL and ROW are
* treated as coordinates with respect to the current viewPerspective.
*
* Returns whether or not this move is a merge.
* */
public boolean move(int col, int row, Tile tile) {
int pcol = viewPerspective.col(col, row, size()),
prow = viewPerspective.row(col, row, size());
if (tile.col() == pcol && tile.row() == prow) {
return false;
}
Tile tile1 = vtile(col, row, viewPerspective);
values[tile.col()][tile.row()] = null;
if (tile1 == null) {
values[pcol][prow] = tile.move(pcol, prow);
return false;
} else {
values[pcol][prow] = tile.merge(pcol, prow, tile1);
return true;
}
}
tile方法, 可以返回在某个固定的视角(也就是重定向坐标系下)的(col, row)的地方的tile对象
/** Return the current Tile at (COL, ROW), where 0 <= ROW < size(),
* 0 <= COL < size(). Returns null if there is no tile there. */
public Tile tile(int col, int row) {
return vtile(col, row, viewPerspective);
}
toString方法,可以返回board的状态,便于我们向GUI发送是否改变board状态,以及是否要生成新的随机tile.
@Override
/** Returns the board as a string, used for debugging. */
public String toString() {
Formatter out = new Formatter();
out.format("%n[%n");
for (int row = size() - 1; row >= 0; row -= 1) {
for (int col = 0; col < size(); col += 1) {
if (tile(col, row) == null) {
out.format("| ");
} else {
out.format("|%4d", tile(col, row).value());
}
}
out.format("|%n");
}
return out.toString();
}
任务部分
我们需要完成的任务都是在Model类中,
任务一:
public static boolean emptySpaceExists(Board b)
这个task是要求我们判断是否有空白的tile在一块板子上, 我们只需要遍历板子即可
/** Returns true if at least one space on the Board is empty.
* Empty spaces are stored as null.
* */
public static boolean emptySpaceExists(Board b) {
int size = b.size();
for (int i = 0; i < size; i++){
for (int j = 0; j < size; j++){
if (b.tile(i, j) == null){
return true;
}
}
}
return false;
}
任务二:
public static boolean maxTileExists(Board b)
这个是让我们判断是否存在游戏结束的另一个条件,即存在2048块,在这里我们不应该直接使用2048这个魔法数字,而是应该使用常量MAX_PIECE
/**
* Returns true if any tile is equal to the maximum valid value.
* Maximum valid value is given by MAX_PIECE. Note that
* given a Tile object t, we get its value with t.value().
*/
public static boolean maxTileExists(Board b) {
int size = b.size();
for (int i = 0; i < size; i++){
for (int j = 0; j < size; j++){
if (b.tile(i, j) != null && b.tile(i, j).value() == MAX_PIECE){
return true;
}
}
}
return false;
}
任务三:
public static boolean atLeastOneMoveExists(Board b)
是否存在至少一个可以移动的,至少一个可以移动就是指有空值,或者上下左右四个方位可以移动,有空值的功能我们在任务一中已经实现,我们只需判断是否有邻居可以合并
public static boolean atLeastOneMoveExists(Board b) {
boolean flag = false;
// 是否存在空白块
if (emptySpaceExists(b)) return true;
// 是否存在相邻的相同块
for (int i = 0; i < b.size(); i++){
for (int j = 0; j < b.size(); j++){
if (i < b.size() - 1 && b.tile(i, j).value() == b.tile(i + 1, j).value()){
flag = true;
}
if (j < b.size() - 1 && b.tile(i, j).value() == b.tile(i, j + 1).value()){
flag = true;
}
}
}
return flag;
}
任务四:
public boolean tile(Side side)
我们需要对游戏的一次操作进行移动,也就是当用户按下上下左右四个键的时候,我们需要对游戏板进行操作,这里我们需要一次确定每个板上的tile的最终移动位置,这是它所要求的,并且根据规则,我们不可以将合并过的板子再次合并,因此,我们应该以从上往下行的顺序进行合并,而不能从下往上,从上往下的好处是我们可以快速确定每个板子的最终位置,对于列的遍历顺序我们没有要求,从左往右,从右往左都可以,但是我们可以设计一个helper函数帮助我们处理每个列中的合并移动逻辑,这样可以使我们专注于更小的设计逻辑。
public boolean tilt(Side side) {
boolean changed;
changed = false;
// TODO: Modify this.board (and perhaps this.score) to account
// for the tilt to the Side SIDE. If the board changed, set the
// changed local variable to true.
board.setViewingPerspective(side);
int c = board.size();
String oldBoard = this.toString();
for (int i = 0; i <= c - 1; i++) {
columnHelper(i);
}
String newBoard = this.toString();
if (!oldBoard.equals(newBoard)) {
changed = true;
}
checkGameOver();
if (changed) {
setChanged();
}
board.setViewingPerspective(Side.NORTH);
return changed;
}
public void columnHelper(int column) {
boolean[] st = new boolean[board.size()];
for (int i = board.size() - 2; i >= 0; i--) {
// 向上合并
if (board.tile(column, i) != null) {
// 向上合并
moveHelper(column, i, st);
}
}
}
public void moveHelper(int column, int row, boolean[] st) {
int i = row + 1;
while (i < board.size() - 1) {
if (board.tile(column, i) == null) {
i++;
} else break;
}
if (board.tile(column, i) == null) {
board.move(column, i, board.tile(column, row));
} else if (!st[i] && board.tile(column, i).value() == board.tile(column, row).value()) {
st[i] = true;
board.move(column, i, board.tile(column, row));
score += board.tile(column, i).value();
} else {
board.move(column, i - 1, board.tile(column, row));
}
}
测试部分
项目采用junit测试
我们一般采用junit的测试形式是
import org.junit.Test;
import static org.junit.Assert.*;
并且我们需要吧每个测试方法都设计为实例方法,并且在明明函数的时候采取驼峰命名法
下面是一个测试的简单形式
@Test
/** Checks right two pieces merge when 3 adjacent pieces have same value. */
public void testTripleMerge1() {
int[][] before = new int[][]{
{2, 0, 0, 0},
{2, 0, 0, 0},
{2, 0, 0, 0},
{0, 0, 0, 0},
};
int[][] after = new int[][]{
{4, 0, 0, 0},
{2, 0, 0, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
};
updateModel(before, 0, 0, false);
String prevBoard = model.toString();
boolean changed = model.tilt(Side.NORTH);
checkChanged(Side.NORTH, true, changed);
checkModel(after, 4, 0, prevBoard, Side.NORTH);
}