CS61B Proj0 2048
极度建议:
第四个问题tilt 逻辑很复杂, 如果不会测试, 最好先看完3.1, 了解测试的知识后再继续写. 不然计算使用FP的编程方式, 方便测试, 但是语句执行的过程中出现问题的地方, 依然发现不了, 而且对这个问题, 需要很多耐心, 本人从早上6:00 一直到下午14:00 间断性的完成本题, 最后再学习完3.1, 知道测试的方法后, 重新排查, 最后才发现了问题所在.
游戏
游戏背景:
在一个正方形的方格网络中, 每个方格可能是空的, 也可以是一个带有2 或者 2的次方整数.
游戏阶段:
在游戏的起始阶段, 会有随机2(75%可能性) 或者 4(25%可能性)的方格出现在背景板中.
玩家行为:
可以以东南西北四个方向移动方格网络. 图块也会随着这个方向移动.
规则
移动的规则:
- 每一次移动, 方格网络中都会有随机数量的图块添加到空方块
合并的规则:
- 两个图块如果数值相同, 则移动后会形成一个拥有两个图块数值之和的新图块出现
- 合并后出现的新方块不会和其他方块合并
- 当两个图块合并时, 玩家会获得合并后的积分作为自身的分数
分数规则:
- 每次移动, 都会更新
- 更新增加的分数为和新出现的方块的数值之和
输赢规则:
输: 倾斜改变不了棋盘 赢: 有数值为2048的方块出现
可能会感觉到挫折的地方:
其中会有初学者不熟悉的语法, 甚至未来的课程也不会出现.
游戏者中的类:
1. Tile类 -- 方格:
Tile 实例 == null, 表示为空的方块
我们只需要使用Tile的实例t的value方法, t.value()会返回方块的数值
2. Side类 -- 倾斜的方向:
Side类是一种特殊的类: Enum 我们不需要使用任何该类的任何方法, 也不需要操作它的实例变量
我们只需要使用该类的一种方法: 类属性的赋值.(Enum不能使用new关键字构造一个新的实例, 而是使用类属性传递数值)
Side s == Side.NORTH
复制代码
3. Model类 -- 模型:
该类代表整个有戏的状态. 我们只需要编辑该文件即可.
4. Board类 -- 背景:
这个代表游戏的方格网络背景.
可以假想为一个二元数组: 以列为索引, 按照从左往右的顺序存储数组.
每个数组再以行为索引, 从下往上存储元素.
我们只需要使用该类的以下三个实例方法:
- board.setViewingPerspective(Side) : 将board的Side方向设置为NORTH方向. (即方向标准以Side为NORTH方向为标准, 旋转board)
- board.tile(i, j) : 返回i列j行的tile的数值
- board.move(i, j, tile) : 移动board中的tile到board的(i列j行)中
任务
完成四个函数的逻辑
- emptySpaceExists
- maxTileExists
- atLeastMoveExists (相对困难)
- tilt (预计需要3 - 10h才能完成)
1. public static boolean emptySpaceExists(Board b)
要求:
- 如果棋盘中的任何一个图块为空, 这个方法返回True, 否则返回为False. 提示:
- 我们只需要使用Board的实例方法 tile 和 size
- Board使用对其数据使用private关键字, 我们不能直接使用这些数据. 只能使用操作这些数据的方法. (数据抽象)
使用的方法介绍:
- tile方法 接收两个参数, 列 和 行, 返回Board实例中对应行和类的方块的返回值.
- size | 不接受参数, 返回Board实例中的行和列的长度.
解决方法: 遍历二维数组, 判断其中的元素是否有null
可能出现的问题: board实例中的存储的不一定是tile, 还有可能是null.如果是null,不可访问属性值
public static boolean emptySpaceExists(Board b) {
// TODO: Fill in this function. check Board have empty tile.
for (int i = 0; i < b.size(); i += 1) {
for (int j = 0; j < b.size(); j += 1) {
if (b.tile(i, j) == null) {
return true;
}
}
}
return false;
}
复制代码
2. public static boolean maxTileExists(Board b)
要求: 如果Board实例中有一个方格的数值为2048. 返回True. 但是我们不应该使用2048这个数值, 而是使用MAX_PIECE这个变量代替2048.
解决思路: 遍历二维数组, 判断其中的元素是否有2048.
public static boolean maxTileExists(Board b) {
// TODO: Fill in this function.
/*
* this is problem is using null compare with int, but they can't compare
* so that it need to add condition that t is null type.
* */
/*
* null can only compare with object rather than other primitive type
* */
for (int i = 0; i < b.size(); i += 1) {
for (int j = 0; j < b.size(); j += 1) {
if (b.tile(i, j) != null && b.tile(i, j).value() == MAX_PIECE) {
return true;
}
}
}
return false;
}
复制代码
3. public static boolean atLeastOneMoveExists(Board b)
要求: 如果使用了倾斜了, 方格网络中的方格还可以移动, 返回True, 否则返回False
有效移动的条件:
- 有一个方格为null
- 有一个方格为2048
- 两个相邻的图块的数值为相同的值
解题方法:
- 对于条件1和2:
直接使用之前的两个方法即可.
- 对于条件3:
遍历i-1列 和 j-1行的二维数组. 对其中的每个元素进行一次左右和上下比较.
因为不能覆盖最后一列的上下比较 和 最后一行的左右比较. 所以添加一下特殊条件
当为i - 1 列的时候进行一次 (i + 1, j) 和 (i + 1, j + 1) 的比较.
当为j - 1 行的时候进行一次 (i, j + 1) 和 (i + 1, j + 1) 的比较.
但是本人因为懒, 直接将逻辑整合到了一起.
public static boolean atLeastOneMoveExists(Board b) {
// TODO: Fill in this function.
if (emptySpaceExists(b) || maxTileExists(b)) {
return true;
}
for (int i = 0; i < b.size() - 1; i += 1) {
for (int j = 0; j < b.size() - 1; j += 1) {
if (b.tile(i, j) != null && b.tile(i + 1, j) != null) {
int left = b.tile(i, j).value();
int right = b.tile(i + 1, j).value();
if (left == right) {
return true;
}
}
if (b.tile(i, j) != null && b.tile(i, j + 1) != null) {
int up = b.tile(i, j).value();
int down = b.tile(i, j + 1).value();
if (up == down) {
return true;
}
}
if (b.tile(i + 1, j + 1) != null && b.tile(i + 1, j) != null) {
if (b.tile(i + 1, j + 1).value() == b.tile(i + 1, j).value()) {
return true;
}
}
if (b.tile(i + 1, j + 1) != null && b.tile(i, j + 1) != null) {
if (b.tile(i + 1, j + 1).value() == b.tile(i, j + 1).value()) {
return true;
}
}
}
}
return false;
}
复制代码
4. public boolean tilt(Side side)
这是游戏的主要逻辑 -- 控制方格的移动, 但是很困难. 我们从这个问题中, 可以初步意识到到计算机的本质 就是 管理复杂性.
要求:
- 按照side方向, 移动棋盘. 如果棋盘发生了变化. 将提供的骨骼中的changed变量设置为true.
- 记录因为合并产生的图块的数据总值. 使用score(score为Model类中的私有变量)
score += board.tile(i, j).value()
复制代码
- 所有的移动由Board实例的move方法实现. 每次移动对于给定图块调用一次move.
- 每一次访问方格使用Board类的tile方法进行访问.
方法:
- 有一个TestUpOnly类, 提供了NORTH方向的测试.
- 该测试使用的背景假想为行从下往上(0-3). 列从左往右(0-3).
- 在每列(以0为准)中, 顶行(0,3)元素不变, 所有的操作从次顶行(0,2)元素 至 最后一行(0,0)元素.
- 如果上行为空, 下行的元素可以往上移动.
- 如果上行元素的数值 和 下行元素的数值相同. 也可以移动到改行, move方法自动在该位置自动建立一个新数值(为原先两倍)的方块.
- move的使用方法, 不是一格一格的移动(诸如: 1->2, 2->3), 而是整体移动. 诸如 TIle t = board.tile(0, 0) 它可以到达(0, 2). 那么直接使用board.move(0, 2, t) 7.Board实例有一个setViewPerspective(SIde s)方法, 可以改变背景的方向.
输入一个方向, 背景板则以该方向为NORTH方向.
诸如输入SOUTH, 则原本的SOUTH为NORTH方向. 如果想要将背景恢复原来的方向, 只需要进行调用board.setViewPerspective(Side.North)即可.
解题思路:
-
将元素的移动以列为单位分为4组.
-
建立一个val数组收集元素, 便于接下来的比较.
-
从次高元素开始到最底层元素每个元素寻找上方是否有空格, 是否可以合并.
-
往上方寻找是否有可以移动的元素
- 上方有空的Tile
- 上方有数值和需要移动的Tile数值一样的方格
board实例中需要移动的tile的前后位置. 所以我们想要完成这些操作, 主要确定的是j的位置.
而后通过j的位置进行更改.Tile t = board.tile(i,j).value(); board.move(i, new_j, t) val[j] = 0; val[new_j] 复制代码-
score 和 val数组: 因为逻辑不同, 不能将两者的逻辑整合到一起.
- 如果移动到达的位置空格, score不变. val初始位置置0, 新位置置原来位置的数值
- 如果移动到达的位置不是空格, score改变. val初始位置置0, 新位置置-1(因为不能继续合并了)
-
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);
for(int i = 0; i < size(); i += 1) {
int[] val = col_tile(board, i);
// 出现问题的地方, 将val[j] != 0 渐入suite中, 不然达不到条件直接直接返回StopIteration.
for (int j = size() - 2; j >= 0; j -= 1) {
if (val[j] == 0) {
continue;
}
Tile t = board.tile(i, j);
// 选择是有空格
int zero = zero_row(val, j);
// 1. 有空格
if (zero != j) {
board.move(i, zero, t);
val[j] = 0;
val[zero] = board.tile(i, zero).value();
changed = true;
// 移动到空格处了, 看是否可以合并. 如果可以合并. 如果不可以合并, 不需要操作
if (up_equal(val, zero, zero)) {
val[zero] = 0;
val[zero + 1] = -1;
t = board.tile(i, zero);
board.move(i, zero + 1, t);
score += board.tile(i, zero + 1).value();
changed = true;
}
// 2. 没有空格
} else {
if (up_equal(val, j, j)) {
val[j] = 0;
val[zero + 1] = -1;
t = board.tile(i, j);
board.move(i, j + 1, t);
score += board.tile(i, j + 1).value();
changed = true;
}
}
}
}
board.setViewingPerspective(Side.NORTH);
checkGameOver();
if (changed) {
setChanged();
}
return changed;
}
/** 将Board实例中的第i列元素收集到一个列表中 */
public static int[] col_tile(Board b, int i) {
int[] test = new int[b.size()];
for (int j = 0; j < b.size(); j += 1) {
if (b.tile(i,j) == null) {
test[j] = 0;
} else {
test[j] = b.tile(i,j).value();
}
}
return test;
}
/** 返回数组lst第i个元素之后最后一个0的位置 */
public static int zero_row(int[] lst, int i) {
int res = i + 1;
for (res = i + 1; res < lst.length; res += 1) {
if (lst[res] != 0) {
return res - 1;
}
}
return res - 1;
}
// 这个出现了问题, 实际想要解决的问题 和 这个函数不一致
/** 判断数组当前值和zero + 1 值是否相等 */
public static boolean up_equal(int[] lst, int zero, int j) {
if (zero + 1 >= lst.length) {
return false;
}
int test1 = lst[j];
int test2 = lst[zero + 1];
return test1 == test2;
}