在 Linux 下實現智慧貪吃蛇
本週的任務是在 Linux 環境下編寫程式碼,實現一隻智慧的貪吃蛇,使其能通過演算法具有 “感知 - 決策 - 行動” 的能力。
熟悉 Linux 環境
對於 Linux ,我之前早有耳聞,卻沒有真正的使用過。大家都說計算機從業者應該學會使用 Linux ,但具體是為什麼我也不甚了了。總之,我這次總算有機會在自己的計算機上裝一個 Linux ,實際感受一下。
下載安裝 Ubuntu
作為一個 MacBook 使用者,此前就已經購買了最新版的 Parallel Desktop 軟體。而下載安裝免費的開源系統Ubuntu Linux 也就非常方便了。
點選免費系統下的 下載 Ubuntu Linux 即可
終端與 vim 編輯器
進入到 Ubuntu,可以看到類似這樣的桌面。如果你找不到終端在哪裡開啟,可以輸入快捷鍵ctrl + alt + T開啟。
然後在終端中鍵入
sudo apt-get install vim
下載安裝 vim 編輯器。
一般系統會 built-in gcc 編譯器。你可以在終端中輸入gcc -v
檢視你的 gcc 版本。如果沒有,仿照上面方法鍵入
sudo apt-get install gcc
即可安裝 gcc 。
然後就可以用 vim 來編輯程式碼,或直接用 gcc 編譯寫好的程式碼了!
VT 100 終端標準
在字元終端上完成“清屏”“修改游標位置”“設定字元前景和背景色”等操作,是通過輸出 esc序列實現的。對於 VT100 終端, printf(“\033[2J”) 就實現了清屏。詳細內容可以參考,
然而在實踐中我發現,上述的控制碼雖然看起來實現了清屏,但是其實之前列印的東西其實還留在上面,並沒有真的被清除。想象一下如果寫一個貪吃蛇遊戲,貪吃蛇的每次移動都會留下一個介面,那一定不是我們想要的效果。
可以看到右邊是有滾動條的
通過搜尋,我瞭解到把上述printf("\033[2J")
改為printf("\033[3J")
或printf("\e[3J")
即可真正清除之前的列印記錄。(事實上,在程式碼裡是system("clear && printf '\e[3J'")
;。)
改為system(“printf ‘\e[3J’”);後,之前的記錄基本清楚了,但仍保留著一個半介面
再改為system(“clear && printf ‘\e[3J’”);後,介面就很完美啦
加入 kbhit()
在加入 kbhit() 之前,我們的貪吃蛇遊戲還做不到在不輸入時使蛇自動向當前方向前進一步,並且還會顯示輸入的字元,還需要摁回車確認。遊戲體驗很糟。
(具體實現程式碼可參閱我下面一篇部落格)
編寫智慧演算法
在完成上述的學習後,我們終於可以開始編寫我們的人工智慧演算法,使貪吃蛇能夠每秒自動走一步了。
智慧演算法中最關鍵的函式是決定智慧蛇接下來往哪個方向走的函式——即返回一個決定方向的字元的函式——其餘則與正常貪吃蛇沒有什麼不同。
基礎智慧演算法
這個函式初步的演算法如下:
def whereGoNext{
move[4] = {'a', 'd', 'w', 's'}
distance[4] = {0, 0, 0, 0}
FOR i in range(4)
IF 蛇按照move[i]的方向走一步沒有走到' '
IF 走到的是食物
return move[i]
ELSE
distance[i] = 9999
ELSE
distance[i] = |Fx - H'x| + |Fy - H'y|
ENDFOR
選出distance中最小的值對應下表p
IF distance[p] = 9999
game over
ELSE
return move[p]
}
該演算法可用如下C語言程式碼實現:
char whereGoNext(void){
char move[4] = {'a', 'd', 'w', 's'};
int minDistIndex = 0;
int distance[4] = {0, 0, 0, 0};
int i = snakeLength - 1;
int dx = 0, dy = 0;
int moveIndex = 0;
for (moveIndex = 0; moveIndex < 4; moveIndex ++) {
switch (move[moveIndex]) {
case 'a':
dx = -1;
dy = 0;
break;
case 'd':
dx = 1;
dy = 0;
break;
case 'w':
dx = 0;
dy = 1;
break;
case 's':
dx = 0;
dy = -1;
break;
default:
break;
}
// if next step is eating the food, do it.
if (map[snakeY[i] - dy][snakeX[i] + dx] == '$') {
return move[moveIndex];
}
// cannot move backward
else if (snakeX[i] + dx == snakeX[i - 1] && snakeY[i] - dy == snakeY[i - 1]) {
distance[moveIndex] = 9999;
}
// cannot move to somewhere not a blank
else if (map[snakeY[i] - dy][snakeX[i] + dx] != ' '){ /*have already determined if it is '$'*/
distance[moveIndex] = 9999;
}
else{
distance[moveIndex] = abs(foodX - (snakeX[i] + dx)) + abs(foodY - (snakeY[i] - dy));
}
if (distance[minDistIndex] > distance[moveIndex]) {
minDistIndex = moveIndex;
}
}
if (distance[minDistIndex] == 9999) {
isGameOver = 1;
}
return move[minDistIndex];
}
最終的效果如圖:
然而目前的智慧演算法還有很大的改進空間。比如遇到這樣的情況時,
智慧蛇就會義無反顧的向前。並被這個長度為 5 的障礙物困死。
BFS 廣度優先搜尋演算法
演算法的思路可以大致概括為:探索蛇頭周圍去到的位置,再探索這些位置周圍可能的位置……直到第一次找到食物,即找到了從當前位置到食物的最短路徑。
這個演算法可以保證蛇絕不會死,然而問題也顯而易見,就是如果找不到要找的路徑怎麼辦。比如對於下圖的情況來說:
蛇無法找到蛇頭到食物的路徑。然而是不是意味著遊戲結束了呢?當然不是,蛇只要再往前走兩步就有路徑了。所以問題的本質在於,上述演算法沒有考慮到整個圖是動態的,蛇每走一步,蛇尾也發生了變化。
判斷蛇頭到蛇尾是否聯通的演算法
通過查閱部落格我瞭解到,只要蛇頭和蛇尾保持聯通,那蛇就一定不會進入死衚衕。所以我們可以定義一條虛擬蛇。在每一次真實蛇去吃食物之前,先讓虛擬蛇去吃,如果吃完之後的虛擬蛇蛇頭與蛇尾是聯通的,則說明這是一條安全路徑,可以放心去吃;如果不能聯通,則向遠離蛇尾的方向徘徊一步——在行動之前先想象一下行動的結果,聽起來很有智慧的意味。