《寫給大家看的C語言書》雖為入門暢銷書,但其附錄 B 的「21點撲克遊戲」卻是典型的教學反面教材:全域變數氾濫、函數耦合極高、邏輯與顯示混雜、無錯誤處理、隨機數種子重複設定等問題集大成。本系列文章將逐步剖析其缺陷,並展示現代 C 語言重構思路。本文為第四部分,聚焦於主控流程解耦與函數介面規範化。
// constants.h
typedef enum { ROLE_DEALER = 0, ROLE_PLAYER = 1 } Role;
typedef enum { ACE_LOW = 0, ACE_HIGH = 1 } AceMode;
typedef enum { NO = 0, YES = 1 } Bool;
// game_types.h
#define MAX_CARDS 52
#define MAX_HAND 11 // 21點最多11張牌 (11個A)
typedef struct {
int cards[MAX_CARDS];
int count;
} Deck;
typedef struct {
int points[2]; // [ACE_LOW], [ACE_HIGH]
int total; // 最終有效點數
} HandScore;
typedef struct {
Deck deck;
HandScore dealer;
HandScore player;
Bool game_over;
} GameState;
// 發牌:從牌堆取一張,更新手牌分數,返回牌面值
int deal_card(Deck *deck, HandScore *hand) {
if (deck->count == 0) return -1; // 牌發完
int idx = rand() % deck->count;
int card = deck->cards[idx];
deck->cards[idx] = deck->cards[--deck->count]; // 交換移除 O(1)
// 更新分數 (Ace 雙計分邏輯內聚於此)
if (card == 1) { // Ace
hand->points[ACE_LOW] += 1;
hand->points[ACE_HIGH] += 11;
} else if (card >= 11) { // J/Q/K
hand->points[ACE_LOW] += 10;
hand->points[ACE_HIGH] += 10;
} else {
hand->points[ACE_LOW] += card;
hand->points[ACE_HIGH] += card;
}
// 計算有效總分
hand->total = (hand->points[ACE_HIGH] <= 21) ? hand->points[ACE_HIGH] : hand->points[ACE_LOW];
return card;
}
// 判斷勝負:純邏輯,無 I/O
typedef enum { RESULT_DEALER_WIN, RESULT_PLAYER_WIN, RESULT_PUSH } GameResult;
GameResult judge_winner(const HandScore *dealer, const HandScore *player) {
if (dealer->total == 21) return RESULT_DEALER_WIN; // 莊家黑傑克
if (dealer->total > 21) {
return (player->total > 21) ? RESULT_PUSH : RESULT_PLAYER_WIN;
}
if (player->total > 21) return RESULT_DEALER_WIN;
if (dealer->total >= player->total) return RESULT_DEALER_WIN;
return RESULT_PLAYER_WIN;
}
// game_flow.c
void play_round(GameState *g) {
init_deck(&g->deck);
reset_hand(&g->dealer);
reset_hand(&g->player);
// 初始發兩張牌 (交替)
deal_card(&g->deck, &g->dealer);
deal_card(&g->deck, &g->player);
deal_card(&g->deck, &g->dealer);
deal_card(&g->deck, &g->player);
// 玩家回合
while (!g->game_over && g->player.total < 21) {
display_status(g);
if (ask_hit_or_stand() == 'S') break;
deal_card(&g->deck, &g->player);
}
// 莊家回合 (規則固定:<17 必補)
while (!g->game_over && g->dealer.total < 17) {
deal_card(&g->deck, &g->dealer);
}
// 結算
g->game_over = YES;
display_final(g);
GameResult res = judge_winner(&g->dealer, &g->player);
announce_result(res);
}
| 原始缺陷 | 重構方案 | 收益 |
|---|---|---|
| 全域變數 | 封裝於 `GameState` 結構體,傳遞指標 | 狀態可見性明確,支援多局並行、易於序列化/測試 |
| 發牌/計分/顯示混雜 | 拆分為 `deal_card` (純邏輯) + `display_*` (純 I/O) | 邏輯可單元測試,介面可替換 (CLI/GUI/Web) |
| 主流程硬編碼 | 狀態機 `play_round` 驅動 | 流程清晰,易擴展 (如加入分牌、雙倍下注、保險) |
| `srand` 重複呼叫 | 程式啟動時 **唯一** 呼叫 `srand((unsigned)time(NULL))` | 保證隨機序列品質 |
| 魔術數字 | 枚舉/常數集中定義於頭檔 | 語義自文檔化,修改單點生效 |
重構不僅是「整理代碼」,更是將隱性業務知識顯性化、將耦合關係解耦合的過程。從這個 21 點遊戲的演進中,我們看到:良好的 C 程式設計同樣能實現高內聚、低耦合、可測試、可維護的架構 —— 關鍵在於嚴謹的資料建模與函數契約設計。