用Python寫一個中國象棋AI?

我本人是大二學生,對編程比較感興趣,現在正在學習python語言,如今正在做中國象棋的項目,我已經做出了能下棋的一步,現在在對人機對戰方面很感興趣,不知道從哪裡入手,有沒有大神對中國象棋AI有簡單的認識嗎?介紹點資料或者是代碼給我參考一下。多謝!


我只講一下棋類AI一般的實現方法(或者叫傳統的實現方法):

我在這裡貼了一個實現棋類軟體AI的簡單代碼(C語言):

本人原創四人象棋,大家有什麼補充的嗎? - 知乎用戶的回答 - 知乎

這是一個我自己設計的象棋,中國象棋的迷你版——小象棋。

該代碼包含了實現棋類軟體AI的基本思想,你把它改為中國象棋的規則就可以了。

寫一個象棋類AI主要包含以下四步:

第一步:局面表示 -- 構成可下棋的基本元素
第二步:走法生成 -- 實現下棋的規則
第三步:局面評估 -- 量化一個局面的雙方優劣勢
第四步:局面搜索 -- 讓電腦具備思考的能力

我以家鄉曾經流行一時的民間二人對弈遊戲《六子沖棋》為例,來說明如何寫一個人機對弈的AI。

六子沖棋的規則請參考百度百科:六子沖_百度百科

以下通過這個例子,具體來說明如何通過上述四步完整的實現一個棋類AI。

這是原創的完整代碼,注釋以教程的形式呈現,已經很詳細。

(JavaScript版詳見GitHub:qq20739111/OldSix)

(以下是C語言版)

/*************************************************************
* = 民間六子棋(六子沖)人機博弈引擎實現與教程 =
*
* www.leilei.name
*
* by LeiLei 2010.3.2 - 2010.3.5
*
*
* 本教程主要講解六子沖棋的博弈引擎實現,不講解界面實現部分。
* 本教程共分四節講解:
*
* 第一節:局面表示 -- 構成可下棋的基本元素
* 第二節:走法生成 -- 實現下棋的規則
* 第三節:局面評估 -- 量化一個局面的雙方優劣勢
* 第四節:局面搜索 -- 讓電腦具備思考的能力
*
* 本教程主要以便於理解為目標,不追求代碼技巧,希望對寫代碼實踐
* 較少的你,會有所幫助。
*/

/*************************************************************
* = 附:六子沖介紹 =
*
* 六子沖是流傳於中國民間的一類棋類遊戲。由於這個遊戲對環境的
* 要求不高,孩子們大都是在光滑的地面或石板上畫上方格,以石子或木
* 棍、草節等為棋子,並有簡單的比賽規則:
*
* 縱橫各四條直線組成一個正方形棋盤,直線相交的地方為落子點。
* 開局時放子處為上下左右邊線上的落子點,且不同方的子不可交叉放置。
* 遊戲雙方著二色棋子各6個在一 個「九宮」型棋盤上進行對抗因為遊戲雙
* 方各著6個棋子,故名「六子沖」。 棋子只能停留在棋盤上的落子點,棋
* 子只能在線上移動,棋子只能移動一步(即相鄰落子點),每回合只能移
* 動1個棋子。消滅對方棋子的方法只有一條,也很簡單。那就是:二子打
* 一子。即在棋盤上攻擊方的2個棋子(2子必須相連並主動移動其中的1個)
* 與被攻方的1個棋子皆處在一條直線上並相鄰時,被攻方的這個棋子就被
* 消滅。重複上面的步驟,保護自己的棋子並消滅對方的棋子,直到最後
* 勝利。
*
* 開始:雙方棋子數均為六顆,分列棋盤四周,見圖片「六子沖開始時」。
*
* 吃子:行棋一方若將兩顆棋子移至一排,且一頭挨著對方的一顆棋時,
* 則可吃子,見圖片「吃子」。
*
* 注意:
*   1.行棋一方若將三顆棋子移至一排,不可吃子,見圖1。
*   2.行棋一方若將兩顆棋子移至一排,但一頭挨著對方的兩顆棋,不可
* 子吃,見圖2。
*   3.行棋一方若將兩顆棋子移至一排,但兩頭分別挨著對方的一顆棋,
* 不可吃子,見圖3。
*   4.行棋一方若將兩顆棋子移至一排,且一頭挨著對方的一顆棋時,但
* 對方的該顆棋後有我方棋,不可吃子見圖4。
*
* 流傳:
*    有好多民間代代相承的傳統兒戲,在六七十年代仍十分盛行,80年
* 代後逐漸衰落。80年代以後,由於社會生活和居住環境的變化,孩子們
* 聚在一起玩耍的機會較少,又有新興的各類現代化的高檔玩具流行,這
* 樣的遊戲則逐漸鮮為人知了。
*    六子沖就是其中最有代表性的一項遊戲.也是當年的小孩子因陋就
* 簡玩的一種棋類遊戲。據傳,六子沖遊戲源自中國古代戰爭的士兵陣
* 型訓練,後逐漸演變為一種棋類遊戲。六子沖規則簡單,上手容易,但
* 變化無窮,是一種讓人玩起來就欲罷不能的智力對抗遊戲。六子沖遊戲
* 在上世紀主要流行於中國四川一帶。
*    在中國山區農村流傳甚廣,,由於規則簡單,工具可信手拈來,是
* 我國鄉間常見的棋類遊戲。在商洛鎮安,涪城等地農村流行。
* 顧問:姜年檑
* (以上文字摘自百度百科,參見原文請訪問:
* http://baike.baidu.com/view/2472074.htm)
*
*/

#include &

/*************************************************************
* = 第一節 局面表示 =
*
* 1.1 棋子表示
*
* 棋子可以隨便用個數字表示,可以把它定義為常量,
* 但是有時候為了方便運算和轉換,也應該精心挑選用哪些數字表示棋子。
* 這裡演示就隨便選兩個數字。
*
* (1)需要定義用來表示棋子的常量
*
* 如下所示:
*/

#define STONE 3 //定義常量 石頭(白色棋子)
#define LEAF 7 // 樹葉(黑色棋子)
#define WHITE 0 // 白方用0表示
#define BLACK 1 // 黑方用1表示
#define NOPIECE 0 // 無棋子用0

/*
* 1.2 棋盤表示
*
* 我們可以用數組來表示棋盤
* 用4*4的二維數組就可以表示這個遊戲的棋盤結構和棋子在棋盤上的分布了。如下(簡單吧):
* int board[4][4] = { // 對應坐標:
* 3, 3, 3, 3, // (0,0), (1,0), (2,0), (3,0),
* 3, 0, 0, 3, // (0,1), (1,1), (2,1), (3,1),
* 7, 0, 0, 7, // (0,2), (1,2), (2,2), (3,2),
* 7, 7, 7, 7 // (0,3), (1,3), (2,3), (3,3),
* };
*
* 數組下標構成的坐標表示棋子在棋盤上的位置。
* 可以用0表示棋盤上無棋子。
* 我們可以用4*4的數組表示棋盤,但是為了運算方便,這裡我們採用8*8的一維數組來裝棋盤,效果更好。
* 我們可以把棋盤放在數組的中央。
* 然後我們用一個8*8的inBoard數組來標識棋盤在數組中的位置。
* 棋子在棋盤上的位置,我們直接用數組下標表示。
*
* 所以,
* (1)我們需要一個用來表示棋盤的數組
* (2)我們用一個數組來標識棋盤在數組中的位置(用來判斷棋子是否在棋盤上)
* (3)寫幾個函數來轉換坐標。
*
* 如下所示:
*/

//棋盤數組(帶開局棋子位置)
int board[64] = {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 7, 7, 7, 7, 0, 0,
0, 0, 7, 0, 0, 7, 0, 0,
0, 0, 3, 0, 0, 3, 0, 0,
0, 0, 3, 3, 3, 3, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0
};

//用來標記棋子是否在棋盤上的數組
static const int inBoard[64] = {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0
};

//根據棋盤的特徵,我們寫三個可以轉換和取得棋盤坐標的函數//////////////////////////

//由棋盤數組下標獲得棋子X坐標
int getXFromLocation(int location){
return (location 7) - 2;
}

//由棋盤數組下標獲得棋子Y坐標
int getYFromLocation(int location){
return (location &>&> 3) - 2;
}

//由棋子X坐標,Y坐標獲得棋盤數組下標
int getLocationFromXY(int x, int y){
return (x + 2) + (y + 2 &<&< 3); } /* * 1.3 當前走棋方表示 * * (1)我們需要一個變數來表示當前該哪方走棋 * (2)還需要一個用來改變走其方的函數changePlayer() * * 如下所示: */ int currentPlayer = WHITE; //初始化為白方走棋,BLACK表示黑方走棋 void changePlayer(){ currentPlayer = 1 - currentPlayer; //改變走棋方,不是0 就是1 } /* * 1.4 在棋盤上放棋子和拿走棋子 * * 有了棋盤,我們就可以在棋盤上放棋子了 * * (1)所以我們還需要幾個函數用來在棋盤上放棋子和拿走棋子。 * * 如下所示: */ //在棋盤上放一枚棋子的函數 void addPiece(int location){ //根據位置和當前走棋方添加棋子 int piece; piece = currentPlayer * 4 + 3; //由當前走棋方計算當前走棋方棋子 if(inBoard[location]){ board[location] = piece; } } //在棋盤上拿走一枚棋子的函數 void delPiece(int location){ //根據位置刪除棋子 if(inBoard[location]){ board[location] = NOPIECE; //NOPIECE == 0表示無棋子 } } /************************************************************* * = 第二節 走法生成 = * * 2.1 走法表示 * * 走法表示就是用一個變數來表示棋子從棋盤上哪裡走到哪裡。 * 我們可以定義一個結構體來表示一個走法,也可以用一串數字表示。 * 在本程序里,我們用一個int類型來表示一個走法,低位表示起點位 * 置(的棋盤數組下標),高位表示目的位置(的棋盤數組下標)。 * * 由此, * (1)我們需要寫幾個函數來處理走法的起點和終點。 * * 如下: */ //根據走法生成走法起點位置(起點位置的棋盤數組下標) int generateMoveFrom(int move){ return move 255; } //根據走法生成走法目的位置(目的位置的棋盤數組下標) int generateMoveTo(int move){ return move &>&> 8;
}

//由起點位置和目的位置合成走法
int composeMove(int locationFrom, int locationTo){
return locationFrom + locationTo * 256;
}

/*
* 2.2 走法生成
*
* 走法生成就是生成一個局面可以有哪些走法,一般是用一個函數來生成所有可能的走法。
* 生成的這些走法,我們可以保存在一個走法列表裡,方面使用。
* 我們可以用這個走法列表來判斷一步棋是否合法,最重要的是,我們需要用這個走法列表來搜索所有可能的局面。
* 六子沖的每顆棋子都是按上下左右四個方向走,通過對棋盤數組下標的觀察,我們可以用原位置的數組下標分別
* 加上-8、8、-1、1四個數就分別得到往上、下、左、右四個方向走一步的位置的數組下標。所以我們用一個數組
* 存儲這四個數。
*
* (1)定義一個常量MAX_GEN_MOVES來表示最大生成的走法數
* (2)我們需要聲明一個數組用來保存生成的走法列表
* (3)用一個數組來表示棋子可走的方向
* (4)還需要寫一個一次性生成所有走法的函數。
*/

#define MAX_GEN_MOVES 32 //這裡順便定義一個常量來表示走法的最大生成數

static int theMoves[MAX_GEN_MOVES]; //定義一個走法數組用來保存生成的所有走法,一個局面最多走法數不會超過MAX_GEN_MOVES種

static const char movesTable[4] = {-8, 8, -1, 1}; //這個數組用來表示棋子的走子方向

//走法生成函數,產生一個局面的所有走法
int generateAllMoves(int *moves){ //傳遞一個走法列表數組指針,返回生成的所有走法
int i, from, to, genCount;
int myPiece, pieceFrom, pieceTo;

//走法計數器清零
genCount = 0;
//獲取當前走棋方標記
myPiece = currentPlayer * 4 + 3; //根據當前走棋方,確定當前走棋方棋子

//遍歷棋盤找到當前走棋方棋子
for(from = 0; from &< 64; from++){ //取得當前位置的棋子 pieceFrom = board[from]; //1.如果找到的不是本方棋子,繼續找 if(pieceFrom != myPiece){ continue; } //2.如果找到本方棋子,探測四個方向是否可走 for(i = 0; i &< 4; i++){ to = from + movesTable[i]; //目標位置 if(inBoard[to]){ //如果還在棋盤內 pieceTo = board[to]; //獲取此位置棋子 if(pieceTo == NOPIECE){ //如果此位置無棋子,此走法可行 moves[genCount] = composeMove(from, to); //保存走法 genCount++; //計數 } } }//end for(i) }//end for(from) return genCount; //返回生成的走法數 } /* * 2.3 走一步棋 * * 要走一步棋很簡單,我們只需要先把要走的棋子從棋盤的原位置上拿 * 走,然後再把這顆棋子放在棋盤上的目標位置上。我們可以利用前面 * 的addPiece()和delPiece()函數來寫一個走棋函數。 * * 在走一步棋之前,我們最好先判斷這步棋是否符合走棋規則,以免引 * 起以後的混亂,我們可以利用走法生成函數來判斷一步棋是否合法。 * 有的時候為了提高效率也可以單獨寫一個函數判斷走法是否合法。 * 在這裡為了簡單就用直接利用走法生成函數,生成所有走法,如果走 * 法在走法列表裡說明走法合法。 * * 走一步棋後往往會引發吃子,六子沖的吃子需要我們進行檢查,六子 * 沖的吃子不光與所走棋子有關,還與其他棋子的合作有關。 * * 在下一節的局面搜索中,我們還會用到走棋函數,並且在搜索的過程 * 中,我們會在棋盤上走棋,以探測未來棋局的變化和走勢,為了不破 * 壞當前局面,每走一步棋我們就要記錄當前的走法和所吃的子,以便 * 還原,所以在吃子過程中,我們需要記錄吃子的位置。 * * (1)寫一個能在棋盤上走一步棋的函數。 */ /**吃子參考方案 - 檢查序列法** //吃子檢查序列數組 static int eatSequence[2][2][4] = { {{7, 3, 3, 0},{0, 7, 3, 3}}, {{3, 7, 7, 0},{0, 3, 7, 7}} }; //檢查吃子的函數,無返回吃子位置部分 int checkEat(int to){ int i, j, k, step, eat, check; //檢查吃子專用 int pieceSequence[4]; //pieceSequence用來保存棋子序列(檢查吃子時用) // 檢查吃子,從上往下、從下往上、從左往右、從右往左,沿四個方向檢查吃子情況 // 比如:從左往右檢查,只需檢查是否呈「●○○_」或「_●○○」序列(假如白方為當前方) for(i = 0; i &< 4; i++){ //1. 取得焦點位置(焦點就是當前檢查點) check = to; //check變數這時作為焦點位置 //2. 取得步長 step = movesTable[i]; //3. 把焦點位置往反方向移動到棋盤邊上 while(inBoard[check]){ check -= step; //往反方向移動焦點 } //4. 往正方向取得棋子序列,保存在數組pieceSequence中 for(j = 0; j &< 4; j++){ form += step; //往正方向移動焦點 if(inBoard[check]){ pieceSequence[j] = board[check]; } } //5. 把焦點位置再次往反方向移動到棋盤邊上,以便由焦點位置根據吃子序列數組取得吃子位置 while(inBoard[check]){ check -= step; //往反方向移動焦點 } //6. 檢查棋子序列是否與兩個吃子序列匹配,若匹配則吃子 for(k = 0; k &< 2; k++){ //6.1 初始化為有子可吃 eat = 1; //6.2 檢查序列是否完全匹配 for(j = 0; j &< 4; j++){ if(pieceSequence[j] != eatSequence[currentPlayer][k][j]){ eat = 0; } } //6.3 檢查到序列完全匹配則,按位置吃子 if(eat == 1){ delPiece(check + step * (k + 1)); //由焦點取得吃子位置 } } } } */ //能根據走法走一步棋的函數 int makeOneMove(int move, int *eatTable){ //傳遞一個用來保存吃子位置的數組 int i, genCount, isLegalMove, from, to; int step, focus, myPiece, oppPiece, eatCount; //吃子專用 isLegalMove = 1; //初始化走法為不合法 genCount = generateAllMoves(theMoves); //生成所有走法 //在所有走法中查找當前走法是否存在 for(i = 0; i &< genCount; i++){ //如果找到一個走法等於當前走法 if(theMoves[i] == move){ isLegalMove = 0; //所有走法中有此走法,說明走法合法 break; } } //1.首先判斷走法是否合法 if(isLegalMove == 1){ return 0; //返回走棋失敗 } //2.分解走法,取得起點位置和目的位置 from = generateMoveFrom(move); to = generateMoveTo(move); //3.走一步棋 delPiece(from); addPiece(to); //4.檢查吃子 eatCount = 0; //一步有可能同時吃掉兩子 myPiece = currentPlayer * 4 + 3; //由當前走棋方計算當前走棋方棋子 oppPiece = (1 - currentPlayer) * 4 + 3; //由當前走棋方計算對方棋子 //檢查吃子,從上往下、從下往上、從左往右、從右往左,沿四個方向檢查吃子情況 for(i = 0; i &< 4; i++){ step = movesTable[i]; //取得步長 //檢查是否為第一種吃子情況「○○●_」 所走棋子為序列中的第一個白子 focus = to + step; //把焦點移到棋子前方第一個位置 if(inBoard[focus] board[focus] == myPiece){ focus += step; //把焦點移到棋子前方第二個位置 if(inBoard[focus] board[focus] == oppPiece){ focus += step; //把焦點移到棋子前方第三個位置 if(inBoard[focus] board[focus] == NOPIECE){ focus -= step; //把焦點移到吃子位置 delPiece(focus); //吃子 eatTable[eatCount] = focus; //記錄吃子的位置 eatCount++; //計數 } } } //檢查是否為第二種吃子情況「○○●_」 所走棋子為序列中的第二個白子 focus = to - step; //把焦點移到棋子後方位置 if(inBoard[focus] board[focus] == myPiece){ focus = to + step; //把焦點移到棋子前方第一個位置 if(inBoard[focus] board[focus] == oppPiece){ focus += step; //把焦點移到棋子前方第二個位置 if(inBoard[focus] board[focus] == NOPIECE){ focus -= step; //把焦點移到吃子位置 delPiece(focus); //吃子 eatTable[eatCount] = focus; //記錄吃子的位置 eatCount++; //計數 } } } //檢查是否為第三種吃子情況「●○○_」 所走棋子為序列中的第二個白子 focus = to - step; //把焦點移到棋子後方位置 if(inBoard[focus] board[focus] == oppPiece){ focus = to + step; //把焦點移到棋子前方第一個位置 if(inBoard[focus] board[focus] == myPiece){ focus += step; //把焦點移到棋子前方第二個位置 if(inBoard[focus] board[focus] == NOPIECE){ focus = to - step; //把焦點移到吃子位置 delPiece(focus); //吃子 eatTable[eatCount] = focus; //記錄吃子的位置 eatCount++; //計數 } } } //檢查是否為第四種吃子情況「_○○●」 所走棋子為序列中的第二個白子 focus = to - step; //把焦點移到棋子後方位置 if(inBoard[focus] board[focus] == NOPIECE){ focus = to + step; //把焦點移到棋子前方第一個位置 if(inBoard[focus] board[focus] == myPiece){ focus += step; //把焦點移到棋子前方第二個位置 if(inBoard[focus] board[focus] == oppPiece){ focus = focus; //焦點已經在吃子位置 delPiece(focus); //吃子 eatTable[eatCount] = focus; //記錄吃子的位置 eatCount++; //計數 } } } } //5.交換走棋方 changePlayer(); return 1 + eatCount; //返回吃子的個數加1,我們調用此函數時,只需要用返回值減1就得到吃子個數 } /************************************************************* * = 第三節 局面評估 = * * 3.1 判斷勝負 * * 有了能生成局面上所有能走的棋的函數,程序就能知道哪些棋可以走,哪些棋不能走。 * 現在為了完善規則,我們還差一個判斷勝負的函數了。已知一個局面,是否已經分出 * 勝負呢?看下面函數。 * * (1)需要寫一個判斷勝負的函數。 */ //判斷是否分出勝負的函數 int isCurrentPlayerDie(){ //如果生成零個走法,則已被困死,將返回1 if(generateAllMoves(theMoves)){ return 0; } return 1; } /* * 3.2 局面評估 * * 局面評估就是量化一個局面的對雙方的優劣勢,比如一個局面對白方有 * 利,那麼到底有利多少呢?我們需要給這個局面打一個分。 * * 一個局面的優劣,與很多因素有關:雙方的棋子,棋子的數目,棋子的 * 位置,棋子的靈活型,棋子之前的牽制等。 * * 為了簡單,本程序只對局面進行最簡單的評估,我們設一顆棋子的價值 * 為3,那麼白方棋子的總價值就是3*白方棋子的剩餘數。同樣黑方的總 * 價值就是3*黑方棋子的剩餘數。 * * 我們可以用 白方棋子的總價值 減去 黑方棋子的總價值 表示一個局面 * 的價值(評分),那麼對白方來說分數越高越好,對黑方來說,分數越低 * 越好。對白方來說最好的分數是36,對黑方來說最好的分數是-36,分 * 數越大,對白方越有利,分數越小對黑方越有利,所以,白方走棋的原 * 則是使局面的分數變大,而黑方走棋的原則是使局面的分數變小。 * * (1)我們需要一個評估函數來計算一個局面的評分,如下: */ //局面評估函數 int evaluatePosition(){ int i, whiteValue, blackValue, value; //初始化雙方的總價值 whiteValue = blackValue = 0; //遍歷棋盤,找到棋子 for(i = 0; i &< 64; i++){ if(board[i] == STONE){ whiteValue += 3; } if(board[i] == LEAF){ blackValue += 3; } } //計算局麵價值 value = whiteValue - blackValue; return value; } /************************************************************* * = 第四節 局面搜索 = * * 4.1 搜索函數 -- 極小值極大值演算法 * * 極小值極大值演算法會動態的產生一顆博弈樹,這顆樹的每一個節點就是一個局面, * 它返回最有利於當前走棋方的走法和搜索到的最佳局面的評分。極小值極大值算 * 法是很多其他博弈搜索演算法的基礎。 * * 極小值極大值演算法的設計思想是輪到雙方走棋時,都去尋找最有利與自己的走法 * 並假設對方也會走出一步好棋。 * * 總之,極小值極大值演算法的作用就是從龐大的博弈樹中儘可能的找到最有利於當 * 前走棋方的走法。 * * 在局面搜索的過程中搜索函數會嘗試在棋盤上走棋,以取得未來幾步的局面。 * 為了不破壞當前局面,我們還需要一個撤銷走一步棋的函數,用來還原局面。 * * (1)我們需要一個變數來保存搜索函數找到的最佳走法 (2)需要一個全局變數來跟蹤搜索的當前深度。 * (3)需要寫一個撤銷走法的函數 * (4)一個極小值極大值搜索函數 */ #define SEARCH_DEPTH 7 //設置搜索深度 #define INFINITY_VALUE 100 //設置局麵價值無窮大為100,因為局麵價值最大為36, //100不可能達到,所以就相當於無窮大。 //最佳走法 int bestMove; //搜索深度 int theDepth; //撤銷一步棋的函數 void undoOneMove(int move, int eatCount, int *eatTable){ int i, from, to; from = generateMoveFrom(move); to = generateMoveTo(move); //還原被吃的子 for(i = 0; i &< eatCount; i++){ addPiece(eatTable[i]); //添加對方棋子(注意,這時走棋方為對方,添加對方棋子) } changePlayer(); //交換走棋方 delPiece(to); //刪除棋子 addPiece(from); //添加己方棋子 } //極小值極大值搜索函數 int MinMaxSearch(int depth){ //depth是搜索博弈樹的深度,就相當於能看到將來的多少步棋。 //因為隨著深度的增加,博弈樹的節點樹呈指數級增長,數據相 //當龐大,所以深度不易太深,一般7層以上PC機算起來就很吃力了。 int i, genCount, value, bestValue; int allMoves[MAX_GEN_MOVES]; int eatCount, eatTable[2]; //還原局面時用 //如果搜索到指定深度,則返回局面評估值 if(depth == 0){ return evaluatePosition(); } //初始化最佳值 if(currentPlayer){ bestValue = INFINITY_VALUE; //正無窮 }else{ bestValue = -INFINITY_VALUE; //負無窮 } genCount = generateAllMoves(allMoves); for(i = 0; i &< genCount; i++){ eatCount = makeOneMove(allMoves[i], eatTable); //走棋 if(eatCount){ //如果走棋成功 theDepth++; value = MinMaxSearch(depth - 1); //遞歸 undoOneMove(allMoves[i], eatCount - 1, eatTable); //還原 theDepth--; if(currentPlayer){ //如果當前走棋方是黑方,找一個評分最小的局面(對黑方最有利) if(value &< bestValue){ bestValue = value; if(depth == SEARCH_DEPTH){ //如果是根節點 保存最佳走法 bestMove = allMoves[i]; } } }else{ //如果當前走棋方是白方,找一個評分最大的局面(對白方最有利) if(value &> bestValue){
bestValue = value;
if(depth == SEARCH_DEPTH){ //如果是根節點 保存最佳走法
bestMove = allMoves[i];
}
}
}
}
}

//如果是殺棋,就根據距殺棋的步數給出評價
if(currentPlayer){ //如果是黑方
if(bestValue == INFINITY_VALUE){
return INFINITY_VALUE - theDepth;
}
}else{ //是白方
if(bestValue == -INFINITY_VALUE){
return theDepth - INFINITY_VALUE;
}
}

return bestValue; //返回找到的最佳局面評分
}

/*
* 4.2 讓電腦走棋
*
* 有了上面的設施,就可以讓電腦走棋了,寫個函數,很簡單。
*
* (1)讓電腦走棋的函數
*/

//讓電腦走棋
int computerThink(){
int eatCount, eatTable[2];
theDepth = 0; //距根結點的距離
MinMaxSearch(SEARCH_DEPTH);
eatCount = makeOneMove(bestMove, eatTable);
return eatCount - 1; //返回吃子個數
}

////////////////////////////////////////////////////////////////////////////////
// 教程完 //
////////////////////////////////////////////////////////////////////////////////

//定義個變數用來設置電腦(引擎)是白方或者是黑方
int engine = BLACK;

//寫個翻轉棋盤的函數
int flipLocation(int location){
location = 63 - location;
return location;
}

//寫個簡單的界面吧
void showBoard(){
int i, j, piece;
int location;

printf("
");
printf(" y ********
");
for(i = 0; i &< 4; i++){ printf(" %d ", i); for(j = 0; j &< 4; j++){ location = getLocationFromXY(j, i); //取得數組下標 if(engine == WHITE){ //如果引擎持白方,翻轉棋盤位置 location = flipLocation(location); } piece = board[location]; if(piece == STONE){ printf("●"); //白子(在CMD中此符號顏色是反的) }else if(piece == LEAF){ printf("○"); //黑子(在CMD中此符號顏色是反的) }else if(piece == NOPIECE){ printf("╋"); }else{ printf(" "); } } printf(" "); } printf(" ******** "); printf(" 0 1 2 3 x "); } //////////////////////////////////////////////////////////////////////////////// // 下面是演示程序 // //////////////////////////////////////////////////////////////////////////////// int main(){ int move, from, to, fromX, fromY, toX, toY, eatTable[2]; char command; /* *********測試用代碼********* int i, genCount, value, eatCount; genCount = generateAllMoves(theMoves); //生成開局所有走法 printf("開局時,白方有%d種走法,分別是: ", genCount); for(i = 0; i &< genCount;i++){ printf("from %d to %d ", generateMoveFrom(theMoves[i]), generateMoveTo(theMoves[i])); } printf("開局時,局面的價值是:%d ", evaluatePosition()); eatCount = makeOneMove(theMoves[0], eatTable); printf("走棋後吃了%d顆子 ", eatCount - 1); undoOneMove(theMoves[0], eatCount - 1, eatTable); value = MinMaxSearch(SEARCH_DEPTH); printf("開局搜索局面,找到的最佳局面評分:%d ", value); printf("開局搜索局面,找到的最佳走法是:from %d to %d ", generateMoveFrom(bestMove), generateMoveTo(bestMove)); */ printf("///////////////////////////// "); printf("// 六子沖棋1.0 // "); printf("///////////////////////////// "); printf(" = 六子沖介紹 = "); printf(" 六子沖是流傳於中國民間的一類棋類遊戲。由於這個遊戲對環境的 "); printf("要求不高,孩子們大都是在光滑的地面或石板上畫上方格,以石子或木 "); printf("棍、草節等為棋子,並有簡單的比賽規則: "); printf(" 縱橫各四條直線組成一個正方形棋盤,直線相交的地方為落子點。 "); printf("開局時放子處為上下左右邊線上的落子點,且不同方的子不可交叉放置。 "); printf("遊戲雙方著二色棋子各6個在一 個「九宮」型棋盤上進行對抗因為遊戲雙 "); printf("方各著6個棋子,故名「六子沖」。棋子只能停留在棋盤上的落子點,棋 "); printf("子只能在線上移動,棋子只能移動一步(即相鄰落子點),每回合只能移 "); printf("動1個棋子。消滅對方棋子的方法只有一條,也很簡單。那就是:二子打 "); printf("一子。即在棋盤上攻擊方的2個棋子(2子必須相連並主動移動其中的1個) "); printf("與被攻方的1個棋子皆處在一條直線上並相鄰時,被攻方的這個棋子就被 "); printf("消滅。重複上面的步驟,保護自己的棋子並消滅對方的棋子,直到最後 "); printf("勝利。 "); printf(" "); printf("開始:雙方棋子數均為六顆,分列棋盤四周,見圖片「六子沖開始時」。 "); printf(" "); printf("吃子:行棋一方若將兩顆棋子移至一排,且一頭挨著對方的一顆棋時, "); printf(" 則可吃子,見圖片「吃子」。 "); printf(" "); printf("注意: "); printf("  1.行棋一方若將三顆棋子移至一排,不可吃子,見圖1。 "); printf("  2.行棋一方若將兩顆棋子移至一排,但一頭挨著對方的兩顆棋,不可 "); printf(" 子吃,見圖2。 "); printf("  3.行棋一方若將兩顆棋子移至一排,但兩頭分別挨著對方的一顆棋, "); printf(" 不可吃子,見圖3。 "); printf("  4.行棋一方若將兩顆棋子移至一排,且一頭挨著對方的一顆棋時,但 "); printf(" 對方的該顆棋後有我方棋,不可吃子見圖4。 "); printf(" "); printf("流傳: "); printf("   有好多民間代代相承的傳統兒戲,在六七十年代仍十分盛行,80年 "); printf(" 代後逐漸衰落。80年代以後,由於社會生活和居住環境的變化,孩子們 "); printf(" 聚在一起玩耍的機會較少,又有新興的各類現代化的高檔玩具流行,這 "); printf(" 樣的遊戲則逐漸鮮為人知了。 "); printf("   六子沖就是其中最有代表性的一項遊戲.也是當年的小孩子因陋就 "); printf(" 簡玩的一種棋類遊戲。據傳,六子沖遊戲源自中國古代戰爭的士兵陣 "); printf(" 型訓練,後逐漸演變為一種棋類遊戲。六子沖規則簡單,上手容易,但 "); printf(" 變化無窮,是一種讓人玩起來就欲罷不能的智力對抗遊戲。六子沖遊戲 "); printf(" 在上世紀主要流行於中國四川一帶。 "); printf("   在中國山區農村流傳甚廣,,由於規則簡單,工具可信手拈來,是 "); printf(" 我國鄉間常見的棋類遊戲。在商洛鎮安,涪城等地農村流行。 "); printf(" 顧問:姜年檑 "); printf(" (以上文字摘自百度百科,參見原文請訪問: "); printf(" http://baike.baidu.com/view/2472074.htm) "); printf(" 開始: "); showBoard(); while(1){ printf(" 輸入Q退出,輸入D下棋:"); command = getch(); if(command == "q" || command == "Q"){ return 0; } if(command == "d" || command == "D"){ //選擇了下棋 printf(" 輸入S選擇石頭(白棋),輸入L選擇樹葉(黑棋):"); command = getch(); if(command == "s" || command == "S"){ //人選擇了白棋 engine = BLACK; //設置引擎為黑棋 printf(" 您選擇了石頭(白棋)! "); }else if(command == "l" || command == "L"){ //人選擇了黑棋 engine = WHITE; //設置引擎為白棋 printf(" 您選擇了樹葉(黑棋)! "); }else{ continue; } while(1){ if(isCurrentPlayerDie() currentPlayer != engine){ printf(" 不好意思,你已戰敗!再來一局請重新打開程序! "); printf(" 輸入q退出:"); command = getch(); if(command == "q" || command == "Q"){ return 0; } }else if(isCurrentPlayerDie() currentPlayer == engine){ printf(" 恭喜你,電腦被你打敗!再來一局請重新打開程序! "); printf(" 輸入q退出:"); command = getch(); if(command == "q" || command == "Q"){ return 1; } }else{ if(engine == WHITE){ //引擎為白棋 showBoard(); if(1){ //如果電腦持白方則先走 printf(" 電腦正在思考... "); computerThink(); move = bestMove; from = generateMoveFrom(move); //分解走法 to = generateMoveTo(move); from = flipLocation(from); //翻轉位置 to = flipLocation(to); fromX = getXFromLocation(from); //取得坐標 fromY = getYFromLocation(from); toX = getXFromLocation(to); toY = getYFromLocation(to); printf(" 電腦所走的棋是:from (%d,%d) to (%d,%d) ", fromX, fromY, toX, toY); showBoard(); } if(isCurrentPlayerDie()){ //如果玩家已敗 continue; } if(1){ //輪到人走棋 printf(" 輪到您下棋,請輸入坐標 x,y x,y :"); scanf("%d,%d %d,%d", fromX, fromY, toX, toY); printf(" 您走的棋是:from (%d,%d) to (%d,%d) ", fromX, fromY, toX, toY); from = getLocationFromXY(fromX, fromY); //合成位置 to = getLocationFromXY(toX, toY); from = flipLocation(from); //翻轉位置 to = flipLocation(to); move = composeMove(from, to); //合成走法 while(!makeOneMove(move, eatTable)){ printf(" 輸入坐標錯誤,請重新輸入坐標:"); scanf("%d,%d %d,%d", fromX, fromY, toX, toY); from = getLocationFromXY(fromX, fromY);//合成位置 to = getLocationFromXY(toX, toY); from = flipLocation(from); //翻轉位置 to = flipLocation(to); move = composeMove(from, to); //合成走法 getchar(); } } } if(engine == BLACK){ //引擎為黑方 showBoard(); if(1){ //輪到人走棋 printf(" 輪到您下棋,請輸入坐標 x,y x,y :"); scanf("%d,%d %d,%d", fromX, fromY, toX, toY); printf(" 您走的棋是:from (%d,%d) to (%d,%d) ", fromX, fromY, toX, toY); from = getLocationFromXY(fromX, fromY);//合成位置 to = getLocationFromXY(toX, toY); move = composeMove(from, to); //合成走法 while(!makeOneMove(move, eatTable)){ printf(" 輸入坐標錯誤,請重新輸入坐標:"); scanf("%d,%d %d,%d", fromX, fromY, toX, toY); from = getLocationFromXY(fromX, fromY);//合成位置 to = getLocationFromXY(toX, toY); move = composeMove(from, to); //合成走法 getchar(); } showBoard(); } if(isCurrentPlayerDie()){ //如果電腦已敗 continue; } if(1){ //如果電腦黑方則後走 printf(" 電腦正在思考... "); computerThink(); move = bestMove; from = generateMoveFrom(move); //分解走法 to = generateMoveTo(move); fromX = getXFromLocation(from); //取得坐標 fromY = getYFromLocation(from); toX = getXFromLocation(to); toY = getYFromLocation(to); printf(" 電腦所走的棋是:from (%d,%d) to (%d,%d) ", fromX, fromY, toX, toY); } } } } } } return 0; }

運行效果:

以上說的都不是中國象棋,
以上說的都不是Python語言,

但我相信看懂了這兩個例子已經完全能夠搞定中國象棋的AI了!

授人以魚不如授人以漁。

至於中國象棋AI如何用Python封裝實現,Python會告訴你。

鬼笑聲 ~ ~ 消失在你CPU的電流聲中…

……


最近正在寫,工作量比較大,調試中,有興趣可以和我交流,知乎內消息,然後可加QQ聊。

昨天的心得是AlphaBeta搜索中,要檢查自殺的局面直接截斷,有兩種寫法:

第一種是生成對手走法,看是否殺自己的帥;好處是代碼不用改,壞處是從單步看,運行效率不高。因為GenMoveList在這一步出現了兩次,分別產生了自己的著法N和N*M個對手的著法。

第二種是假設自己的帥是車、炮、馬、兵,看看能不能吃到對方的車、炮、馬、兵;

好處是從單步看效率高一些,要寫一坨。

象棋巫師 - 象棋百科全書 傾向於第二種。

第一種寫法,從單步看,運行效率不高,但是,稍微改一下AlphaBeta,就變成了可下步使用!~

既然生成了黑棋走法,GenMoveList

那麼要遞歸到下一次AlphaBeta,就不需要再生成,

所以,在AlphaBeta參數中,把這個結果帶到下一次的參數中,反而會加速搜索!~

所以,我傾向於第一種

現在糾結的是局面評估和走法排序的時候,

要不要加上靈活度的問題?

因為多數情況下,即沒吃子,也沒將軍

那麼僅僅從子力和靜態位置上看

局面沒啥優劣,分數相同。

MoveList的大小可以代表局面棋子的靈活度Flex,簡稱F

在AB搜索中,我產生了這一代紅棋的 F ,又產生了下一代黑棋的F

我感覺,這兩個值其實在排序和評估價值時候有用!~

還沒考慮清楚

有興趣的同學可以知乎內消息,然後可加QQ聊。


中國象棋的AI我沒有實現過,不過我寫過一個簡單的tic-tac-toe的AI。(也就是3*3的棋盤,畫x和o,三個點連成一線就贏的那個遊戲)。

原理都是通的,你需要一個評價當前局勢的評價函數(我是用了當前節點下獲勝的概率),和一個搜索演算法(我是用了min max tree和alpha-beta剪枝)。

程序鏈接

https://github.com/zjusbo/tic-tac-toe

最後說一句,其實用哪種語言實現AI並不重要(不考慮性能時),用Python, C, JAVA,甚至是javascript. 只要把演算法搞通就好。

建議搜索 最大最小樹 和 alpha-beta剪枝


象棋還沒做過,不過做過五子棋,相信也可以給你一點思路。首先,ai需要一個估值函數,就是要返回當前局面的分數。例如,估值函數先找到一個空白點,向各個方向掃,有一個白棋或者黑棋該點加分。這樣你就能得到這個棋盤的分數。然後就可以配合各種演算法,例如貪心演算法和極大極小。


我寫過國際象棋的AI。

核心部分是生成並搜索最大最小樹配合Alpha-Beta剪枝。AI選取在有限資源(搜索時間)情況下搜索後評分最高的走法。

我用C++和Java都實現過,性能還是可以的。不過用純Python的話,估計性能要差一到多個數量級。


http://blog.jobbole.com/80007/ 這個思路很清楚了,對於jibem的AI應該夠用了。從國際象棋改到中國象棋其實很簡單。


推薦閱讀:

為什麼 sqlmap 源碼看起來那麼費勁?
營銷人想學python,卻被卡在pycharm官網下載不了這環,求助?
pycharm和eclipse+pydev的對比?
0基礎小白學python,我想打算學習selenium+python 這一塊,該怎麼辦?
做數據分析里有哪些Python能做,而MATLAB不能做的?

TAG:Python | 中國象棋 | pygame |