【简单AI井字棋】
井字棋AI開發
- 1.井字棋的原理
- 井字棋流程圖
- 井字棋實現過程
- 1.游戲菜單
- 2.打印棋盤
- 3.人先走
- 4.電腦先走
- 5.玩家下棋
- 6.電腦下棋
- 原始方法
- AI下棋
- 1.暴力枚舉
- 2.極大極小算法
我們要開發一款AI項目,最重要的是得理解它的原理
1.井字棋的原理
雙方輪流下棋,若其中一方在水平、垂直、斜線方向上形成三個棋子連線,則獲勝
井字棋流程圖
井字棋實現過程
1.游戲菜單
這是一個雙人游戲,自然有一個先后的問題,那么人先走和電腦先走的情況肯定是不一樣的,所以我們得用switch函數將其分開來運行。
void menu() {printf("\n\n\n");printf("*************************************************\n");printf("******* ********\n");printf("******* 歡迎使用井字棋人機對弈系統 ********\n");printf("******* ********\n");printf("*************************************************\n");printf("\n\n\n");printf("正在為您跳轉,請稍后...\n");Sleep(2000);system("CLS");printf("\n\n\n");printf("**************************************\n");printf("******* 1.PlayerFirst ********\n");printf("******* 2.ComputerFirst ********\n");printf("******* 0.Exit ********\n");printf("**************************************\n");}2.打印棋盤
在沒有下棋之前,棋盤肯定是空的,所以我們得將棋盤初始化,我們每下一次,棋盤就變化一次,因此我們還得打印一下棋盤,以便我們觀察。
棋盤呢有多種形式,帶邊框的和不帶邊框的就看你喜歡哪種了,具體思路是一樣的,就是細節上有略微的差異
3.人先走
void game1() {//存儲數據--二維數組char board[ROW][COL];//棋盤初始化Inintboard(board, ROW, COL);//打印棋盤Displayboard();char ret = 0;while (1){//玩家下棋PlayerMove();Displayboard();//判斷玩家是否贏了ret = IsWin();if (ret != 'C')break;//電腦下棋ComputerMove();Displayboard();//判斷電腦是否贏了ret = IsWin();if (ret != 'C')break;}if(ret == 'O'){Sleep(1000);system("CLS");printf("玩家贏了\n");Sleep(2000);}else if (ret == 'X'){Sleep(1000);system("CLS");printf("電腦贏了\n");Sleep(2000);}else if(ret == 'Q'){Sleep(1000);system("CLS");printf("平局\n");Sleep(2000);}Displayboard(); }4.電腦先走
void game2() {//存儲數據--二維數組char board[ROW][COL];//棋盤初始化Inintboard(board, ROW, COL);//打印棋盤Displayboard();char ret = 0;while (1){//電腦下棋ComputerMove();Displayboard();//判斷電腦是否贏了ret = IsWin();if (ret != 'C')break;//玩家下棋PlayerMove();Displayboard();//判斷玩家是否贏了ret = IsWin();if (ret != 'C')break;}if (ret == 'O'){Sleep(1000);system("CLS");printf("玩家贏了\n");Sleep(2000);}else if (ret == 'X'){Sleep(1000);system("CLS");printf("電腦贏了\n");Sleep(2000);}else if (ret == 'Q'){Sleep(1000);system("CLS");printf("平局\n");Sleep(2000);}Displayboard(); }5.玩家下棋
此項目中,我們用的是二維數組來制作的棋盤,所以玩家在下棋的時候只用輸入需要落入棋子的坐標即可,既然是輸入坐標,那么就有可能是輸入錯的,或者在該位置已經有棋子了,因此我們需要先判斷輸入坐標的合法性,如果合法了我們再判斷該位置是否有棋子了。
//玩家下棋 void PlayerMove() {printf("玩家走\n");int x = 0;int y = 0;while (1){printf("請輸入下棋的坐標>\n");scanf("%d %d", &x, &y);if (x >= 1 && x <= ROW && y >= 1 && y <= COL){if (board[x - 1][y - 1] == ' '){board[x - 1][y - 1] = MAN;break;}else{printf("坐標被占用,請重新輸入\n");}}else{printf("坐標錯誤,請重新輸入\n");}} }6.電腦下棋
原始方法
利用srand,rand函數產生1-3的隨機值x和y(因此無需判斷電腦下棋坐標的合法性),然后判斷該位置是否有棋子了,就可以了,本質上就是把玩家下棋里玩家輸入的坐標改成rand函數生成的隨機值,很簡單。
為啥這沒有代碼呢,別問,問就是博主要給你們看更高逼格的
AI下棋
針對井字棋游戲,在游戲設計過程中可按照以下基本原則進行:
1. 如果下在該位置可以贏棋,那么久下在該位置
2. 如果對手下在該位置可以贏棋,那就下在該位置
3. 如果中心位置空閑,那么下在中心位置要優于邊上和角上位置
4. 如果角上位置空閑,那么下在角上位置要優于邊上位置
5. 如果只有邊上位置空閑,那么只能下在邊上位置
在這呢,博主提供兩種方法,第一種暴力枚舉所有下棋的可能,然后下棋,第二種通過極大極小搜索算法找到每次下棋的最優解,然后下棋。
1.暴力枚舉
此種方法有局限性,只能是電腦先手,才能做到肯定贏棋,最差也是平局的局面,如果是人先手的話,人的下棋有9種可能,暴力枚舉的次數太多,這就沒必要了,這就是枚舉的局限性。
//電腦下棋 void ComputerMove(char board[ROW][COL], int row, int col) {int x = 0;int y = 0;int ret = CountNum(board, row, col);while (1){if (ret == 1|| ret == 3 || ret == 5 || ret == 7){x = rand() % row;y = rand() % col;if (board[x][y] == ' ')board[x][y] = 'X';break;}}if (ret == 0){board[0][0] = 'X';}if (ret == 2){//5 types , 8 situations// 1if (board[0][1] == 'O'){board[2][0] = 'X';}// 2if (board[1][0] == 'O'){board[0][2] = 'X';}// 3 if (board[0][2] == 'O'){board[2][2] = 'X';}// 4if (board[2][0] == 'O'){board[2][2] = 'X';}// 5if (board[2][1] == 'O'){board[2][0] = 'X';}// 6if (board[1][2] == 'O'){board[0][2] = 'X';}// 7if (board[1][1] == 'O'){board[0][1] = 'X';}// 8if (board[2][2] == 'O'){board[0][2] = 'X';}}if (ret == 4){// 1@1if (board[0][1] == 'O' && board[2][0] == 'X'){if (board[1][0] == 'O'){board[2][2] = 'X';}else{board[1][0] = 'X';}}// 2@1if (board[1][0] == 'O' && board[0][2] == 'X'){if (board[0][1] == 'O'){board[2][2] = 'X';}else{board[0][1] = 'X';}}// 3@1if (board[0][2] == 'O' && board[2][2] == 'X'){if (board[1][1] == 'O'){board[2][0] = 'X';}else{board[1][1] = 'X';}}// 4@1if (board[2][0] == 'O' && board[2][2] == 'X'){if (board[1][1] == 'O'){board[0][2] = 'X';}else{board[1][1] = 'X';}}// 5@1if (board[2][1] == 'O' && board[2][0] == 'X'){if (board[1][0] == 'O'){board[0][2] = 'X';}else{board[1][0] = 'X';}}// 6@1if (board[1][2] == 'O' && board[0][2] == 'X'){if (board[0][1] == 'O'){board[2][0] = 'X';}else{board[0][1] = 'X';}}// 7@1if (board[1][1] == 'O' && board[0][1] == 'X'){if (board[0][2] == 'O'){board[2][0] = 'X';}else{board[0][2] = 'X';}}// 8@1if (board[2][2] == 'O' && board[0][2] == 'O'){if (board[0][1] == 'O'){board[2][0] = 'X';}else{board[0][1] = 'X';}}}if (ret == 6){// 1@1@1if (board[0][0] == 'X' && board[2][0] == 'X'&& board[2][2] == 'X' && board[0][1] == 'O'&& board[1][0] == 'O'){if (board[1][1] == ' '){board[1][1] = 'X';}else{board[1][1] = 'X';}}// 2@1@1if (board[0][0] == 'X' && board[0][2] == 'X' &&board[2][2] == 'X' && board[0][1] == 'O' &&board[1][0] == 'O'){if (board[1][1] == ' '){board[1][1] = 'X';}else{board[1][1] = 'X';}}// 3@1@1if (board[0][0] == 'X' && board[2][0] == 'X' &&board[2][2] == 'X' && board[0][2] == 'O' &&board[1][1] == 'O'){if (board[1][0] == ' '){board[1][0] = 'X';}else{board[2][1] = 'X';}}// 4@1@1if (board[0][0] == 'X' && board[0][2] == 'X' &&board[2][2] == 'X' && board[2][0] == 'O' &&board[1][1] == 'O'){if (board[0][1] == ' '){board[0][1] = 'X';}else{board[1][2] = 'X';}}// 5@1@1if (board[0][0] == 'X' && board[2][0] == 'X' &&board[0][2] == 'X' && board[1][0] == 'O' &&board[2][1] == 'O'){if (board[0][1] == ' '){board[0][1] = 'X';}else{board[1][1] = 'X';}}// 6@1@1if (board[0][0] == 'X' && board[0][2] == 'X' &&board[2][0] == 'X' && board[0][1] == 'O' &&board[1][2] == 'O'){if (board[1][0] == ' '){board[1][0] = 'X';}else{board[1][1] = 'X';}}// 7@1@1if (board[0][0] == 'X' && board[0][1] == 'X' &&board[2][0] == 'X' && board[1][1] == 'O' &&board[0][2] == 'O'){if (board[1][0] == 'O'){board[1][2] = 'X';}else{board[1][0] = 'X';}}// 8@1@1if (board[0][0] == 'X' && board[0][2] == 'X' &&board[2][0] == 'X' && board[0][1] == 'O' &&board[2][2] == 'O'){if (board[1][0] == ' '){board[1][0] = 'X';}else{board[1][1] = 'X';}}}if (ret == 8){//7@1@1@1if (board[0][0] == 'X' && board[0][1] == 'X' &&board[1][2] == 'X' && board[2][0] == 'X' &&board[0][2] == 'O' && board[1][0] == 'O' &&board[1][1] == 'O'){if (board[2][1] == 'O'){board[2][2] = 'X';}else{board[2][1] = 'X';}}}}2.極大極小算法
為方便表達游戲的狀態,通常使用樹或圖來表達
為了更好的理解搜索過程,這里就先介紹博弈樹中一些常用的概念
最重要的就是估值函數,估值函數的能力真正決定了你所寫的AI的能力,估值函數判斷的越準確,AI越能找到最優解。
估值函數并不是唯一的,不同的人有不同的想法,用的編程語言也不相同,所寫出來的估值函數也就不一樣,所以下面我就展示偽碼來體現一種估值函數,希望讀者能有所感悟,能寫出更好的估值函數
function eval() {int temp[9] = 0for i:0 to 8temp[i] = board[i]int win = 0int lose = 0for i:0 to 8int sum = 0for j:0 to 2sum += temp[line[i][j]]if(sum == 3)then return MAX = +INFINITYelse if (sum == -3)then return MIN = -INFNINTYelse if (-3<sum<0)then lose ++else if(0<sum<3)then win ++endifreturn win-lose }估值函數的不同,寫出來的極大極小搜索算法也不同,下面我也僅通過偽碼來展示極大極小搜索算法
function minmaxSearch() {int bestMoves[9] = { 0 }index = 0int bestValue = -INFINITYif (depth == 0 and isFull())then return eval()elsefor pos:0 to 8if(board[pos] == NULL)then board[pos] = MAXint value = minSearch(depth-1)if(value>bestValue)then bestValue = valueint index = 0bestMoves[index] = poselse if (value == bestValue)then bestMoves[++index] = posendifboard[pos] = NULLendifendifreturn bestMoves[index] }上述例子才用的是一維數組表示棋盤,在打印棋盤時每打印3個數據就換行,這樣就保證了棋盤的正常展示,根據實際需要也可以用二維數組,改一改過程即可
還有一個十分重要的算法:阿爾法-貝塔剪枝算法,在搜索過程中,我們會發現,當搜索到一定程度時,下面的結果都不能取得最優解了,繼續搜索下去,就會造成時間的浪費,因此就要把他cut掉,這就誕生了剪枝算法
但由于井字棋的搜索樹并不大,搜索時間也不多,所以是用不到阿爾法-貝塔剪枝算法的。這里就不繼續闡述了。
至此,井字棋AI介紹就完了,你get到了嗎?
總結
- 上一篇: 搜狗输入法不错,附带的进程需要一个个把e
- 下一篇: QtreeWidget添加右键菜单