課程設計選題:黑白棋 (Othello/Reversi)。技術棧:**純 C 語言 + Windows SDK (GDI/User32) + Visual Studio**,拒絕 MFC、C++、托管代碼、第三方圖形庫。目標:深度理解 Windows 消息機制、GDI 繪圖流程、遊戲邏輯與 AI 搜索算法的工程落地。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance,
LoadIcon(NULL, IDI_APPLICATION), LoadCursor(NULL, IDC_ARROW),
(HBRUSH)(COLOR_WINDOW+1), NULL, L"OthelloClass", NULL };
RegisterClassEx(&wc);
HWND hWnd = CreateWindow(L"OthelloClass", L"黑白棋", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);
ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
| 消息 | 處理邏輯 | 關鍵點 |
|---|---|---|
| `WM_CREATE` | 載入資源 (位圖、聲音)、初始化棋盤狀態、創建子窗口/菜單 | `LoadBitmap`/`LoadMenu`/`CreateWindow` (對話框) |
| `WM_PAINT` | **核心繪圖** : `BeginPaint` → 繪製棋盤格線、棋子、合法落子提示、分數面板 → `EndPaint` | **雙緩衝防閃爍** : 兼容 DC + 位圖,先畫內存再 `BitBlt` 到屏幕 |
| `WM_LBUTTONDOWN` | 座標轉換 (屏幕→棋盤格)、合法性校驗、翻轉邏輯、AI 觸發、檢測勝負 | 客戶區座標 `LOWORD(lParam)`/`HIWORD(lParam)` |
| `WM_COMMAND` | 菜單/對話框命令:新遊戲、難度選擇、設置、幫助、英雄榜 | `DialogBoxParam` 模態對話框 |
| `WM_TIMER` | AI 思考動畫、落子動畫幀更新 | `SetTimer`/`KillTimer` |
| `WM_DESTROY` | 釋放 GDI 對象 (`DeleteObject`)、殺死定時器、保存配置、`PostQuitMessage(0)` | **資源泄漏零容忍** |
使用 **Visual Studio 資源編輯器** 可視化設計,生成 `resource.h` (ID 宏) 與 `.rc` 腳本,編譯鏈入可執行文件。
#define BOARD_SIZE 8
#define EMPTY 0
#define BLACK 1
#define WHITE 2
typedef struct {
BYTE board[BOARD_SIZE][BOARD_SIZE]; // 0空 1黑 2白
int currentPlayer; // BLACK/WHITE
int blackCount, whiteCount;
int gameState; // PLAYING, BLACK_WIN, WHITE_WIN, DRAW
int aiLevel; // 1/2/3
bool vsAI;
} GameState;
GameState g_game; // 全局單例 (簡單項目可接受)
const int dx[8] = {-1,-1,-1,0,0,1,1,1};
const int dy[8] = {-1,0,1,-1,1,-1,0,1};
// 檢查 (x,y) 落子是否合法,可選執行翻轉
bool IsValidMove(int x, int y, bool doFlip) {
if (g_game.board[y][x] != EMPTY) return false;
int player = g_game.currentPlayer;
int opponent = 3 - player;
bool anyFlipped = false;
for (int dir = 0; dir < 8; dir++) {
int nx = x + dx[dir], ny = y + dy[dir];
int count = 0;
while (InBoard(nx, ny) && g_game.board[ny][nx] == opponent) {
nx += dx[dir]; ny += dy[dir]; count++;
}
if (count > 0 && InBoard(nx, ny) && g_game.board[ny][nx] == player) {
if (doFlip) {
nx = x + dx[dir]; ny = y + dy[dir];
while (g_game.board[ny][nx] == opponent) {
g_game.board[ny][nx] = player;
nx += dx[dir]; ny += dy[dir];
}
g_game.board[y][x] = player;
UpdateCount(player, count + 1); // 更新棋子數
}
anyFlipped = true;
}
}
return anyFlipped;
}
Player Move (WM_LBUTTONDOWN)
→ Validate & Flip → Check Opponent Has Move?
→ Yes: Switch Player → If AI Turn → SetTimer(AI_THINK)
→ No: Check Current Player Has Move?
→ Yes: Continue (Player gets another turn)
→ No: Game Over → Calculate Winner → Show Result Dialog
| 級別 | 策略 | 特點 |
|---|---|---|
| Level 1 (入門) | **固定權重表 (Positional Weight Table)** | 角 > 邊 > 內部,無搜索,極快,固定走法,易破解 |
| Level 2 (進階) | **最大翻轉貪心 + 隨機打擾** | 選翻轉最多位置,平局隨機,仍屬短視,無前瞻 |
| Level 3 (專家 - 未完成) | **Minimax + Alpha-Beta 剪枝 + 迭代加深 + 置換表** | 多層搜索 (4-6 層),動態評估函數 (行動力、穩定性、邊緣、奇偶性),時間控制 |
const int WEIGHT[8][8] = {
{120, -20, 20, 5, 5, 20, -20, 120},
{-20, -40, -5, -5, -5, -5, -40, -20},
{ 20, -5, 15, 3, 3, 15, -5, 20},
{ 5, -5, 3, 3, 3, 3, -5, 5},
{ 5, -5, 3, 3, 3, 3, -5, 5},
{ 20, -5, 15, 3, 3, 15, -5, 20},
{-20, -40, -5, -5, -5, -5, -40, -20},
{120, -20, 20, 5, 5, 20, -20, 120}
};
// 角(120)極高,角旁(-20/-40)極低 (避免送角),邊緣次之
int AlphaBeta(int depth, int alpha, int beta, int player) {
if (depth == 0 || GameOver()) return EvaluateBoard(player);
Move moves[MAX_MOVES]; int moveCount = GenerateMoves(moves, player);
if (moveCount == 0) return -AlphaBeta(depth-1, -beta, -alpha, 3-player); // Pass
for (int i = 0; i < moveCount; i++) {
MakeMove(&moves[i]);
int score = -AlphaBeta(depth-1, -beta, -alpha, 3-player);
UndoMove(&moves[i]);
if (score >= beta) return beta; // Beta cutoff
if (score > alpha) alpha = score;
}
return alpha;
}
// 迭代加深 + 時間控制
Move FindBestMove(int maxTimeMs) {
Move bestMove;
for (int depth = 1; ; depth++) {
if (GetTickCount() - startTime > maxTimeMs) break;
int alpha = -INF, beta = INF;
Move moves[MAX_MOVES]; int n = GenerateMoves(moves, g_game.currentPlayer);
for (int i = 0; i < n; i++) {
MakeMove(&moves[i]);
int score = -AlphaBeta(depth-1, -beta, -alpha, 3-g_game.currentPlayer);
UndoMove(&moves[i]);
if (score > alpha) { alpha = score; bestMove = moves[i]; }
}
}
return bestMove;
}
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// 1. 創建兼容 DC 與位圖 (僅首次/尺寸變化時創建,靜態變量緩存)
static HDC hMemDC = NULL; static HBITMAP hMemBmp = NULL, hOldBmp = NULL;
RECT rc; GetClientRect(hWnd, &rc);
if (!hMemDC || rc.right != memWidth || rc.bottom != memHeight) {
if (hMemDC) { SelectObject(hMemDC, hOldBmp); DeleteObject(hMemBmp); DeleteDC(hMemDC); }
hMemDC = CreateCompatibleDC(hdc);
hMemBmp = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
hOldBmp = SelectObject(hMemDC, hMemBmp);
memWidth = rc.right; memHeight = rc.bottom;
}
// 2. 繪製到內存 DC
FillRect(hMemDC, &rc, GetSysColorBrush(COLOR_WINDOW)); // 背景
DrawBoardGrid(hMemDC); // 格線
DrawPieces(hMemDC); // 棋子 (TransparentBlt / MaskBlt 實現透明)
DrawValidHints(hMemDC); // 合法落子高亮
DrawScorePanel(hMemDC); // 分數、當前玩家
// 3. 一次性 BitBlt 到屏幕
BitBlt(hdc, 0, 0, rc.right, rc.bottom, hMemDC, 0, 0, SRCCOPY);
EndPaint(hWnd, &ps);
return 0;
}
關鍵優化**:靜態緩存 `hMemDC`/`hMemBmp` 避免幀幀創建銷毀;`WM_ERASEBKGND` 返回 `TRUE` 禁止系統擦除背景;`InvalidateRect(hWnd, &dirtyRect, FALSE)` 實現**區域重繪** (僅重繪變化區域)。
這個專案雖是課程作業,但踩過的坑 (消息循環、GDI 泄漏、AI 算法、繪圖閃爍、全局耦合) 正是 Windows 系統級開發的必修課。代碼已開源 (假設鏈接),歡迎交流指正。