一、項目簡介
這是一個可以單人游玩的黑白棋小游戲。
采用鼠標左鍵點擊的方式下子。下子之后,處于該點和原本同顏色棋子之間的棋子會轉變顏色。本游戲代碼設置了可以調整難度的AI(改變內部的difficult參數(shù)),可以隨自己的喜好調整。
編譯環(huán)境:visual c++ 6.0
第三方庫:Easyx2017
二、運行截圖
三、源碼解析
首先看游戲的主體部分,也就是其運行邏輯。
void play(void) // 游戲過程 { MOUSEMSG m; int x, y; // 初始化棋子 for(x = 0; x < 8; x++) for(y = 0; y < 8; y++) map[x][y] = 0; map[3][4] = map[4][3] = 'B'; map[3][3] = map[4][4] = 'W'; // 開始游戲 print(); mciSendString("play 音樂\\背景音樂.wma from 0 repeat", NULL, 0, NULL); do { if (Canput('B')) // 如果玩家有下子位置 { while(true) { while(true) { m = GetMouseMsg(); // 獲取鼠標消息 if(m.uMsg == WM_LBUTTONDOWN && m.x - 26 < 37 * 8 && m.y - 26 < 37 * 8) // 如果左鍵點擊 break; } x = (m.y - 26) / 37; y = (m.x - 26) / 37; if(judge(x, y, 'B')) // 如果當前位置有效 { draw(x, y, 'B'); // 下子 mciSendString("play 音樂\\下子.wma from 0", NULL, 0, NULL); print(); putimage(37 * y, 37 * x, &img[3]); // 標識下子點 break; } else continue; } if (quit('W')) // 計算機是否失敗 break; } if (Canput('W')) // 如果計算機有下子位置 { clock_t start; start = clock(); D('W', 1); // 搜索解法 while (clock() - start < CLOCKS_PER_SEC); draw(X, Y, 'W'); print(); mciSendString("play 音樂\\下子.wma from 0", NULL, 0, NULL); putimage(37 * Y, 37 * X, &img[4]); // 標識下子點 if (quit('B')) // 玩家是否失敗 break; } }while (Canput('B') || Canput ('W'));
我們定義了運算用的變量x,y,以及鼠標變量m。MOUSEMSG是Easyx中的結構體,用于保存鼠標消息。
然后初始化棋盤,即在中間的四個位置放上黑白各兩顆棋子。map[x][y]是一個二維字符串組,用”B”和”W”分別表示黑棋和白棋。
之后進入Do-while循環(huán),循環(huán)條件為兩方至少有一方可以下子。
首先玩家(黑)先行動。我們想要達成的目的是,如果我們點擊棋盤的一個點,這里允許下子則下子,不能下子則繼續(xù)檢測。
這個結構采取一個雙層循環(huán)來完成,內層不斷調用GetMouseMsg獲取鼠標信息(GetMouseMsg是Easyx中的函數(shù),其返回值為之前提到過的MOUSEMSG結構,.x和.y分別表示鼠標點擊的橫縱坐標位置),如果點擊,則確定該點的位置,內層循環(huán)結束。判定此次落子是否有效,有效則下子并終止外層循環(huán),無效則返回外層循環(huán)的頭部。
然后白方下子。這里利用一個動態(tài)規(guī)劃函數(shù)算出一個較好的落子點(與難度相關),并在計算開始之前計時。計算結束判定是否經過一秒(CLOCKS_PER_SEC表示一秒鐘內CPU運行的時鐘周期數(shù)),如果不到一秒則延遲到一秒后落子,防止影響人類棋手心態(tài)。然后同樣執(zhí)行落子程序以及音樂播放程序。
在整個Play函數(shù)外部,前方應該還有一個初始化函數(shù)load(),之后還有勝利處理函數(shù)。
之后,我們看其中的每一個函數(shù)應當如何實現(xiàn)。
這是全局當中聲明的函數(shù)。
void load(void); // 加載素材 void print(void); // 畫棋盤 void draw(int, int, char); // 下當前子 int judge(int, int, char); // 判斷當前是否可以落下 bool Canput(char); // 判斷是否有棋可吃 bool quit(char); // 判斷是否有棋存活 bool ask(void); // 彈出對話框 int D(char, int); // 動態(tài)規(guī)劃 void play(void); // 游戲過程
下面的是全局變量。
const int difficult = 6; // 難度 const int move[8][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, -1}, {1, 1}, {-1, 1}}; // 八個方向擴展 char map[8][8]; // 棋盤 IMAGE img[5]; // 保存圖片 int black, white; // 雙方的棋子數(shù) int X, Y; // 白棋的下子點
加載素材并且初始化變量:
void load(void) // 加載素材 { // 加載圖片 loadimage(&img[0], "圖片\\空位.bmp"); loadimage(&img[1], "圖片\\黑子.bmp"); loadimage(&img[2], "圖片\\白子.bmp"); loadimage(&img[3], "圖片\\黑子1.bmp"); loadimage(&img[4], "圖片\\白子1.bmp"); // 加載音樂 mciSendString("open 音樂\\背景音樂.wma", NULL, 0, NULL); mciSendString("open 音樂\\和局.wma", NULL, 0, NULL); mciSendString("open 音樂\\勝利.wma", NULL, 0, NULL); mciSendString("open 音樂\\失敗.wma", NULL, 0, NULL); mciSendString("open 音樂\\下子.wma", NULL, 0, NULL); // 初始化棋盤 initgraph(340, 340); IMAGE qipan; loadimage(&qipan, "圖片\\棋盤.bmp"); putimage(0, 0, &qipan); setorigin(26, 26); SetWindowText(GetHWnd(), "黑白棋AI版"); }
loadimage是Easyx庫中的函數(shù),用于加載圖像。本案例中第一個參數(shù)是保存圖像的 IMAGE 對象指針,第二個是圖像地址。這個函數(shù)還可以拉伸圖片,或者自動適應IMAGE的大小,具體用法參見Easyx的官方文檔。
mciSendString是<mmsystem.h>當中的函數(shù),用來播放多媒體文件的API指令。
initgraph這個函數(shù)用于初始化繪圖窗口。
putimage用于在當前設備上繪制指定圖像。本案例當中代表在(0,0)處繪制棋盤圖形。
setorigin也在Easyx當中,用于設置坐標原點。
SetWindowText是Windows API宏,聲明在WinUser.h當中,用于設定窗口文本
繪制棋盤:
void print(void) // 畫棋盤 { int x, y; black = white = 0; for(x = 0; x < 8; x++) for(y = 0; y < 8; y++) switch(map[x][y]) { case 0: putimage(37 * y, 37 * x, &img[0]); break; case 'B': putimage(37 * y, 37 * x, &img[1]); black++; break; case 'W': putimage(37 * y, 37 * x, &img[2]); white++; break; } }
利用雙層循環(huán)遍歷棋盤,根據(jù)map[x][y]中的字符,在對應位置繪制棋子就可以了。
落子:
void draw(int x, int y, char a) // 下當前子 { char b = T(a); // 敵方子 int i, x1, y1, x2, y2; bool sign; for (i = 0; i < 8; i++) { sign = false; x1 = x + move[i][0]; y1 = y + move[i][1]; while (0 <= x1 && x1 < 8 && 0 <= y1 && y1 < 8 && map[x1][y1]) { if(map[x1][y1] == b) sign = true; else { if(sign) { x1 -= move[i][0]; y1 -= move[i][1]; x2 = x + move[i][0]; y2 = y + move[i][1]; while (((x <= x2 && x2 <= x1) || (x1 <= x2 && x2 <= x)) && ((y <= y2 && y2 <= y1) || (y1 <= y2 && y2 <= y))) { map[x2][y2] = a; x2 += move[i][0]; y2 += move[i][1]; } } break; } x1 += move[i][0]; y1 += move[i][1]; } } map[x][y] = a; }
對落子點的八個方向進行檢測。如果在該方向上,遇到的第一個棋子與落子顏色不同,并且沿著這條線下去,最后能找到一個和落子顏色相同的棋子,則將這之間的所有棋子改變顏色。
判斷當前位置是否可以落子:
int judge(int x, int y, char a) // 判斷當前是否可以落下,同draw函數(shù) { if(map[x][y]) // 如果當前不是空的返回0值 return 0; char b = T(a); int i, x1, y1; int n = 0, sign; for (i = 0; i < 8; i++) { sign = 0; x1 = x + move[i][0]; y1 = y + move[i][1]; while (0 <= x1 && x1 < 8 && 0 <= y1 && y1 < 8 && map[x1][y1]) { if(map[x1][y1] == b) sign++; else { n += sign; break; } x1 += move[i][0]; y1 += move[i][1]; } } return n; // 返回可吃棋數(shù) }
和上一個函數(shù)差不多的邏輯。
判斷是否有棋可吃:
bool Canput(char c) { int x, y; for(x = 0; x < 8; x++) for(y = 0; y < 8; y++) if(judge(x, y, c)) return true; return false; }
遍歷棋盤,在每個地方都調用judge函數(shù)即可。
判斷是否有棋存活
bool quit(char c) { int x, y; bool b = false, w = false; for(x = 0; x < 8; x++) for(y = 0; y < 8; y++) { if(map[x][y] == c) return false; } return true; }
同樣是簡單的遍歷。判斷map當中的字符是否全與參數(shù)相同即可。
bool ask(void) // 彈出對話框 { HWND wnd = GetHWnd(); int key; char str[50]; ostrstream strout(str, 50); strout <<"黑:" <<black <<" 白:" <<white <<endl; if (black == white) strout <<"世界和平"; else if(black > white) strout <<"恭喜你贏了!"; else strout <<"小樣,還想贏我。"; strout <<"\n再來一局嗎?" <<ends; if(black == white) key = MessageBox(wnd, str, "和局", MB_YESNO | MB_ICONQUESTION); else if(black > white) key = MessageBox(wnd, str, "黑勝", MB_YESNO | MB_ICONQUESTION); else key = MessageBox(wnd, str, "白勝", MB_YESNO | MB_ICONQUESTION); if(key == IDYES) return true; else return false; }
GetHWnd在Easyx中定義,用于返回繪圖窗口句柄。在 Windows 下,句柄是一個窗口的標識,得到句柄后,可以使用 Windows API 中的函數(shù)實現(xiàn)對窗口的控制。
ostrstream strout(str,50);,作用是建立輸出字符串流對象strout,并使strout與字符數(shù)組str關聯(lián)(通過字符串流將數(shù)據(jù)輸出到字符數(shù)組str),流緩沖區(qū)大小為50。
MessageBox()函數(shù)包含在頭文件 windows.h中,它的功能是彈出一個標準的Windows對話框。返回值是一個int型的整數(shù),用于判斷用戶點擊了對話框中的哪一個按鈕。
AI決定落子位置:
int D(char c, int step) { // 判斷是否結束遞歸 if (step > difficult) // 約束步數(shù)之內 return 0; if (!Canput(c)) { if (Canput(T(c))) return -D(T(c), step); else return 0; } int i, j, max = 0, temp, x, y; bool ans = false; // 建立臨時數(shù)組 char **t = new char *[8]; for (i = 0; i < 8; i++) t[i] = new char [8]; for (i = 0; i < 8; i++) for (j = 0; j < 8; j++) t[i][j] = map[i][j]; // 搜索解法 for (i = 0; i < 8; i++) for (j = 0; j < 8; j++) if (temp = judge(i, j, c)) { draw(i, j, c); temp -= D(T(c), step + 1); if (temp > max || !ans) { max = temp; x = i; y = j; ans = true; } for (int k = 0; k < 8; k++) for (int l = 0; l < 8; l++) map[k][l] = t[k][l]; } // 撤銷空間 for (i = 0; i < 8; i++) delete [] t[i]; delete [] t; // 如果是第一步則標識白棋下子點 if (step == 1) { X = x; Y = y; } return max; // 返回最優(yōu)解 }
先看函數(shù)的輸入。Char c代表落子方,step是當前函數(shù)遞歸了幾次。要保證這個遞歸次數(shù)不大于difficult的值,以此來決定AI的強度。
#define T(c) ((c == 'B') ? 'W' : 'B'),意思是c為黑或者白,T(c)為對立的白或者黑。這個函數(shù)的目的是,也讓AI考慮黑方應該如何下棋,以此來一步一步地推斷出最佳位置。
搜索解法采取遍歷的方法。如果某一點可下,則下該點(不是直接打印在棋盤上),將數(shù)據(jù)存儲到臨時變量當中。遞歸調用指定次數(shù),在這個過程中累計翻轉的棋子,如果值超過了之前的最大值,則將落子點更新為該位置。當遍歷完成之后,AI就能得出每一點在預計步數(shù)之內的最大收益,從而得出最佳落點。
四、完整源碼
C語言網提供由在職研發(fā)工程師或ACM藍橋杯競賽優(yōu)秀選手錄制的視頻教程,并配有習題和答疑,點擊了解:
一點編程也不會寫的:零基礎C語言學練課程
解決困擾你多年的C語言疑難雜癥特性的C語言進階課程
從零到寫出一個爬蟲的Python編程課程
只會語法寫不出代碼?手把手帶你寫100個編程真題的編程百練課程
信息學奧賽或C++選手的 必學C++課程
藍橋杯ACM、信息學奧賽的必學課程:算法競賽課入門課程
手把手講解近五年真題的藍橋杯輔導課程