用c++实现2048---核心算法
写在前面
这一篇主要讲如何相应操纵者给出的指令,对地图进行相应的操作。
这里我最开始的思路很简单,就是将地图的移动方式分为四种,即上(w)下(s)左(a)右(d)。
但是如果分为四个函数的话,那就太繁琐了,复用性太差,相同的代码复制四次只修改一点,十分难看,所以通过思考将四个移动方式合并成为一个移动方式就是本篇所要思考的内容。
根据指令移动地图
最外层思路
在最外层的MoveMap()函数就是用来识别wasd这四个指令。
void MoveMap(int(&MAP)[MAP_SIZE][MAP_SIZE], const char command, int &score) {
/*根据指令移动地图*/
switch (command) {
case 'w':
Move(MAP, 0, 0, score);
break;
case 's':
Move(MAP, 0, MAP_SIZE - 1, score);
break;
case 'a':
Move(MAP, 1, 0, score);
break;
default:
Move(MAP, 1, MAP_SIZE - 1, score);
}
}
用一个switch来识别四个方向,通过修改函数Move()内的参数来识别是哪个方向的。
通过观察可以发现:
- 向上移动的参数为0, 0
- 向下移动的参数是0, MAP_SIZE-1
- 向左移动个参数是1, 0
- 向右移动的参数是1, MAP_SIZE-1
也就是说,两两之间存在联系。下面我就详细解释一下怎么将其通过转换将相似之处合并的。
Move函数
函数的代码实现如下
void Move(int(&MAP)[MAP_SIZE][MAP_SIZE], int direction, int position, int &score) {
/*竖直移动合并*/
//函数声明
void DeleteZero(int(&MAP)[MAP_SIZE][MAP_SIZE], int position);
void Transpose(int(&MAP)[MAP_SIZE][MAP_SIZE]);
//如果是水平方向上的话, 先转置
if (direction == 1) {
Transpose(MAP);
}
//首先移动所有格子,让其靠在一起
DeleteZero(MAP, position);
for (int j = 0; j < MAP_SIZE; j++) {
int start = abs(MAP_SIZE - 1 - position);
int curPos = -start;
//合并
while (curPos-start < MAP_SIZE-1) {
int cur = MAP[abs(curPos)][j];
int next = abs(curPos + 1);
if ((cur != 0) && (cur == MAP[next][j])) {
MAP[next][j] = 0;
MAP[abs(curPos)][j] *= 2;
curPos += 2;
score++;
}
else {
curPos++;
}
}
}
//补充因合并产生的空位
DeleteZero(MAP, position);
//如果是水平方向上的话, 最后再转置一下
if (direction == 1) {
Transpose(MAP);
}
}
函数总共传入四个参数,第一个就是地图,第二个是移动的方向,第三个为扫描的起始位置,第四个为分数。
转置地图
首先我考虑的是,如何将上下移动和左右移动合并到一起。
因为地图MAP是一个二维数组,所以我们可以这样思考:
- 向上移动是令所有格子从每一列标号为MAP_SIZE-1的位置移向0位置。
- 向左移动是令所有格子从每一行标号为MAP_SIZE-1的位置移向0位置。
- 向下移动是令所有格子从每一列标号为0的位置移向MAP_SIZE-1位置。
- 向右移动是令所有格子从每一行标号为0的位置移向MAP_SIZE-1位置。
那么这样一分就应该很明白了,参数direction用来分辨是对行操作还是对列操作(分辨上下还是左右),参数posititon是区分从哪里移动到哪里(分辨上左还是下右)。
那么如果将左右通过变换为上下操作,那么我们就可以只考虑position了。所以我这里用了转置。
通过将整个地图作为一个矩阵进行转置,即通过对对角线进行反转,那么就可以将左右变换为上下。
所以通过direction分辨是上下还是左右,上下为0,左右为1。如果为左右,就对地图进行转置,即函数Transpose()。通过转置以后,我们就可以只考虑移动的问题了。
移动问题
因为地图中可能所有的格点没有全部都靠在一起,这样会对我们识别相同格子造成困扰,所以我们需要首先移动格子,让所有格子靠在一起,移动的方向就是通过操纵者决定。这里通过DeleteZero()函数决定。
void DeleteZero(int(&MAP)[MAP_SIZE][MAP_SIZE], int position) {
/*移动地图(竖直移动)而进行的靠拢, 即删除0*/
for (int j = 0; j < MAP_SIZE; j++) {
for (int i = 0 - position; i < MAP_SIZE - 1 - position; i++) {
if (MAP[abs(i)][j] == 0) {
//找到从i往后第一个不为0的坐标,然后把其填到当前位置
for (int zeroPos = i + 1; zeroPos < MAP_SIZE - position; zeroPos++) {
if (MAP[abs(zeroPos)][j] != 0) {
MAP[abs(i)][j] = MAP[abs(zeroPos)][j];
MAP[abs(zeroPos)][j] = 0;
break;
}
}
}
}
}
}
原理就是通过position参数将上下移动合并。从0到MAP_SIZE-1不变,从MAP_SIZE-1到0则转化为-MAP_SIZE到0,这样就可以统一了两者的移动方向,移动的话只需要+1即可。然后在寻找坐标的时候取绝对值。
之后的合并也是同理,通过将其在坐标系上化为从-MAP_SIZE-1到MAP_SIZE-1再去绝对值即可。然后对每一个格点判断移动方向上是否有相同的格子,如果有的话就合并。
合并结束后再合并一下因为合并产生的空位,让所有格点靠在一起。
最后再将水平方向上的移动转置以下,回归成原来的水平方向。
判断是否结束
合并操作完以后,就需要判断游戏是否结束了。
bool IsOver(int(&MAP)[MAP_SIZE][MAP_SIZE]) {
/*判断游戏是否结束*/
for (int i = 0; i < MAP_SIZE; i++) {
for (int j = 0; j < MAP_SIZE; j++) {
if (MAP[i][j] == 0) {
return false;
}
}
}
return true;
}
逻辑也很简单,如果MAP中没有0了,那就返回已经空了,否则跳出返回还没空。
最后
以上就是实现游戏2048的一个简易过程。其实过程比较粗糙,很多地方还能够再优化一下,不过目前的程度的确已经足够了,所以就不再继续深入了。
下一步的目标是以这个程序为基础,编写一个可以自动玩2048的AI。
所有代码都放在我的GitHub中,喜欢的麻烦点个赞或者关注噢。