CS61B:proj0 2048记录

前言

这是第一个proj,我记得在我第一次学习这门课时感觉无从下手,随着不断的学习,大概是一年之后再看这个proj似乎也没有当时的那种恐惧,大概花了四个小时全部做完,下面我仅分享一下我的思路,本文不涉及重要的代码。

2048的玩法

在第一次玩的时候,我就像AI一样在那随便乱窜,完全不知道怎么玩。后来才知道原来相同的数字可以进行叠加,2 + 2 = 4, 4 + 4 = 8….然后是1024+1024 = 2048,也就是说,只要你格子的最大数字是2048,那么你就获胜了!

同理,游戏有赢也有输,不能总是赢赢赢

image-20250727211831699

如果说当前方格内没有空格子时,你基本就要完蛋了,这个时候还需要检查是否可以有上下左右可以合成的相邻格子,如果没有,那么真就Game Over了!

思考:我们已经得到了结束游戏的条件,即当前格子最大值为2048或者当前无法合成新的数字。

我们先跟着文档指导来完成一下一些前置函数。

首先是检查当前是否 有空格子的函数

emptySpaceExists(Board b)

这个函数不难,不过对于刚上手的人来说,我觉得可能不知道怎么获取一个格子才是比较难的地方。不过跟着官方文档来,也还是很简单的。

For this method, you’ll want to use the tile(int col, int row) and size() methods of the Board class.

官方文档提示我们可以使用tile这个函数,这个函数长这样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** Return the current Tile at (COL, ROW), when sitting with the board
     *  oriented so that SIDE is at the top (farthest) from you. */
    private Tile vtile(int col, int row, Side side) {
        return values[side.col(col, row, size())][side.row(col, row, size())];
    }

    /** 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);
    }

其实为了更加工程化,官方对这个2048进行了包装,要让我来实现,直接返回一个数组的arr[col][row]就行了,似乎这就是我与大佬之间的差距。

所以,当我们调用这个方法时,本质就是去获取方格子上的一个格子。于是乎,这样我们就可以进行这个函数的编写了。显然,如果这个函数的返回值是null,说明当前是一个emptySpace,否则就是存在一个格子。

maxTileExists(Board b)

这个函数返回的是当前方格子的最大值,找最大值这个不难,相信大家都会。我们获取一个格子后,就可以调用这个格子的value方法来获取其内存储的数字,这样就可以进行比较。

atLeastOneMoveExists(Board b)

这个函数的意思要我们判断当前格子至少能进行一次移动,判断的标准就是

  • 当前格子有空格子
  • 格子满了,但是相邻的两个格子数字相同,也就是可以合并,这样也算一次移动

这样也可以进行判断,不过,进行相邻格子进行搜索的时候有个技巧,如果你是第一次实现这个功能,很容易把代码写成下面这个样子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
for (int i = 0; i < size; i++) {
    for (int j = 0; j < size; j++) {
        // 上
        if (isVaild(i-1, j)) {
            doSth();
        }
        // 下
        if (isVaild(i+1, j)) {
            doSth();
		}
        
        //左
        
        
        
        // 右
        
    }
}

这样写相当不好维护,其实你可能没有注意到我们可以用循环来替代这个上下左右搜索

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int[] dx = {-1, 1, 0, 0};
int[] dy = {0, 0, -1, 1};

for (int i = 0; i < size; i++) {
    for (int j = 0; j < size; j++) {
        for (int k = 0; k < 4; k++) {
         	int x = i + dx[k];
            int y = j + dy[k];
            
            // 这样我们就得到一组坐标
        }
    }
}

更节省空间的,我们可以只使用一个数组,你可以自己试一下。

tilt(Side side)

在正确编写完上面的代码并测试完后,你可以运行Main试一下,如果一切正常,格子里面就会出现一些数字。

image-20250727221432664

这里,我们先不考虑怎么合并,先考虑怎么把格子进行移动。在Board类中,有一个方法是move函数,它的定义如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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;
        }
    }

也就是说,指定一个坐标和格子,调用move方法就可以移动这个格子,而且,这个move方法还可以合并两个格子,即如果移动到的那个坐标没有数字,那么就移动到上面;否则,就合并两个格子。我们先来看看怎么移动一个格子吧,在此之前,我们先要熟悉这个网格的坐标。

image-20250727222025330

左下角是原始坐标,这样,我们就可以直接去移动一个点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
boolean changed = false;

for (int i = 0; i < board.size(); i++) {
    for (int j = board.size() - 2; j >= 0; j--) {
        Tile t = board.tile(i, j);
        if (t != null) {
            board.move(3, 3, t);
            changed = true;
        }
    }
}
return changed;

这样,当我们键入上下左右时,就应该可以把这个点移动到右上角。

你键入“上下左右”时,可能会发现什么都不起作用,这可能是因为你的操作系统或者IDEA的语言不是英文导致的

我们可以违反适应规则,修改一些其它的代码。

打开GUISource.java,把case部分的代码进行修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
switch (command) {
            case "↑" :
            case "向上箭头" :
                command = "Up";
                break;
            case "→" :
            case "向右箭头" :
                command = "Right";
                break;
            case "↓" :
            case "向下箭头" :
                command = "Down";
                break;
            case "←" :
            case "向左箭头" :
                command = "Left";
                break;
            default :
                break;
        }

这样,你应该可以移动了,不用担心,自动评分只会使用Model这个程序。

官方文档提示我们可以先完成一个向上合并的操作,然后通过测试。其实通过测试你就可以明白2048的每个格子之间是怎么进行合并的。我们需要注意一些特例,即每个格子在这一列上最多会被合并一次。

来举个例子,请考虑下面这个格子

1
2
3
4
{0, 0, 2, 0},
{0, 0, 2, 0},
{0, 0, 0, 0},
{0, 0, 4, 0},

如果向上合并的话,答案应该是多少呢?是

1
2
3
4
{0, 0, 4, 0},
{0, 0, 4, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},

还是

1
2
3
4
{0, 0, 8, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},

应该是前者,对吧?你需要记住我上文说的:每个格子至多会有一次合并,这样来看,上面两个2已经被合成4了,所以那个格子不应该再和下面那个4进行合并了。

补全其它方向

实际上,如果我们要给每个方向都写一份代码,那么这是不太好维护的。官方文档想了一个十分巧妙的方法,就是坐标变换,只要我们有向上合并的代码,那么经过行列转置,我们就可以拥有其它方向。

例如,这是一个随机的格子

1
2
3
4
0 0 0 0
8 2 0 2
0 0 0 0
0 0 0 0

显然,如果我们按下<-,这样就可以合并第二行的格子。经过旋转后,格子可以变为

1
2
3
4
0 0 8 0
0 0 2 0
0 0 0 0
0 0 2 0

这样就转换为向上合并的情况,合并完后,我们就再反转回来即可。

官方给我提供了十分方便的坐标轴旋转函数,对应的就是”上北下南,左西右东“。我们可以通过

1
board.setViewingPerspective(side);

来设置坐标系,记得最后还原!

通过测试

写完所有代码后,就可以运行TestModel测试,如果你全部通过了,可以提交到自动评分机上!

我的代码通过了所有的测试

image-20250727225951681

一次失败的挑战

image-20250727224851448

你能通关吗?欢迎贴图证明哦!

Licensed under CC BY-NC-SA 4.0
花有重开日,人无再少年
使用 Hugo 构建
主题 StackJimmy 设计