写在前面的废话
作者本人只是一个小白,在自学cs61b 完成 proj0 的时候因为不熟悉Java语法遇到了非常大的挫折(只会C,类用的都不太明白),同时目前网上fa-23的课程相关的HW,lab,proj等的答案比较稀少,所以想写这一篇博客帮助和我一样的小白。
文中的代码没有考虑什么算法,都是暴力遍历(还没学数据结构和算法),希望路过的大佬手下留情
这里附上课程网站
课程网站(fa-23)
前言
课程网站上对这个项目有比较详细的解释,但是大部分是英文,会造成一定障碍,但是还是比较推荐去直接阅读英文,只在必要的时候使用翻译软件,对英语学习有比较不错的帮助。
1. public static boolean emptySpaceExists(Board b)
源代码如下
/** Returns true if at least one space on the Board is empty.
* Empty spaces are stored as null.
* */
//翻译
/** 如果板上仍有空白格,则返回真。
* 空白格被存储为null。
* */
public static boolean emptySpaceExists(Board b) {
int i , j;
for(i = 0 ; i < b.size() ; i++ ){
for(j = 0; j < b.size(); j++ ){
if(b.tile(i,j) == null ){
return true ;
}
}
}
return false;
}
解题思路就是双循环遍历表格中的每一个格子,如果有一个格子为 null
则返回 true
.
这里的 null
表示的是空地址
根据 Board
文件中的下列代码
/** 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);
}
我们知道可以通过调用 public Tile tile(int col, int row);
这一函数,获得 _values[side.col(col, row, size())][side.row(col, row, size())]
的值(中间经过对函数 private Tile vtile(int col, int row, Side side)
的调用)
因为 vtile(int , int , Side )
函数是 private
类型,所以只能通过调用 tile(int , int )
函数的方法进行调用。
这里的 _viewPerspective
非常重要,它用于计算表格中每一个方块的“相对位置”具体内容我会在任务4中解释
这里要强调的是,看下面的代码:
/** Create a board where RAWVALUES hold the values of the tiles on the board
* (0 is null) with a current score of SCORE and the viewing perspective set to north. */
public Board(int[][] rawValues) {
int size = rawValues.length;
_values = new Tile[size][size];
_viewPerspective = Side.NORTH;
for (int col = 0; col < size; col += 1) {
for (int row = 0; row < size; row += 1) {
int value = rawValues[size - 1 - row][col];
Tile tile; //注意这部分
if (value == 0) {
tile = null; //重点关注
} else {
tile = Tile.create(value, col, row);
}
_values[col][row] = tile; //到这里
}
}
}
这部分代码创建了一个 Tile
类型的变量,在2.1的课上会讲到,这些变量只存储地址,而不是整个变量内容,因此,看重点关注句,值为零的格子 tile = null;
这里仅仅令地址为空,并没有创建新的 Tile(int value, int col, int row)
的 Tile
类型的变量,并将 tile._value
的值赋为零。
所以原文中的if语句中,判断条件是 b.tile(i,j) == null
而不是 b.tile(i,j).value() == 0
因为 b.tile(i,j)
不一定是一个 Tile
类型的变量,不一定可以调用函数 public int value()
2. public static boolean maxTileExists(Board b)
依旧先上源代码
/**
* Returns true if any tile is equal to the maximum valid value.
* Maximum valid value is given by this.MAX_PIECE. Note that
* given a Tile object t, we get its value with t.value().
*/
//翻译
/**
* 如果有任何一个格子中的值等于最大有效值(2048),则返回真。
* 最大有效值通过 this.MAX_PIECE 获得。
* 对于一个Tile类型的对象t ,使用t.value() 来获得t的值。
*/
public static boolean maxTileExists(Board b) {
int i , j;
for(i = 0 ; i < b.size() ; i++ ){
for(j = 0; j < b.size(); j++ ){
int a = 0 ;
if(b.tile(i,j) != null ){
a = b.tile(i,j).value();
}
if(a == Model.MAX_PIECE){
return true ;
}
}
}
return false;
}
这部分逻辑上和 public static boolean emptySpaceExists(Board b)
类似,只不过比较的是某个块中的具体值。这里使用 if
语句判断块是否为 null
int a = 0 ;
if(b.tile(i,j) != null ){ //如果 (i,j) 处的块不是 null,则调用 value() 函数获取(i,j)上的值.
a = b.tile(i,j).value(); //注意 b 为 Board 类型,b.tile(int ,int ) 才是 Tile 类型.
}
3. public static boolean atLeastOneMoveExists(Board b)
代码如下
/**
* Returns true if there are any valid moves on the board.
* There are two ways that there can be valid moves:
* 1. There is at least one empty space on the board.
* 2. There are two adjacent tiles with the same value.
*/
//翻译
/**
* 在当前界面上仍可以进行合规的移动的时候返回真
* 通过下面两点判断是否存在合法的移动
* 1. 界面上至少有一个空格
* 2. 有两个相邻的格子的值是相同的
*/
public static boolean atLeastOneMoveExists(Board b) {
if(emptySpaceExists(b)){ //调用 任务1 编写的函数判断界面上有无空格
return true;
}else{
int i , j;
// 双循环检查每一个格子是否和右侧格子的值相等
//仅遍历前三行
for(i = 0 ; i < b.size() ; i++ ){
for(j = 0; j < b.size()-1 ; j++ ){
if(b.tile(i,j).value() == b.tile(i,j+1).value()){
return true ;
}
}
}
//双循环检查每一个格子是否和下面的格子的值相等
//仅遍历左三列
for(i = 0 ; i < b.size()-1 ; i++ ){
for(j = 0; j < b.size() ; j++ ){
if(b.tile(i,j).value() == b.tile(i+1,j).value()){
return true ;
}
}
}
}
return false;
}
第一次循环遍历示意图
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | X | X | X | X |
1 | X | X | X | X |
2 | X | X | X | X |
3 | j+1 | j+1 | j+1 | j+1 |
第二次循环遍历示意图
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | X | X | X | j+1 |
1 | X | X | X | j+1 |
2 | X | X | X | j+1 |
3 | X | X | X | j+1 |
这里的行号和列号标注并不准确,后面有解释
注意一下这里
if(b.tile(i,j).value() == b.tile(i+1,j).value()){
return true ;
}
因为已经用 emptySpaceExists(b)
函数检测过是否有空白格,所以这里可以直接调用 value()
函数,不用担心 b.tile(i,j)
是否为 null
。
4. public void tilt(Side side)
Main Task: Building the Game Logic
在这个函数里我们要建立游戏的逻辑,先看代码,后面再详细解释。
/** Tilt the board toward SIDE.
*
* 1. If two Tile objects are adjacent in the direction of motion and have
* the same value, they are merged into one Tile of twice the original
* value and that new value is added to the score instance variable
* 2. A tile that is the result of a merge will not merge again on that
* tilt. So each move, every tile will only ever be part of at most one
* merge (perhaps zero).
* 3. When three adjacent tiles in the direction of motion have the same
* value, then the leading two tiles in the direction of motion merge,
* and the trailing tile does not.
* */
//翻译
/**欸嘿,太长了,哥们懒得写了
* 这里主要就是介绍合并的规则,相信大家多少都玩过或看人玩过2048,所以就不翻译了
* 合理!
* */
public void tilt(Side side) {
this.board.setViewingPerspective(side); //调整相对方向为上
int i , j , record ;
for(i = 0; i < this.size(); i++ ){
int bottom = this.size();
for(j = this.size() - 2; j >= 0 ; j-- ){
int object = 0;
if(board.tile(i,j) != null ){
object = board.tile(i,j).value();
} //获取被操作格的值
if(object != 0){ //如果被操作格为0,则不进行检测或移动操作
for(record = j+1; record < bottom; record++ ){ //遍历寻找目标格
if(board.tile(i,record) != null) { //寻找有数字的格或为位于最顶端的空白格
if (board.tile(i, record).value() != object) { //不相等则移动到前一个格子
board.move(i, record - 1, board.tile(i, j));
break;
} else{ //相等则合并,并计算分数
board.move(i, record, board.tile(i, j));
score += 2 * object;
bottom = record;
break;
}
}else if(record == bottom - 1){
board.move(i, record , board.tile(i, j));
break;
}
}
}
}
}
this.board.setViewingPerspective(Side.NORTH); //将方向调整为原来方向
checkGameOver();
}
这部分有两个重点一是 二维数组的相对方向和基准点 二是 程序逻辑和游戏规则准确匹配
名字我瞎起的,不要在意 😐
二维数组的相对方向和基准点
We strongly recommend starting by thinking only about the up direction, i.e. when the provided side parameter is equal to Side.NORTH.
课程引导我们对方向做出调整,关于方向的函数和变量再 Side.java
文件中
public enum Side {
/** The parameters (COL0, ROW0, DCOL, and DROW) for each of the
* symbolic directions, D, below are to be interpreted as follows:
* The board's standard orientation has the top of the board
* as NORTH,
*
* and rows and columns (see Model) are numbered from its lower-left corner.
* //注意这句话,二维数组的(0,0)是其左下角的格子。
*
* Consider the board oriented
* so that side D of the board is farthest from you. Then
* * (COL0*s, ROW0*s) are the standard coordinates of the
* lower-left corner of the reoriented board (where s is the
* board size), and
* * If (c, r) are the standard coordinates of a certain
* square on the reoriented board, then (c+DCOL, r+DROW)
* are the standard coordinates of the squares immediately
* above it on the reoriented board.
* The idea behind going to this trouble is that by using the
* col() and row() methods below to translate from reoriented to
* standard coordinates, one can arrange to use exactly the same code
* to compute the result of tilting the board in any particular
* direction. */
NORTH(0, 0, 0, 1),
EAST(0, 1, 1, 0),
SOUTH(1, 1, 0, -1),
WEST(1, 0, -1, 0);
/** The side that is in the direction (DCOL, DROW) from any square
* of the board. Here, "direction (DCOL, DROW) means that to
* move one space in the direction of this Side increases the row
* by DROW and the colunn by DCOL. (COL0, ROW0) are the row and
* column of the lower-left square when sitting at the board facing
* towards this Side. */
Side(int col0, int row0, int dcol, int drow) {
this._row0 = row0;
this._col0 = col0;
this._drow = drow;
this._dcol = dcol;
}
/** 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;
}
/** Parameters describing this Side, as documented in the comment at the
* start of this class. */
private final int _row0, _col0, _drow, _dcol;
}
以 WEST
为例
下面的表格是标准标注( NORTH
为标准方向)
3 | X | X | X | X |
---|---|---|---|---|
2 | X | X | X | X |
1 | X | X | X | X |
0 | X | X | X | X |
NORTH | 0 | 1 | 2 | 3 |
为了让按下 左方向键 的操作等价于按下 上方向键 的操作,我们让格子“旋转”。
NORTH | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | X | X | X | X |
1 | X | X | X | X |
2 | X | X | X | X |
3 | X | X | X | X |
注意到此时每一列的标号没有改变,但是每一个格子的行号发生了改变
下面表上每个格子位置的改变
BEFORE:
i:3 | (3,0) | (3,1) | (3,2) | (3,3) |
---|---|---|---|---|
i:2 | (2,0) | (2,1) | (2,2) | (2,3) |
i:1 | (1,0) | (1,1) | (1,2) | (1,3) |
i:0 | (0,0) | (0,1) | (0,2) | (0,3) |
NORTH (row,col) | j:0 | j:1 | j:2 | j:3 |
AFTER
WEST (row,col) | i:0 | i:1 | i:2 | i:3 |
---|---|---|---|---|
j:0 | (3,0) | (3,1) | (3,2) | (3.3) |
j:1 | (2,0) | (2,1) | (2,2) | (2,3) |
j:2 | (1,0) | (1,1) | (1,2) | (1,3) |
j:3 | (0,0) | (0,1) | (0,2) | (0,3) |
不难看出 NORTH(i,j)
== WEST(3-j,i)
根据下面的函数
/** Return the standard column number for square (C, R) on a board
* of size SIZE oriented with this Side on top. */
// 计算相对列数
/** 当NORTH时,return c;
* 当WEST时,return 3-r;
*/
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. */
// 计算相对行数
/** 当NORTH时,return r;
* 当WEST时,return c;
*/
public int row(int c, int r, int size) {
return _row0 * (size - 1) - c * _dcol + r * _drow;
}
与我们的推理相符合
注意:这个程序中,标准坐标始终是以
Side NORTH
为标准坐标,使用语句this.board.setViewingPerspective(side);
仅仅是告诉程序,将我们输入的int i, j;
以何种规则转化为标准坐标。
即我们输入的是相对坐标,程序将相对坐标转化为绝对坐标(标准坐标),我们对相对坐标进行向上的操作,被程序转化为对应的绝对坐标的向左,右,上,下的操作。
程序逻辑和游戏规则准确匹配
直接上一个手写图示
字迹一般,忍一下,sorry
但是除了这些仍有些情况我们忽略了
举个例子
4 | 0 | 2 | 2 | (右移) |
---|---|---|---|---|
正确结果 | ||||
0 | 0 | 4 | 4 | |
我们的结果 | ||||
0 | 0 | 0 | 8 |
我们的结果违反了规则中的这一条
- 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.原文位置
即是说,做过合并的格子不可以再一次进行合并,所以在程序里我们声明变量 bottom
表示可以移动到的最后一个待检测格子。
还是示意图
所以我们就顺利完成了这个逻辑的设计!
看不懂图的试试直接看代码,图和代码都看不懂的可以试着再评论区留言,应该有路过的大佬出手。。。
作者本人也会看评论区的,希望帮到大家。
结语(废话)
个人认为 proj0
的难点主要在对 java
语言的不熟悉,这篇博客技术含量基本没有,但是希望能帮到一些刚开始自学的小白解决一些问题,关于 CS61B fa-23
的后续 lab
和 proj
应该会继续更新,毕竟现在网上关于 fa-23
的解析比较少,HW
不一定都会写,但是写的应该会选个人认为有价值的写一写分享,希望能让自学的朋友少一点崩溃 😃
课程整个学完了应该会把所以源码发到github或gitee上,但是不知道要等到什么时候哩(bushi