用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中,喜歡的麻煩點個贊或者關注噢。