UCB CS61B fa23 proj0(2048) 解析与答案

写在前面的废话

作者本人只是一个小白,在自学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;
    }

第一次循环遍历示意图

0123
0XXXX
1XXXX
2XXXX
3j+1j+1j+1j+1

第二次循环遍历示意图

0123
0XXXj+1
1XXXj+1
2XXXj+1
3XXXj+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 为标准方向)

3XXXX
2XXXX
1XXXX
0XXXX
NORTH0123

为了让按下 左方向键 的操作等价于按下 上方向键 的操作,我们让格子“旋转”。

NORTH0123
0XXXX
1XXXX
2XXXX
3XXXX

注意到此时每一列的标号没有改变,但是每一个格子的行号发生了改变

下面表上每个格子位置的改变
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:0j:1j:2j:3

AFTER

WEST (row,col)i:0i:1i:2i: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; 以何种规则转化为标准坐标。
即我们输入的是相对坐标,程序将相对坐标转化为绝对坐标(标准坐标),我们对相对坐标进行向上的操作,被程序转化为对应的绝对坐标的向左,右,上,下的操作。

程序逻辑和游戏规则准确匹配

直接上一个手写图示图解1

字迹一般,忍一下,sorry

但是除了这些仍有些情况我们忽略了
举个例子

4022(右移)
正确结果
0044
我们的结果
0008

我们的结果违反了规则中的这一条

  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.原文位置

即是说,做过合并的格子不可以再一次进行合并,所以在程序里我们声明变量 bottom 表示可以移动到的最后一个待检测格子。

还是示意图
图解2
所以我们就顺利完成了这个逻辑的设计!

看不懂图的试试直接看代码,图和代码都看不懂的可以试着再评论区留言,应该有路过的大佬出手。。。
作者本人也会看评论区的,希望帮到大家。

结语(废话)

个人认为 proj0 的难点主要在对 java 语言的不熟悉,这篇博客技术含量基本没有,但是希望能帮到一些刚开始自学的小白解决一些问题,关于 CS61B fa-23 的后续 labproj 应该会继续更新,毕竟现在网上关于 fa-23 的解析比较少,HW 不一定都会写,但是写的应该会选个人认为有价值的写一写分享,希望能让自学的朋友少一点崩溃 😃
课程整个学完了应该会把所以源码发到github或gitee上,但是不知道要等到什么时候哩(bushi

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值