《寫給大家看的 C 語言書(第 2 版)》附錄 B 提供一個 21點(Blackjack)遊戲範例。本系列文章將深度剖析其代碼問題,並展示重構過程。這是第一部分,著重於整體架構與核心邏輯缺陷。
dispCard() 同時負責「顯示牌面」與「計算點數」,導致莊家/玩家抽牌時邏輯耦合。例如:
void dispCard(int cardDrawn, int points[]) {
switch(cardDrawn) {
case 11: puts("Jack"); points[ACELOW] += 10; points[ACEHIGH] += 10; break;
// ... 其他情況同樣混合顯示與計分
}
}
後果:無法單獨測試顯示邏輯;計分規則分散在多處;莊家與玩家共用同一 points 陣列易造成狀態混淆。
原代碼大量使用全域變數 (askedForName, cards[], playerPoints[], dealerPoints[], total[] 等),函數間隱性耦合極強。例如 initCardsScreen() 初始化多個全域陣列,dealerGetsCard()、playerGetsCard() 直接修改全域狀態,導致程式流程難以追蹤。
玩家回合使用遞迴呼叫 playerGetsCard(),但終止條件依賴使用者輸入與點數判斷,極易導致堆疊溢位或邏輯死循環。莊家回合同樣問題。
getAns() 使用 scanf() 或 getchar() 無緩衝區清理,換行符殘留會導致後續輸入讀取失敗。
牌面值 (1, 11, 12, 13)、點數計算 (10, 11)、陣列大小等硬編碼在多處,維護性極差。
下一部分將展示如何:
POKER (牌堆) 與 GAMER (玩家狀態) 封裝資料dispCard() 為純顯示 disp() 與計分 update()dealcard()、getcard()、declare_winner() 等單一職責函數這個 21 點案例雖然代碼品質低,但恰好是練習「代碼審查與重構」的絕佳教材——比寫新代碼更能鍛鍊工程思維。