《寫給大家看的C語言書》附錄B的21點(Blackjack)遊戲雖然完整實現了遊戲邏輯,但代碼結構混亂、全局變量濫用、函數耦合度高,是典型的「能跑不能維護」代碼。本系列文章將逐步重構該程序,展示從面向過程到模塊化設計的演進過程。
main.c,無模塊劃分cards[]、playerPoints[]等十餘個全局變量dispCard同時負責顯示邏輯與分數計算DEALER 0、ACEHIGH 1等宏定義缺乏語義if-else超過5層,極難追蹤將單體拆分為四個核心模塊:
/* poker.h - 撲克牌管理 */
typedef struct {
int cards[52];
int num_cards;
} Poker;
void poker_init(Poker *p);
int poker_draw(Poker *p); // 抽牌並自動洗牌
/* gamer.h - 玩家狀態 */
typedef struct {
int score; // 最終分數
int low; // A按1計算的分數
int high; // A按11計算的分數
} Gamer;
void gamer_add_card(Gamer *g, int card);
int gamer_score(const Gamer *g);
/* game.h - 遊戲流程 */
typedef enum { PLAYER, DEALER } Role;
void game_21(void); // 單局遊戲入口
/* ui.h - 交互界面 */
void ui_show_card(int card);
void ui_show_score(const char *role, int score);
int ui_ask_hit(void); // 返回1要牌,0停牌
Gamer結構體,外部不可直接修改low/highpoker_draw只負責發牌,不處理分數;gamer_add_card只更新分數,不輸出gamer_score邊界情況Poker結構體支撐多副牌、洗牌算法替換等擴展/* 重構前:200+行纏繞在main中 */
int main() {
// 初始化、發牌、玩家操作、莊家邏輯、判勝負全在這裡
}
/* 重構後:清晰的聲明式流程 */
void game_21(void) {
Poker deck; poker_init(&deck);
Gamer player = {0}, dealer = {0};
// 莊家首張
gamer_add_card(&dealer, poker_draw(&deck));
ui_show_score("莊家", gamer_score(&dealer));
// 玩家階段
gamer_add_card(&player, poker_draw(&deck));
while (ui_ask_hit()) {
gamer_add_card(&player, poker_draw(&deck));
if (gamer_score(&player) > 21) break;
}
// 莊家階段
while (gamer_score(&dealer) < 17) {
gamer_add_card(&dealer, poker_draw(&deck));
}
// 判勝負
ui_declare_winner(gamer_score(&player), gamer_score(&dealer));
}
下一篇將引入狀態機模式管理遊戲階段,實現GameState枚舉驅動的事件循環,進一步分離UI與邏輯,支撐圖形界面或網絡對戰擴展。同時將引入ctype.h標準化輸入處理,修復原代碼getAns()的緩衝區殘留缺陷。
重構不是為了炫技,而是為了讓代碼「可讀、可測、可改」。經典教材的代碼往往停留在「教會語法」層面,工程化改造才是通往專業開發的必經之路。