上一篇我們完成了整個程序的基礎框架,那么在講到真正的搜索算法前,我們先來看看五子棋如何評估當前局勢,以及如何計算某個位置的價值。
一、五子棋
在五子棋中,包括成五,活三,活二等定勢,下圖為山東師范大學董紅安在2005年的碩士畢業論文中使用的的評分表,可以供我們來參考。
但是對於四子棋來說,上述評分卻並不適用,因為棋盤空間大小的原因,任何一個維度只有4子的空間,一旦沒有落成,或是任意一個位置被對方下了,那么該位置將沒有任何價值。
二、潛在可能性評估
我們以這張圖來舉例,當黑棋在(0,3,0)這個位置落子后,我們來分析之后的可能性。
首先在xy平面內,有如下三種方式取勝:
除xy平面內之外,我們還需要考慮立體斜着四子連成的情況:
最后還要考慮垂直四子連成的情況:
並且在計算價值時,我們要注意,在上述任何一種情況中,只要預計的路線上有對方棋子出現,那么這條線(僅僅是單條線,不是整體)的評分將為0,因為他已經不能實現勝利。
三、展平化
為了更方便的計算,我們通過將chessBoard[x][y][z]符合規則的任意連續的四個子編入序列,並通過計數的方式實現計分。
我們通過一段代碼來了解這個過程:
struct flatData{ int a; int b; int c; int d; }; typedef std::vector<flatData> PicesFlatDataList; int isWin(int board[4][4][4]); int ChessBoard::isWin(int board[4][4][4]) { PicesFlatDataList flats; for(int y = 0;y < 4;y++) { for(int z = 0;z < 4;z++) { flatData data; data.a = board[0][y][z]; data.b = board[1][y][z]; data.c = board[2][y][z]; data.d = board[3][y][z]; flats.push_back(data); } } for(int x = 0;x < 4;x++) { for(int z = 0;z < 4;z++) { flatData data; data.a = board[x][0][z]; data.b = board[x][1][z]; data.c = board[x][2][z]; data.d = board[x][3][z]; flats.push_back(data); } } for(int x = 0;x < 4;x++) { for(int y = 0;y < 4;y++) { flatData data; data.a = board[x][y][0]; data.b = board[x][y][1]; data.c = board[x][y][2]; data.d = board[x][y][3]; flats.push_back(data); } } for(int y = 0;y < 4;y++) { flatData data; data.a = board[0][y][0]; data.b = board[1][y][1]; data.c = board[2][y][2]; data.d = board[3][y][3]; flats.push_back(data); } for(int x = 0;x < 4;x++) { flatData data; data.a = board[x][0][0]; data.b = board[x][1][1]; data.c = board[x][2][2]; data.d = board[x][3][3]; flats.push_back(data); } for(int y = 0;y < 4;y++) { flatData data; data.a = board[0][y][3]; data.b = board[1][y][2]; data.c = board[2][y][1]; data.d = board[3][y][0]; flats.push_back(data); } for(int x = 0;x < 4;x++) { flatData data; data.a = board[x][0][3]; data.b = board[x][1][2]; data.c = board[x][2][1]; data.d = board[x][3][0]; flats.push_back(data); } for(int z = 0;z < 4;z++) { flatData data; data.a = board[0][0][z]; data.b = board[1][1][z]; data.c = board[2][2][z]; data.d = board[3][3][z]; flats.push_back(data); } for(int z = 0;z < 4;z++) { flatData data; data.a = board[3][0][z]; data.b = board[2][1][z]; data.c = board[1][2][z]; data.d = board[0][3][z]; flats.push_back(data); } flatData data; data.a = board[0][0][0]; data.b = board[1][1][1]; data.c = board[2][2][2]; data.d = board[3][3][3]; flats.push_back(data); data.a = board[0][0][3]; data.b = board[1][1][2]; data.c = board[2][2][1]; data.d = board[3][3][0]; flats.push_back(data); data.a = board[3][0][0]; data.b = board[2][1][1]; data.c = board[1][2][2]; data.d = board[0][3][3]; flats.push_back(data); data.a = board[3][0][3]; data.b = board[2][1][2]; data.c = board[1][2][1]; data.d = board[0][3][0]; flats.push_back(data);
}
不難看出邏輯簡單粗暴,直接遍歷整個棋盤,並且將所有橫豎斜的可能性加入FlatData的abcd四個位置中,再把該條加入到整個list中,為后續其他功能提供數據。
四、局勢評分及輸贏判斷
在上一步的基礎上,我們要做的是根據每組FlatData(展平后的數據格式)來給出我們評估的分數。
int whiteNum = 0,blackNum = 0; for(auto iter = flats.begin();iter != flats.end();iter++) { whiteNum = 0; blackNum = 0; if(iter->a == chessPicesStatus::black) blackNum++; else if(iter->a == chessPicesStatus::white) whiteNum++; if(iter->b == chessPicesStatus::black) blackNum++; else if(iter->b == chessPicesStatus::white) whiteNum++; if(iter->c == chessPicesStatus::black) blackNum++; else if(iter->c == chessPicesStatus::white) whiteNum++; if(iter->d == chessPicesStatus::black) blackNum++; else if(iter->d == chessPicesStatus::white) whiteNum++; if(whiteNum == 4) return chessPicesStatus::white; if(blackNum == 4) return chessPicesStatus::black; } return chessPicesStatus::empty;
以上為判斷輸贏的代碼,可以看出就是在上一步的基礎上添加了abcd四個位置白棋黑棋的統計,並且判斷是否已經獲勝。對於獲取當前局面分評估的邏輯,其實只是在最后一步統計的時候更加細分一些:
int ChessBoard::getFlatPicesValue(PicesFlatDataList flats, chessPicesStatus status) { int value = 0,whiteNum = 0,blackNum = 0; for(auto iter = flats.begin();iter != flats.end();iter++) { whiteNum = 0; blackNum = 0; if(iter->a == chessPicesStatus::black) blackNum++; else if(iter->a == chessPicesStatus::white) whiteNum++; if(iter->b == chessPicesStatus::black) blackNum++; else if(iter->b == chessPicesStatus::white) whiteNum++; if(iter->c == chessPicesStatus::black) blackNum++; else if(iter->c == chessPicesStatus::white) whiteNum++; if(iter->d == chessPicesStatus::black) blackNum++; else if(iter->d == chessPicesStatus::white) whiteNum++; if(status == chessPicesStatus::white) { //Calculating White Picess if(blackNum != 0) continue; if(whiteNum == 0) value += 1; else if(whiteNum == 1) value += 5; else if(whiteNum == 2) value += 100; else if(whiteNum == 3) value += 5000; else if(whiteNum == 4) { value += 100000; } } else { //Calculating Black Picess if(whiteNum != 0) continue; if(blackNum == 0) value += 1; else if(blackNum == 1) value += 5; else if(blackNum == 2) value += 100; else if(blackNum == 3) value += 5000; else if(blackNum == 4) { value += 100000; } } //cout<<iter->a<<" "<<iter->b<<" "<<iter->c<<" "<<iter->d<<" "<<value<<endl; } return value; }
可以看出我們對於任何一個FlatData,如果該條數據有對方棋子存在,價值即為零,進入下一條。價值從本方棋子數目為零到四分別為1,5,100,5000,10000。
五、單個位置價值評分
為了實現后續的啟發式搜索算法,我們需要計算出每個可下位置的得分,單個位置價值評分大體邏輯與上面兩步基本一樣,是先獲取當前棋盤可落子的位置,接着遍歷與之相連的各種可能性。但由於立體四子棋支持斜着四子連成,所以需要額外注意,此時展開需要分為SimpleFlat與Non-SimpleFlat。具體區別在於:
紅色位置需要計算斜着連成的各種情況,而黑色區域的棋子不需要。所以在遍歷時如果判斷需要Non-SimpleFlat,則需要把更多的(斜着的若干種可能)添加到list中進行計算。
bool ifSimpleFlat(PicesPos pos) { if(pos.x == 1 && pos.y == 0 || pos.x == 2 && pos.y == 0 || pos.x == 0 && pos.y == 1 || pos.x == 3 && pos.y == 1 || pos.x == 0 && pos.y == 2 || pos.x == 3 && pos.y == 2 || pos.x == 1 && pos.y == 3 || pos.x == 2 && pos.y == 3) return true; else return false; } int ChessBoard::getPosValue(int board[4][4][4],PicesPos *pos, chessPicesStatus side) { PicesFlatDataList flatList; // cout<<"x:"<<pos->x<<" y:"<<pos->y<<" z:"<<pos->z<<endl; if(ifSimpleFlat(*pos)) { /* [0] - [1] | [2] /- [3] -\ [4] /_ [5] _\ */ flatData temp0; temp0.a = board[0][pos->y][pos->z]; temp0.b = board[1][pos->y][pos->z]; temp0.c = board[2][pos->y][pos->z]; temp0.d = board[3][pos->y][pos->z]; flatList.push_back(temp0); flatData temp1; temp1.a = board[pos->x][0][pos->z]; temp1.b = board[pos->x][1][pos->z]; temp1.c = board[pos->x][2][pos->z]; temp1.d = board[pos->x][3][pos->z]; flatList.push_back(temp1); flatData temp2; temp2.a = board[0][pos->y][0]; temp2.b = board[1][pos->y][1]; temp2.c = board[2][pos->y][2]; temp2.d = board[3][pos->y][3]; flatList.push_back(temp2); flatData temp3; temp3.a = board[0][pos->y][3]; temp3.b = board[1][pos->y][2]; temp3.c = board[2][pos->y][1]; temp3.d = board[3][pos->y][0]; flatList.push_back(temp3); flatData temp4; temp4.a = board[pos->x][0][0]; temp4.b = board[pos->x][1][1]; temp4.c = board[pos->x][2][2]; temp4.d = board[pos->x][3][3]; flatList.push_back(temp4); flatData temp5; temp5.a = board[pos->x][0][3]; temp5.b = board[pos->x][1][2]; temp5.c = board[pos->x][2][1]; temp5.d = board[pos->x][3][0]; flatList.push_back(temp5); flatData temp6; temp6.a = board[pos->x][pos->y][0]; temp6.b = board[pos->x][pos->y][1]; temp6.c = board[pos->x][pos->y][2]; temp6.d = board[pos->x][pos->y][3]; flatList.push_back(temp6); } else { /* [0] - [1] | [2] /- [3] -\ [4] /_ [5] _\ [6] -|/ [7] -|\ */ flatData temp0; temp0.a = board[0][pos->y][pos->z]; temp0.b = board[1][pos->y][pos->z]; temp0.c = board[2][pos->y][pos->z]; temp0.d = board[3][pos->y][pos->z]; flatList.push_back(temp0); flatData temp1; temp1.a = board[pos->x][0][pos->z]; temp1.b = board[pos->x][1][pos->z]; temp1.c = board[pos->x][2][pos->z]; temp1.d = board[pos->x][3][pos->z]; flatList.push_back(temp1); flatData temp2; temp2.a = board[0][pos->y][0]; temp2.b = board[1][pos->y][1]; temp2.c = board[2][pos->y][2]; temp2.d = board[3][pos->y][3]; flatList.push_back(temp2); flatData temp3; temp3.a = board[0][pos->y][3]; temp3.b = board[1][pos->y][2]; temp3.c = board[2][pos->y][1]; temp3.d = board[3][pos->y][0]; flatList.push_back(temp3); flatData temp4; temp4.a = board[pos->x][0][0]; temp4.b = board[pos->x][1][1]; temp4.c = board[pos->x][2][2]; temp4.d = board[pos->x][3][3]; flatList.push_back(temp4); flatData temp5; temp5.a = board[pos->x][0][3]; temp5.b = board[pos->x][1][2]; temp5.c = board[pos->x][2][1]; temp5.d = board[pos->x][3][0]; flatList.push_back(temp5); flatData temp6; temp6.a = board[pos->x][pos->y][0]; temp6.b = board[pos->x][pos->y][1]; temp6.c = board[pos->x][pos->y][2]; temp6.d = board[pos->x][pos->y][3]; flatList.push_back(temp6); if(pos->x == 0 && pos->y == 0 || pos->x == 1 && pos->y == 1 || pos->x == 2 && pos->y == 2 || pos->x == 3 && pos->y == 3) { flatData temp7; temp7.a = board[0][0][0]; temp7.b = board[1][1][1]; temp7.c = board[2][2][2]; temp7.d = board[3][3][3]; flatList.push_back(temp7); flatData temp8; temp8.a = board[0][0][3]; temp8.b = board[1][1][2]; temp8.c = board[2][2][1]; temp8.d = board[3][3][0]; flatList.push_back(temp8); } else { flatData temp7; temp7.a = board[3][0][0]; temp7.b = board[2][1][1]; temp7.c = board[1][2][2]; temp7.d = board[0][3][3]; flatList.push_back(temp7); flatData temp8; temp8.a = board[3][0][3]; temp8.b = board[2][1][2]; temp8.c = board[1][2][1]; temp8.d = board[0][3][0]; flatList.push_back(temp8); } } int val = getFlatPicesValue(flatList,side); return val; }
至此,我們已經完成了棋盤的評估函數,下一章我們將討論極值搜索算法,也就是我們真正的博弈樹函數。