發(fā):MFC實(shí)現(xiàn)與規(guī)則解析)
簡(jiǎn)介一套在VS2010環(huán)境下使用VCMFC編寫(xiě)的圍棋游戲完整工程面向C入門(mén)者和棋類(lèi)游戲開(kāi)發(fā)愛(ài)好者幫助理解從窗口界面到棋盤(pán)數(shù)據(jù)結(jié)構(gòu)、玩家交互與對(duì)弈規(guī)則的落地過(guò)程。壓縮包共33個(gè)文件主體為10個(gè).h頭文件和8個(gè).cpp源文件此外還有bmp/ico界面資源、rc資源描述、sln/proj工程文件及說(shuō)明文檔整體僅164KB適合快速下載并直接打開(kāi)工程研讀。代碼將棋盤(pán)、棋子、玩家等拆分為獨(dú)立模塊棋盤(pán)類(lèi)負(fù)責(zé)狀態(tài)管理棋子類(lèi)處理落點(diǎn)與顏色玩家類(lèi)接收行列輸入實(shí)現(xiàn)落子合法性判斷與五子連珠勝負(fù)檢測(cè)并給出基于深度優(yōu)先搜索的簡(jiǎn)易AI構(gòu)想壓縮包內(nèi)還附有待解決問(wèn)題清單便于學(xué)習(xí)者沿著作者的思考脈絡(luò)進(jìn)行二次開(kāi)發(fā)或排錯(cuò)。已有98人學(xué)習(xí)特別適合想在VS2010/MFC環(huán)境中快速上手圍棋或同類(lèi)棋類(lèi)項(xiàng)目的人群。1. 拿到「VS2010環(huán)境編寫(xiě)的VC圍棋游戲.rar」先判斷這份代碼值不值得花時(shí)間VS2010環(huán)境編寫(xiě)的VC圍棋游戲這個(gè)壓縮包打開(kāi)后最怕兩種結(jié)局雙擊 .sln 報(bào)版本不兼容或編譯通過(guò)卻一啟動(dòng)就閃退。這類(lèi)項(xiàng)目十有八九是 VC 寫(xiě)的 MFC 對(duì)話框程序棋盤(pán)畫(huà)在自繪控件上落子走消息響應(yīng)代碼量在 1000 到 3000 行之間正好卡在「能讀懂」和「能改成自己的」的分界線上。把它跑通能換到什么一份可斷點(diǎn)調(diào)試的圍棋規(guī)則實(shí)現(xiàn)——?dú)狻⑻嶙?、劫、禁著點(diǎn)都是獨(dú)立函數(shù)一個(gè)用 GDI 做 2D 繪圖的現(xiàn)成范本——畫(huà) 19 路棋盤(pán)、把鼠標(biāo)點(diǎn)擊換算成格點(diǎn)還有一個(gè)能直接替換評(píng)分函數(shù)的 AI 殼。這篇文章寫(xiě)給三類(lèi)人學(xué) VC/MFC 但缺項(xiàng)目練手的學(xué)生有圍棋算法卻卡在繪圖環(huán)節(jié)的開(kāi)發(fā)者以及打開(kāi)別人工程卻編譯不過(guò)、需要快速定位問(wèn)題的從業(yè)者。下面按「認(rèn)骨架 → 寫(xiě)規(guī)則 → 畫(huà)界面 → 排坑 → 加 AI」的路徑拆開(kāi)重建每步都給能直接復(fù)制的代碼和參數(shù)。2. 從 rar 到可編譯工程認(rèn)文件、定數(shù)據(jù)結(jié)構(gòu)、理清消息路由2.1 先認(rèn)文件從 .sln、.vcxproj 和 .rc 判斷項(xiàng)目類(lèi)型解壓后不要急著雙擊任何文件先在命令行里掃一眼頂層結(jié)構(gòu)比任何教程都直觀find . -maxdepth 2 -type f | sort # 或直接列出 rar 內(nèi)的文件清單 unrar l VS2010環(huán)境編寫(xiě)的VC圍棋游戲.rar | head -30看到 .sln 和 .vcxproj 同時(shí)存在說(shuō)明這是 VS2010 原生的解決方案結(jié)構(gòu)。如果只看到 .dsw / .dsp那是更老的 VC6 工程VS2010 也能打開(kāi)但會(huì)做一次格式轉(zhuǎn)換。接著看有沒(méi)有 .rc 資源文件和 resource.h——有這兩樣基本可以鎖定它是 MFC 工程而不是純 Win32 或控制臺(tái)程序。MFC 工程的入口是一個(gè)繼承 CWinApp 的 App 類(lèi)主界面繼承 CDialogEx后面所有消息處理和繪圖邏輯都繞著這兩個(gè)類(lèi)轉(zhuǎn)。如果你還在找 vs2010 的下載安裝教程裝完務(wù)必先打 VS2010 SP1 補(bǔ)丁再開(kāi)工程。SP1 修掉的編譯器 bug 里有幾條正卡在這種老項(xiàng)目編譯路徑上裸裝 VS2010 經(jīng)常會(huì)碰到標(biāo)準(zhǔn)庫(kù)頭文件報(bào)錯(cuò)。打開(kāi)工程后注意左側(cè)的類(lèi)視圖——老資料里常把它叫 VC 管理器——幾個(gè)核心類(lèi)一眼就能看到繼承關(guān)系比逐個(gè)翻文件快得多。2.2 棋盤(pán)數(shù)據(jù)結(jié)構(gòu)帶 padding 的二維數(shù)組是默認(rèn)答案圍棋棋盤(pán) 19×19最省事的表示就是二維數(shù)組。常見(jiàn)做法是開(kāi) 21×21外圈留一圈 padding這是這類(lèi) VC 項(xiàng)目事實(shí)上的標(biāo)準(zhǔn)寫(xiě)法// GameDef.h —— 棋盤(pán)數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一約定 #define BOARD_LEN 19 // 19 路棋盤(pán) #define PAD 1 // 外圈 padding #define MAX_BOARD (BOARD_LEN 2 * PAD) // 21 enum Stone { EMPTY 0, BLACK 1, WHITE 2 }; // 有效格點(diǎn)范圍是 [1][1] ~ [19][19] // board[0][*]、board[20][*]、board[*][0]、board[*][20] 恒為邊界 int board[MAX_BOARD][MAX_BOARD];邏輯說(shuō)明padding 里的值永遠(yuǎn)不是 EMPTY數(shù)氣、提子時(shí)掃描到 padding 就自然停止代碼里一個(gè)越界判斷都不用寫(xiě)。這是把 19 改成 21 的唯一理由一定要在注釋里寫(xiě)清楚否則后來(lái)的人會(huì)把循環(huán)寫(xiě)成從 0 到 18一個(gè)經(jīng)典翻車(chē)點(diǎn)。選型上也可以用一維數(shù)組 int board[361]下標(biāo)按 row * 19 col 換算提子和坐標(biāo)換算會(huì)啰嗦一點(diǎn)用 std::vector 嵌套也常見(jiàn)但 memset 歸零、memcmp 比對(duì)快照都不如 C 數(shù)組干凈。VS2010 對(duì) C0x 支持不全但 STL 老容器完全可用我一般棋盤(pán)用 C 數(shù)組悔棋棧和歷史記錄用 vector各取所長(zhǎng)。2.3 消息路由WM_LBUTTONDOWN 到落子、WM_PAINT 到重繪MFC 程序的消息分發(fā)靠消息映射表我們這個(gè)圍棋游戲只關(guān)心兩條鼠標(biāo)按下負(fù)責(zé)落子WM_PAINT 負(fù)責(zé)畫(huà)棋盤(pán)。映射表的寫(xiě)法如下// GoDlg.cpp —— 消息映射與兩個(gè)核心響應(yīng) BEGIN_MESSAGE_MAP(CGoDlg, CDialogEx) ON_WM_LBUTTONDOWN() ON_WM_PAINT() ON_COMMAND(IDC_BTN_UNDO, CGoDlg::OnBtnUndo) // 悔棋按鈕 END_MESSAGE_MAP() void CGoDlg::OnLButtonDown(UINT nFlags, CPoint point) { // 像素坐標(biāo) - 格點(diǎn)坐標(biāo)MARGIN 是棋盤(pán)邊距CELL_SIZE 是格距 int gx (point.x - MARGIN) / CELL_SIZE 1; int gy (point.y - MARGIN) / CELL_SIZE 1; if (gx 1 gx BOARD_LEN gy 1 gy BOARD_LEN) { if (TryPlaceStone(gy, gx, m_curColor)) // 規(guī)則入口 SwitchTurn(); // 換手 } Invalidate(FALSE); // 只標(biāo)記客戶(hù)區(qū)失效不擦背景 }邏輯說(shuō)明ON_WM_LBUTTONDOWN 宏把鼠標(biāo)消息路由到 OnLButtonDown函數(shù)里先做坐標(biāo)換算再調(diào)規(guī)則入口合法性由 TryPlaceStone 決定合法才換手。Invalidate(FALSE) 觸發(fā)一次異步重繪FALSE 表示不擦背景配合雙緩沖后拉伸窗口也不閃。這里坐標(biāo)參數(shù)順序行、列全工程必須一致混用是后期規(guī)則出現(xiàn)魔幻行為的頭號(hào)嫌疑。參數(shù)說(shuō)明MARGIN 和 CELL_SIZE 是坐標(biāo)換算的兩個(gè)基準(zhǔn)值。按 19 路棋盤(pán)、28 像素格距估算客戶(hù)區(qū)寬度至少 24×2 18×28 552 像素對(duì)話框模板里的寬高用這個(gè)公式算別隨手拉一個(gè) 300 寬的窗口讓棋盤(pán)被裁掉。3. 落子、提子與劫把圍棋規(guī)則完整寫(xiě)進(jìn) 200 行 VC 代碼3.1 數(shù)氣Flood Fill 收集連通塊再掃四鄰空點(diǎn)氣是圍棋規(guī)則的地基。落子合法性、提子、終局判斷全依賴(lài)數(shù)氣函數(shù)這個(gè)函數(shù)寫(xiě)錯(cuò)后面全是空中樓閣。標(biāo)準(zhǔn)做法是 Flood Fill——從目標(biāo)棋子出發(fā)向上下左右遞歸擴(kuò)散收集所有同色連通的棋子再統(tǒng)計(jì)這些棋子相鄰的獨(dú)立空點(diǎn)數(shù)量// Rule.cpp —— 數(shù)氣先收集同色連通塊再統(tǒng)計(jì)獨(dú)立氣口 static bool visited[MAX_BOARD][MAX_BOARD]; static int group[MAX_BOARD * MAX_BOARD][2]; // 連通塊內(nèi)棋子坐標(biāo) static void CollectGroup(int r, int c, int color, int cnt) { if (board[r][c] ! color || visited[r][c]) return; visited[r][c] true; group[cnt][0] r; group[cnt][1] c; cnt; CollectGroup(r - 1, c, color, cnt); CollectGroup(r 1, c, color, cnt); CollectGroup(r, c - 1, color, cnt); CollectGroup(r, c 1, color, cnt); } int CountLiberties(int r, int c) { int color board[r][c]; if (color EMPTY) return 0; int cnt 0; memset(visited, 0, sizeof(visited)); // 每次調(diào)用必須歸零 CollectGroup(r, c, color, cnt); bool seen[MAX_BOARD][MAX_BOARD] {false}; int dirs[4][2] {{1,0},{-1,0},{0,1},{0,-1}}; int libs 0; for (int i 0; i cnt; i) { for (int d 0; d 4; d) { int nr group[i][0] dirs[d][0]; int nc group[i][1] dirs[d][1]; if (board[nr][nc] EMPTY !seen[nr][nc]) { seen[nr][nc] true; libs; } } } return libs; }邏輯說(shuō)明遞歸深度最多 361 層MFC 默認(rèn)??臻g完全扛得住。seen 數(shù)組保證同一塊棋周?chē)粋€(gè)空點(diǎn)只算一口氣——兩顆相鄰棋子共享同一個(gè)氣口漏掉去重提子判斷會(huì)提前誤觸發(fā)表現(xiàn)為「對(duì)方明明還有氣卻被提子」的怪象。參數(shù)說(shuō)明visited 和 group 聲明成 static 是為了減少遞歸傳參開(kāi)銷(xiāo)但 CountLiberties 每次進(jìn)入必須 memset。seen 是函數(shù)局部變量用 {false}初始化即可每次調(diào)用自動(dòng)重建。dirs 方向數(shù)組順序無(wú)所謂但四個(gè)方向不能漏——漏一個(gè)角部的棋子永遠(yuǎn)數(shù)不對(duì)氣。3.2 提子與禁著點(diǎn)先提對(duì)方再查己方死活落子的完整流程是先把棋子臨時(shí)放上棋盤(pán)提走四周所有氣盡的對(duì)方棋子然后檢查己方這顆棋有沒(méi)有氣。己方?jīng)]氣且剛才沒(méi)提成任何子就是禁著點(diǎn)必須整盤(pán)回卷inline int Opponent(int c) { return (c BLACK) ? WHITE : BLACK; } bool TryPlaceStone(int r, int c, int color) { if (board[r][c] ! EMPTY) return false; int saved[MAX_BOARD][MAX_BOARD]; memcpy(saved, board, sizeof(board)); // 整盤(pán)快照失敗可回卷 board[r][c] color; // 先落子 int dirs[4][2] {{1,0},{-1,0},{0,1},{0,-1}}; int captured 0; for (int d 0; d 4; d) { int nr r dirs[d][0], nc c dirs[d][1]; if (board[nr][nc] Opponent(color) CountLiberties(nr, nc) 0) { captured RemoveGroup(nr, nc); // 提掉整塊 } } // 禁著點(diǎn)自己沒(méi)氣且沒(méi)提到子 if (CountLiberties(r, c) 0 captured 0) { memcpy(board, saved, sizeof(board)); // 回卷 return false; } return true; }邏輯說(shuō)明順序不能反。先查己方氣再提對(duì)方會(huì)出現(xiàn)「提完對(duì)方我自己明明活了卻被判成禁著點(diǎn)」的烏龍。圍棋規(guī)則里提子的優(yōu)先級(jí)高于自殺判斷代碼順序必須和規(guī)則順序一致。整體快照回卷比逐顆撤銷(xiāo)更穩(wěn)多花 441 個(gè) int 的內(nèi)存換來(lái)的是永遠(yuǎn)一致的狀態(tài)這筆賬劃算??狱c(diǎn)captured 記錄的是被提棋子數(shù)量而不是塊數(shù)。只要提走一顆以上這手棋就是合法的哪怕己方落子后暫時(shí)沒(méi)氣——這叫「吃子成活」是新手最容易寫(xiě)成自殺禁手的場(chǎng)景。RemoveGroup 內(nèi)部除了清空棋盤(pán)還要把被提每顆棋子的坐標(biāo)記進(jìn)當(dāng)手 Move 結(jié)構(gòu)供悔棋恢復(fù)使用。3.3 劫比兩步前的局面快照或換 Zobrist 哈希簡(jiǎn)單劫的判定有個(gè)討巧做法把每手落子后的完整局面存進(jìn)歷史棧當(dāng)前這手棋如果導(dǎo)致棋盤(pán)和兩步前完全一致說(shuō)明發(fā)生了「提回來(lái)又提回去」的循環(huán)直接判劫。為什么只看兩步前因?yàn)楹?jiǎn)單劫的循環(huán)周期正好是兩手// Rule.cpp —— TryPlaceStone 內(nèi)部的劫校驗(yàn)接 3.2 的代碼 struct BoardSnapshot { int board[MAX_BOARD][MAX_BOARD]; }; std::vectorBoardSnapshot g_history; // 每手棋后 push 一份 // 在禁著點(diǎn)判斷之后、返回 true 之前加入 BoardSnapshot snap; memcpy(snap.board, board, sizeof(board)); if (g_history.size() 2) { BoardSnapshot prev g_history[g_history.size() - 2]; if (memcmp(snap.board, prev.board, sizeof(board)) 0) { memcpy(board, saved, sizeof(board)); // 回卷禁止打劫 return false; } } return true; // 調(diào)用方落子成功后g_history.push_back(snap);邏輯說(shuō)明memcmp 一次對(duì)比 441 個(gè) int納秒級(jí)完成每手棋調(diào)一次完全不是性能瓶頸。g_history 用 std::vector 實(shí)現(xiàn)——VC 項(xiàng)目里最常用的 STL 容器push_back 和 pop_back 都是 O(1)配合悔棋棧天然兼容。調(diào)用方在落子合法并記錄完提子信息后把 snap push 進(jìn)歷史悔棋時(shí)同步 pop兩者的長(zhǎng)度必須嚴(yán)格一致。如果要嚴(yán)格實(shí)現(xiàn)全局同形superko用 Zobrist 哈希給每個(gè)位置三種狀態(tài)各分配一個(gè) 64 位隨機(jī)數(shù)局面哈希等于所有非空點(diǎn)隨機(jī)數(shù)的異或。落子、提子各做兩次異或就是 O(1) 增量更新把整盤(pán) memcmp 換成哈希比對(duì)代碼量增加約 30 行。普通人對(duì)戰(zhàn)程序用不到寫(xiě)清楚注釋留給后人即可。4. 棋盤(pán)繪制與鼠標(biāo)交互用 GDI 畫(huà) 19 路棋盤(pán)落子不漂移4.1 坐標(biāo)換算半格偏移決定你有沒(méi)有「點(diǎn)偏一格」這一章處理整個(gè)工程里最直觀也最容易改壞的部分——用 GDI 做的 2D 繪圖層。棋盤(pán)繪制的核心是兩套坐標(biāo)互轉(zhuǎn)格點(diǎn)坐標(biāo)1 到 19和像素坐標(biāo)。MARGIN 是棋盤(pán)外沿到窗口邊緣的距離CELL_SIZE 是相鄰交叉點(diǎn)間距// BoardView.cpp —— 坐標(biāo)互轉(zhuǎn) // 格點(diǎn) - 像素畫(huà)線、畫(huà)棋子時(shí)用 int GridToPixel(int g) { return MARGIN (g - 1) * CELL_SIZE; } // 像素 - 格點(diǎn)鼠標(biāo)點(diǎn)擊時(shí)用CELL_SIZE/2 是關(guān)鍵四舍五入 int PixelToGrid(int px) { return (px - MARGIN CELL_SIZE / 2) / CELL_SIZE 1; }邏輯說(shuō)明整數(shù)除法向下取整直接拿 (px - MARGIN) / CELL_SIZE鼠標(biāo)落在兩個(gè)交叉點(diǎn)正中間時(shí)會(huì)偏到左邊一格這就是「點(diǎn)偏一格」的玄學(xué)來(lái)源。加上 CELL_SIZE/2 把除法變成四舍五入每個(gè)交叉點(diǎn)周?chē)霃?14 像素半個(gè)格距都是有效命中區(qū)點(diǎn)歪一點(diǎn)也不會(huì)落錯(cuò)格。參數(shù)說(shuō)明MARGIN、CELL_SIZE 在工程里必須全局統(tǒng)一不能一處用宏一處用字面量。棋子直徑取 CELL_SIZE - 4留出相鄰棋子的縫隙棋盤(pán)線用 1 像素默認(rèn)畫(huà)筆星位畫(huà)小實(shí)心圓坐標(biāo)文字用 TextOut 標(biāo)注在格點(diǎn)外側(cè)。這些值都集中在 GameDef.h 里定義調(diào)完 UI 不需要?jiǎng)右?guī)則代碼。4.2 雙緩沖MFC 重繪不閃的標(biāo)配寫(xiě)法直接用 CPaintDC 畫(huà)棋盤(pán)線的后果是每次 Invalidate 觸發(fā) OnPaint窗口先把客戶(hù)區(qū)擦白再逐線畫(huà)落子、拉伸都閃成一片。雙緩沖的本質(zhì)是先在內(nèi)存位圖上畫(huà)完所有內(nèi)容再一次 BitBlt 上屏// GoDlg.cpp —— OnPaint 雙緩沖 void CGoDlg::OnPaint() { CPaintDC dc(this); CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, m_clientW, m_clientH); CBitmap* pOld memDC.SelectObject(bmp); DrawBoard(memDC); // 畫(huà)棋盤(pán)線、星位、坐標(biāo) DrawStones(memDC); // 畫(huà)黑白棋子 dc.BitBlt(0, 0, m_clientW, m_clientH, memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); // 收尾換回原對(duì)象再刪資源 bmp.DeleteObject(); memDC.DeleteDC(); }邏輯說(shuō)明m_clientW / m_clientH 是客戶(hù)區(qū)當(dāng)前尺寸在 OnSize 里用 GetClientRect 取值。DrawBoard 內(nèi)部用 MoveTo / LineTo 畫(huà)橫豎各 19 條線按坐標(biāo)標(biāo)注 A-S 和 1-19DrawStones 遍歷棋盤(pán)數(shù)組非空格點(diǎn)用 Ellipse 畫(huà)實(shí)心圓。所有繪制全在內(nèi)存 DC 完成屏幕只做一次 BitBlt 合并輸出。參數(shù)說(shuō)明畫(huà)棋子注意 Ellipse 的矩形邊界——中心是 GridToPixel 的返回值左上角是中心減半徑右下角是中心加半徑。黑棋用黑色實(shí)心畫(huà)筆加淺灰窄邊框提亮白棋用白色實(shí)心加灰色描邊不然白棋在淺色棋盤(pán)上會(huì)隱身。注意OnPaint 里創(chuàng)建的 GDI 對(duì)象必須成對(duì)釋放先 SelectObject 換回舊對(duì)象再 DeleteObject。GDI 泄漏的典型癥狀是程序運(yùn)行半小時(shí)后棋子畫(huà)不出來(lái)重啟又恢復(fù)正常排查起來(lái)非常痛苦。4.3 悔棋與局面棧vector 做歷史記錄的完整閉環(huán)圍棋的悔棋不只是拿掉最后一顆子——還得把提掉的對(duì)方棋子按原坐標(biāo)放回去所以落子時(shí)必須把提子信息記錄下來(lái)// GameDef.h —— 一手棋的完整記錄 struct Move { int row, col; // 落子位置 int color; // BLACK / WHITE std::vectorstd::pairint,int captured; // 被提的對(duì)方棋子 }; std::vectorMove g_moveStack; // 全局歷史棧// Rule.cpp —— 悔棋實(shí)現(xiàn) void UndoLastMove() { if (g_moveStack.empty()) return; Move mv g_moveStack.back(); g_moveStack.pop_back(); board[mv.row][mv.col] EMPTY; // 摘掉這手棋 for (size_t i 0; i mv.captured.size(); i) { int rr mv.captured[i].first; int cc mv.captured[i].second; board[rr][cc] Opponent(mv.color); // 被提的放回去 } g_history.pop_back(); // 局面快照同步回退劫判斷才不亂 }邏輯說(shuō)明captured 里棋子的顏色恒為對(duì)手色不用單獨(dú)記錄?;谄鍟r(shí)按對(duì)方顏色放回原位再順手把局面快照 pop 掉——這條最容易漏漏掉之后劫判斷會(huì)拿舊局面對(duì)比當(dāng)前棋局直接翻車(chē)。vector 的 back() 返回末尾引用pop_back() O(1) 刪除作為棧用非常順手。參數(shù)說(shuō)明pairint,int 存坐標(biāo)足夠追求可讀性可以換成小結(jié)構(gòu)體。整份歷史棧在「新開(kāi)局」時(shí)用 clear() 一次性清空同時(shí)清掉 g_history兩棧同生共死?;谄灏粹o的消息響應(yīng)里調(diào) UndoLastMove 后記得 Invalidate(FALSE)讓棋盤(pán)立刻刷新。5. 避坑記錄VS2010 編譯運(yùn)行圍棋源碼的 5 個(gè)高頻翻車(chē)點(diǎn)編譯老工程和寫(xiě)新工程完全是兩碼事。v100 工具集、MBCS/Unicode 字符集、GDI 重繪、MFC 運(yùn)行庫(kù)這四個(gè)關(guān)鍵詞湊在一個(gè)圍棋項(xiàng)目里就是新手最常見(jiàn)的勸退現(xiàn)場(chǎng)。下面 5 條按出現(xiàn)概率排序每條都是「現(xiàn)象、原因、解決」三段式照著定位能省出大半天。先說(shuō)兩個(gè)通用習(xí)慣看編譯輸出窗口的第一條錯(cuò)誤而不是最后一條以及把錯(cuò)誤全部清完再按 F5別讓第一次報(bào)錯(cuò)淹沒(méi)在后續(xù)的級(jí)聯(lián)錯(cuò)誤里。5.1 .sln 雙擊打不開(kāi)提示版本不兼容現(xiàn)象用高版本 VS 打開(kāi) VS2010 的 .sln彈窗提示需要進(jìn)行一次單向升級(jí)點(diǎn)確認(rèn)后代碼能看F5 報(bào)錯(cuò) C1083 找不到 stdafx.h 或一堆 MFC 頭文件。原因VS2010 的平臺(tái)工具集是 v100高版本 VS 默認(rèn)把工程換成自己的工具集但老工程里的附加包含目錄沒(méi)有跟著遷移裸裝 VS2010 不帶 SP1 時(shí)部分 MFC 頭文件本身就缺內(nèi)容換誰(shuí)打開(kāi)都編不過(guò)。解決堅(jiān)持用 VS2010 就先裝 VS2010 SP1 再開(kāi)工程換高版本就在項(xiàng)目屬性 → 配置屬性 → 常規(guī) → 平臺(tái)工具集里顯式改成 v140 / v142改完重新生成解決方案。如果 C1083 還指向某個(gè)盤(pán)符下的絕對(duì)路徑頭文件把附加包含目錄改成相對(duì)路徑工程挪了位置才不會(huì)再次崩塌。提示改工具集前先復(fù)制一份 .vcxproj 備份升級(jí)過(guò)程會(huì)改寫(xiě)項(xiàng)目 GUID 和若干屬性不備份就沒(méi)有后悔藥。5.2 界面中文全部亂碼現(xiàn)象編譯鏈接全部通過(guò)運(yùn)行后對(duì)話框按鈕上的「落子」「悔棋」「開(kāi)始」全顯示成問(wèn)號(hào)或菱形豆腐塊英文正常。原因源碼 .cpp/.h 和 .rc 資源文件的保存編碼與項(xiàng)目「字符集」設(shè)置不一致。VS2010 默認(rèn) Unicode 但編輯器可能按系統(tǒng) ANSI 代碼頁(yè)存盤(pán)或者反過(guò)來(lái)工程用多字節(jié)字符集而文件是 UTF-8——編譯器按錯(cuò)誤的編碼讀中文字面量出來(lái)的就是亂碼。解決先統(tǒng)一字符集項(xiàng)目屬性 → 配置屬性 → 常規(guī) → 字符集選「使用 Unicode 字符集」再把所有 .cpp/.h/.rc 用 VS 菜單「文件 → 高級(jí)保存選項(xiàng)」轉(zhuǎn)成 UTF-8 with BOM。注意 .rc 資源文件的亂碼必須單獨(dú)看——資源編譯器按文件內(nèi)聲明的代碼頁(yè)解析正文轉(zhuǎn)了 .rc 沒(méi)轉(zhuǎn)界面上照樣是豆腐塊。判斷文件編碼有個(gè)土辦法記事本打開(kāi)另存為對(duì)話框的編碼下拉框會(huì)顯示當(dāng)前檢測(cè)到的編碼UTF-8 會(huì)明確寫(xiě)出來(lái)ANSI 則對(duì)應(yīng) GBK。5.3 落子瞬間棋盤(pán)閃白現(xiàn)象每點(diǎn)一下鼠標(biāo)棋盤(pán)先整個(gè)白屏再重新畫(huà)出來(lái)連續(xù)落子閃爍肉眼可見(jiàn)拉伸窗口時(shí)更嚴(yán)重。原因OnPaint 里直接用 CPaintDC 一塊塊畫(huà)棋盤(pán)線配合 Invalidate(TRUE) 強(qiáng)制擦除背景重繪路徑變成了「擦白 → 畫(huà)線 → 擦白 → 畫(huà)子」的循環(huán)閃爍是必然結(jié)果。解決照第 4 章的內(nèi)存 DC 雙緩沖先在內(nèi)存位圖畫(huà)完再 BitBlt 上屏Invalidate 參數(shù)從 TRUE 改成 FALSE。改完做一個(gè)回歸測(cè)試按住窗口右下角連續(xù)拖動(dòng)縮放棋盤(pán)應(yīng)該平滑跟手全程看不到白閃。如果雙緩沖后還閃重寫(xiě) OnEraseBkgnd 直接 return TRUE告訴系統(tǒng)背景永遠(yuǎn)不需要擦。5.4 明明圍住了對(duì)方棋卻提不掉現(xiàn)象單測(cè)數(shù)氣函數(shù)結(jié)果正確但實(shí)戰(zhàn)里白棋被黑棋圍死界面上的白子卻始終不消失或者一整塊被提了一半。原因visited 靜態(tài)數(shù)組在上一次調(diào)用后沒(méi)歸零CollectGroup 第二次運(yùn)行時(shí)帶著殘留的 true 標(biāo)記跳過(guò)大量棋子連通塊收集不全氣數(shù)虛高。另一個(gè)隱蔽情況是 TryPlaceStone 的失敗分支先 return 再恢復(fù)棋盤(pán)順序反了棋盤(pán)上留下一顆刪不掉的幽靈棋。解決在 CountLiberties 入口強(qiáng)制 memset(visited, 0, sizeof(visited))并把 TryPlaceStone 每個(gè) return false 分支都改成先恢復(fù)棋盤(pán)再返回。驗(yàn)證用一個(gè)固定棋譜白棋先放 (1,1) 角黑棋連下 (1,2)、(2,1)此刻白棋四鄰全是黑棋或邊界應(yīng)被提掉 1 顆打印白棋總數(shù)從 1 變 0 才算通過(guò)再點(diǎn)悔棋白棋總數(shù)應(yīng)恢復(fù)為 1。這套三步棋能同時(shí)驗(yàn)證提子和悔棋兩條邏輯。5.5 生成的 exe 拷到別的機(jī)器啟動(dòng)閃退現(xiàn)象本機(jī) F5 跑得歡把 Release 目錄下的 exe 單獨(dú)拷到另一臺(tái) Windows 機(jī)器雙擊報(bào) 0xc000007b 或者干脆沒(méi)反應(yīng)。原因工程默認(rèn)動(dòng)態(tài)鏈接 MFC 運(yùn)行庫(kù)VS2010 對(duì)應(yīng) mfc100u.dll 和 msvcr100.dll目標(biāo)機(jī)器沒(méi)裝對(duì)應(yīng)版本運(yùn)行庫(kù)就啟動(dòng)失敗。0xc000007b 還經(jīng)常和 32/64 位不匹配摻在一起工程編的是 x86目標(biāo)機(jī)器沒(méi)裝 x86 運(yùn)行庫(kù)只有 x64 的也會(huì)報(bào)這個(gè)碼。解決不想在目標(biāo)機(jī)器裝運(yùn)行庫(kù)就在項(xiàng)目屬性 → 常規(guī) → MFC 的使用里改成「在靜態(tài)庫(kù)中使用 MFC」exe 體積增大約 1 到 2 MB但完全自包含。同時(shí)檢查配置管理器里的活動(dòng)解決方案平臺(tái)是 Win32 還是 x64與運(yùn)行環(huán)境對(duì)齊。改完重新編譯用 dumpbin /dependents 查看 exe 依賴(lài)列表確認(rèn)沒(méi)有 mfc100u.dll 就說(shuō)明靜態(tài)鏈接成功。6. 給 AI 對(duì)手加一檔「棋感」評(píng)分函數(shù)與自對(duì)弈驗(yàn)證6.1 靜態(tài)評(píng)分落子30 行讓 AI 從隨機(jī)棋手升級(jí)成入門(mén)棋手規(guī)則和界面跑通之后最有成就感的收尾是把只會(huì)隨機(jī)落子的 AI 換成一個(gè)有基本棋感的對(duì)手。最輕量的改進(jìn)是給每個(gè)空點(diǎn)打靜態(tài)評(píng)分——角、星位給加成隨機(jī)擾動(dòng)控制在 10% 以?xún)?nèi)保證同一局面不是每次都下同一個(gè)點(diǎn)// Ai.cpp —— 極簡(jiǎn)靜態(tài)評(píng)分落子 int EvalGrid(int r, int c) { if (board[r][c] ! EMPTY) return -99999; int score rand() % 10; if ((r 1 || r 19) (c 1 || c 19)) score 50; // 角 if (r 1 || r 19 || c 1 || c 19) score 15; // 邊 return score; }實(shí)現(xiàn)注意評(píng)分只在空點(diǎn)上調(diào)用有子的位置直接給極負(fù)分隨機(jī)擾動(dòng)最后加別讓隨機(jī)數(shù)蓋過(guò)棋型分。調(diào)參驗(yàn)證別靠人機(jī)試玩的主觀感受——讓新舊兩個(gè)版本各執(zhí)黑白自對(duì)弈 30 局統(tǒng)計(jì)勝率和平均手?jǐn)?shù)手?jǐn)?shù)明顯變長(zhǎng)說(shuō)明策略變保守了勝率穩(wěn)定過(guò)半說(shuō)明改動(dòng)真實(shí)有效。我個(gè)人的習(xí)慣是每改一個(gè)參數(shù)就往歷史棧里留一條記錄跑完自對(duì)弈回看上一版棋譜而不是靠一局勝負(fù)下結(jié)論。圍棋程序的黑匣子不淺但每一層都有具體驗(yàn)證方法局面快照驗(yàn)證規(guī)則自對(duì)弈統(tǒng)計(jì)驗(yàn)證策略最后才輪到人機(jī)試玩。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取