前言
为什么没有week1的内容呢?🧐
答案是我偷懒了,没有写。😴
以下记录一些自己上cs61b的感悟,以及编程的思路。完整的项目代码见github上的my-cs61b。
lab2
这个 lab 2 主要是教你如何使用 IDEA 的 Debugger 来 debug。
对于这种现代化的 IDE,debug 确实比较容易,可以方便地显示不同变量,提供步入步出等操作,起到隔离信息的作用。
理想的 debug 模式:首先根据一行代码和当前变量,预期这一行代码的功能。如果一致,就说明这一行没出错;如果发现矛盾,说明这一行有问题,或者这一行调用的函数有问题。这时候,可以进一步步入函数。重复上面的步骤,直到发现问题所在。
理想的 debug 应该实现抽象的功能,不能直接就步入每一个函数,不然只会增加工作量。应该先假设所有函数都能正常工作,直到找到不满足预期的情况。这样,才能高效地找到代码出错的部分。
同时,课程还强调了测试驱动开发(Test-driven development, TDD)。在实际编写各种函数之前,首先写一些测试用例,同时可以考虑一些边缘的情况。然后,测试一下,自然全部都无法通过。这时候,先聚焦于比较简单的测试,想办法让该测试通过。然后,聚焦于剩下失败的测试,直到最后通过所有测试。
相比于直接写代码,先写测试可以让你明确输入输出用例。在实际例子的帮助下,可以对代码有更深的理解。一个个通过的测试,也能像玩游戏一样,增加开发的动力。
lec5 SLLists, Nested Classes, Sentinel Nodes
相比于直接的 IntList,SLList 提供了更好的 api,拥有 addFirst()
方法和 getFirst()
方法,方便调用。
Private
对于用户来说,不用考虑递归的细节。
通过将一些类的成员设置成 private,那么类之外无法访问这些成员。这可以隐藏细节。事实上,private 也不是绝对不能访问,可以用库做到这一点。它更像是一种提醒。
Nested class
可以在类当中定义其他的类。
嵌套的类可以实现更多的功能。
保持代码简单
不要增加过多特殊情况。最好寻找普遍的方法,覆盖各种情况。
可以对链表引入假头,又称为哨兵 (sentinel)。
project0 2048
这个项目需要你去实现 2048 游戏的核心功能。自然地,作为第 0 个项目,许多繁杂的部分,包括 GUI、随机生成方块、各种类的抽象,都已经写好了。只需要在 Model.java
中继续写游戏的核心逻辑即可。
这个项目的前三个函数都很容易,最后一个tilt
函数有点意思。
emptySpaceExists
原型: public static boolean emptySpaceExists(Board b)
该函数接受一个 Board 输入,然后判断是否有空的格子。这需要使用 Board 的方法 tile(int col, int row)
从而得到具体的格子。需要注意,如果某个地方没有 Tile,那么该方法返回的是 null
,而不是 0。我想之所以要这样写,是因为这样可以更明确地表示某个地方为空。如果是 0,可能在后续编写代码的时候有歧义。
同时,也需要用到 Board 的 size()
方法,获得大小。代码相当简单,只需要用一个二层循环遍历每一个格子,如果某一个格子为空,返回 true
即可。否则返回 false
。
|
|
maxTileExists
原型:public static boolean maxTileExists(Board b)
这个函数用来检验是否有达到最大值的格子,也就是 2048。这里强调了不要直接将数字硬编码写在程序里,避免 magic number
,而是要使用常量,如 MAX_PIECE
来表示2048。
相当简单,同上,二层循环遍历格子即可。
|
|
atLeastOneMoveExists
原型:public static boolean atLeastOneMoveExists(Board b)
这个函数用来判断当前是否有存在可以移动的空间。如果 Board 上还有空的格子,自然可以继续移动。如果全满,则判断是否有相邻的相同大小的格子存在。如果有,则说明还可以移动。
要解决这个问题,可以分成两个部分——水平相邻和竖直相邻,分别检验,二者对称。可以使用二层循环。
在检验过程中,如果发现相邻中任何一个格子为 null
,则跳过当前循环。然后,使用 b.tile(col, row).value()
来获取值并且比较。
|
|
tilt
原型:public boolean tilt(Side side)
这个函数接受 Side 输入,表明往哪个方向移动。同时,方法需要修改 Board 至移动后的状态,同时增加 score
变量的值,并将 changed
变量设置为 true
。
所有的移动操作都要使用 Board.move(col, row, tile)
。
移动规则
以下的X代表该格子为空。
- 两个相同值的格子,会合成一个有两倍值的格子。
- 合成过后的格子不会再次参与合成。例如
[X, 2, 2, 4]
向左移动,则会变为[4, 4, X, X]
,而不是[8, X, X, X]
。 - 如果有大于等于 3 个相邻格子,则会先合并移动方向上的两个格子。例如,
[X, 2, 2, 2]
向左移动会得到[4, 2, X, X]
而不是[2, 4, X, X]
。
简化的手段
首先,如果有四个移动的方向,那么最后可能要把已有的结果复制粘贴四次,并进行修改。这样很明显相当麻烦,并且很容易出错,增加了维护难度。
因此,Board 已经内置了 setViewingPerspective(side)
方法。只要首先设置视角的方向,就可以把各个方向的操作,都转换成向上移动的操作,然后使用同样的代码求解,最后再把视角变换回来。
同时,合并的具体过程已经经过抽象了。只需要使用 broad.move(col,row,tile)
方法。如果该移动后与原先格子发生合并,则该函数返回 true
,否则返回false
。
方法
在向上移动的时候,很明显每一列都是相互独立的,因此可以考虑封装每一列,传入 Board 和当前列的索引,进而使代码简化。
在每一列中,因为都是向上移动,因此总可以从上往下考虑每一个格子。
需要记录的状态
在 public boolean tiltColumnNorth(int col)
方法中,传入 col,执行一列的操作,并且返回 boolen 代表有没有改变。
对于每一列,从上到下,将 row
从 n-1
遍历到 0
。
为了记录好每次的状态,需要以下变量:
row
:当前行数lastValue
:上一格的值。一开始设置为-1。val
:现在一格的值。只有在当前格非空时才有用。lastTileRow
:上一格的位置。一开始设置为n
。isLastMerged
:上一格是否合并过。一开始设置为false
。
可能的动作:
- 不移动当前格。
- 移动当前格,但是不合并。
- 移动当前格,同时合并。
一开始,我没要搞清楚这几种动作,并且都写在一个函数里面,导致很难维护。后来,根据这些状态,写了一个函数calculateState
,根据返回值来确定状态。然后,在 tiltColumnNorth
方法当中使用 switch 语句。
CalculateState
- 不移动的情况。这时候,
row
等于lastTileRow-1
,并且上一格的值和这一格的值不相等。(此处这一格的值和上一个相等,就一定能移动,因为是从上往下遍历,不会出现上面一个格合并后还和下面的格紧挨着) - 移动但是不合并的情况。这时候,要么
val
不等于lastValue
,要么isLastMerged
值为真,也就是上面一个格合并过。 - 移动并且合并。是上面两种情况的剩下情况。
TiltColumnNorth
获取状态后,就容易执行相应的操作了。
- 不移动时,
lastTileRow
减少 1,isLastMerged
为假,lastValue
赋值为val
。 - 移动但不合并时,将当前格移动到
(col, lastTileRow-1)
处,changed
为真,isLastMerged
为假,lastTileRow
减少 1,lastValue
赋值为val
。 - 移动并且合并时,将当前格移动到
(col, lastTileRow)
处,changed
为真,isLastMerged
为真,lastTileRow
减少 1,lastValue
赋值为两倍的val
。
最后返回 changed
。
完整代码
|
|