上一篇指出原代碼的核心問題:全域變數耦合、函數職責不清、遞迴濫用。本文展示重構關鍵步驟:資料封裝與函數解耦。
// 牌堆:52 張牌 + 當前牌數
typedef struct {
int cards[52];
int num_cards;
} POKER;
// 玩家/莊家狀態:兩種點數計算方式 (A=1 或 11) + 最終有效點數
typedef struct {
int low; // A 算 1 點
int high; // A 算 11 點
int score; // 實際採用點數: high<=21 ? high : low
} GAMER;
設計意圖:將分散的陣列 (cards[], playerPoints[], dealerPoints[], total[]) 封裝為語意清晰的類型,消除「魔術索引」(如 ACELOW=0, ACEHIGH=1)。
void dispCard(int cardDrawn, int points[]) {
switch(cardDrawn) {
case 11: puts("Jack"); points[ACELOW]+=10; points[ACEHIGH]+=10; break;
case 1: puts("Ace"); points[ACELOW]+=1; points[ACEHIGH]+=11; break;
// ...
}
}
disp() + 純計分 update()// 只負責輸出牌面名稱
void disp(int card) {
switch(card) {
case 1: puts("Ace"); return;
case 11: puts("Jack"); return;
case 12: puts("Queen"); return;
case 13: puts("King"); return;
default: printf("%d\n", card); return;
}
}
// 只負責更新點數結構體
void update(int card, int points[2]) { // points[0]=low, points[1]=high
if (card >= 11 && card <= 13) { // J, Q, K
points[0] += 10; points[1] += 10;
} else if (card == 1) { // Ace
points[0] += 1; points[1] += 11;
} else { // 2-10
points[0] += card; points[1] += card;
}
}
收益:disp() 可單獨測試/複用;update() 邏輯集中,易修改規則 (如新增 Joker 牌)。
getcard()// 從牌堆發一張牌,更新玩家狀態,顯示牌面與總分
void getcard(GAMER *p, int card) {
disp(card); // 顯示
update(card, (int[]){p->low, p->high}); // 更新 (需適配結構體)
// 計算最終有效分數
p->score = (p->high <= 21) ? p->high : p->low;
printf("總分:%d\n", p->score);
}
主流程變得極其清晰:
puts("莊家拿牌:"); getcard(&dealer, dealcard(&poker));
puts("你拿牌:"); getcard(&player, dealcard(&poker));
dealcard()// 從牌堆隨機抽一張,用最後一張牌填補空位 (O(1) 移除)
int dealcard(POKER *p) {
int idx = rand() % p->num_cards;
int card = p->cards[idx];
p->cards[idx] = p->cards[--p->num_cards]; // 縮減牌堆
return card;
}
原代碼 (*numCards)-- 分散在多處,現集中於此,且使用 Fisher-Yates 洗牌思想 (隨機抽取+末尾填補) 保證均勻分布。
// 玩家決定要牌/停牌
do {
getcard(&player, dealcard(&poker));
} while (again("繼續要牌(Y/N)?") == YES);
again() 處理輸入緩衝區,回傳枚舉 YES/NO,型別安全。
// 莊家策略:點數 < 17 必須要牌
puts("莊家繼續拿牌:");
do {
getcard(&dealer, dealcard(&poker));
} while (dealer.score < 17);
// 勝負判斷邏輯集中化
void declare_winner(GAMER dealer, GAMER player) {
if (dealer.score == 21) { puts("你輸了。"); return; }
if (dealer.score > 21) {
if (player.score > 21) { puts("平局。"); return; }
puts("你贏了!\a"); return;
}
// dealer < 21
if (player.score > 21 || dealer.score >= player.score) {
puts("你輸了。"); return;
}
puts("你贏了!\a");
}
所有規則集中在 declare_winner(),易於閱讀、修改、單元測試。
main()
├─ init_poker(&poker) // 初始化牌堆
├─ srand(time(NULL)) // 隨機種子
├─ 遊戲主迴圈
│ ├─ getcard(&dealer, dealcard(&poker)) // 莊家首張
│ ├─ getcard(&player, dealcard(&poker)) // 玩家首張
│ ├─ do { getcard(&player, dealcard(&poker)); } while (again(...)) // 玩家回合
│ ├─ do { getcard(&dealer, dealcard(&poker)); } while (dealer.score < 17) // 莊家回合
│ └─ declare_winner(dealer, player) // 判斷勝負
└─ again("繼續遊戲?") 控制外層迴圈
| 指標 | 原始代碼 | 重構版 |
|---|---|---|
| 全域變數 | 10+ 個 | 0 個 (main 區域變數) |
| 函數數量 | 12 個 (耦合) | 8 個 (高內聚低耦合) |
| 遞迴深度 | 不受控 (風險) | 0 (全迴圈) |
| 測試難度 | 極難 (需完整遊戲) | 易 (每函數可獨立測試) |
| 擴充性 | 幾乎不可能 | 易 (如加入分牌、雙倍下注) |
POKER 管牌,GAMER 管分,函數操作結構體指標disp 只顯示,update 只算分,dealcard 只發牌do-while,堆疊安全、邏輯平坦YES/NO、ACE_LOW/HIGH (若保留陣列) 增加可讀性這個重構過程不僅修復了劣質代碼,更展示了「領域建模 → 介面設計 → 流程編排」的軟體工程基本功。下一篇將展示完整可編譯執行的最終代碼。