/* 21點遊戲:對《寫給大家看的C語言書》附錄B之21點程序的重構 */
#include
#include
#include
#include
#include
#include
/* ---------- 常數與類型定義 ---------- */
#define DECKS 1 // 副牌數,修改此處即可支援多副牌
typedef enum { NO, YES } YESNO;
typedef enum { PLAYER_WIN, DEALER_WIN, PUSH } GAME_RESULT;
typedef struct {
int cards[52 * DECKS];
int num_cards;
} POKER;
typedef struct {
int low; // A 算 1 點
int high; // A 算 11 點
int score; // 實際採用點數
int num_cards; // 手牌數 (擴展用)
} GAMER;
/* ---------- 函數宣告 ---------- */
YESNO again(const char *prompt);
void game_21(void);
void init_poker(POKER *);
int dealcard(POKER *);
void disp(int);
void getcard(GAMER *, int);
GAME_RESULT judge_winner(int dealer_score, int player_score);
void declare_winner(GAME_RESULT);
/* ---------- 主程式 ---------- */
int main(void) {
do {
system("cls"); // Windows: cls; Linux/Mac: clear
game_21();
} while (again("再玩一局 (Y/N)? ") == YES);
system("pause"); // Windows 專用,跨平台請移除或用 getchar()
return 0;
}
/* ---------- 核心邏輯實作 ---------- */
/* 發牌:從牌堆隨機取一張,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;
}
/* 顯示牌面:純輸出,無副作用 */
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 getcard(GAMER *p, int card) {
disp(card);
if (card >= 11 && card <= 13) { // J, Q, K
p->low += 10; p->high += 10;
} else if (card == 1) { // Ace
p->low += 1; p->high += 11;
} else { // 2-10
p->low += card; p->high += card;
}
p->score = (p->high <= 21) ? p->high : p->low;
p->num_cards++;
printf("總分:%d\n", p->score);
}
/* 初始化牌堆 */
void init_poker(POKER *p) {
p->num_cards = sizeof(p->cards) / sizeof(p->cards[0]);
for (int i = 0; i < p->num_cards; i++) {
p->cards[i] = i % 13 + 1;
}
}
/* 勝負判斷:純邏輯,無副作用,可單元測試 */
GAME_RESULT judge_winner(int dealer_score, int player_score) {
if (dealer_score == 21) {
return (player_score == 21) ? PUSH : DEALER_WIN;
}
if (dealer_score > 21) {
return (player_score > 21) ? PUSH : PLAYER_WIN;
}
// dealer_score < 21
if (player_score > 21) return DEALER_WIN;
if (dealer_score > player_score) return DEALER_WIN;
if (dealer_score < player_score) return PLAYER_WIN;
return PUSH;
}
/* 結果顯示 */
void declare_winner(GAME_RESULT result) {
switch(result) {
case PLAYER_WIN: puts("你贏了!\a"); break;
case DEALER_WIN: puts("你輸了。"); break;
case PUSH: puts("平局。"); break;
}
}
/* 遊戲主流程 */
void game_21(void) {
POKER poker;
GAMER player = {0}, dealer = {0};
init_poker(&poker);
srand((unsigned)time(NULL));
puts("莊家拿牌:");
getcard(&dealer, dealcard(&poker));
puts("\n你拿牌:");
getcard(&player, dealcard(&poker));
// 玩家回合
do {
getcard(&player, dealcard(&poker));
} while (again("繼續要牌 (Y/N)? ") == YES);
// 莊家回合:點數 < 17 必須要牌
puts("\n莊家繼續拿牌:");
do {
getcard(&dealer, dealcard(&poker));
} while (dealer.score < 17);
// 判斷勝負
declare_winner(judge_winner(dealer.score, player.score));
}
/* 通用 Y/N 確認:清理緩衝區、大小寫不敏感 */
YESNO again(const char *prompt) {
int c;
puts(prompt);
c = getchar();
while (getchar() != '\n'); // 清空本行剩餘字元
return (c == 'y' || c == 'Y') ? YES : NO;
}
/* ---------- 單元測試 (編譯時定義 RUN_TESTS 即可執行) ---------- */
#ifdef RUN_TESTS
void run_tests() {
assert(judge_winner(21, 20) == DEALER_WIN);
assert(judge_winner(21, 21) == PUSH);
assert(judge_winner(22, 19) == PLAYER_WIN);
assert(judge_winner(22, 23) == PUSH);
assert(judge_winner(19, 22) == DEALER_WIN);
assert(judge_winner(19, 20) == PLAYER_WIN);
assert(judge_winner(20, 19) == DEALER_WIN);
assert(judge_winner(18, 18) == PUSH);
assert(judge_winner(17, 21) == PLAYER_WIN);
printf("All judge_winner tests passed.\n");
}
int main_test(void) {
run_tests();
return 0;
}
#endif
# 標準編譯
gcc -std=c99 -Wall -O2 blackjack.c -o blackjack
# 包含單元測試編譯
gcc -std=c99 -Wall -O2 -DRUN_TESTS blackjack.c -o blackjack_test
./blackjack_test # 先跑測試,再跑遊戲
| 維度 | 原始代碼 | 重構版 | 改進原則 |
|---|---|---|---|
| 資料組織 | 10+ 全域陣列/變數 | POKER, GAMER 結構體 | 封裝、領域建模 |
| 函數設計 | 12 個強耦合函數 | 8 個高內聚函數 | 單一職責、介面分離 |
| 控制流 | 遞迴 (風險不可控) | 迴圈 (清晰、安全) | 迴圈優於遞迴 |
| 輸入處理 | scanf 留緩衝區坑 | getchar+清理/ fgets | 緩衝區衛生 |
| 核心邏輯 | 分散、含邏輯錯誤 | judge_winner 純函數 | 純函數、可測試 |
| 輸出耦合 | printf 散落各處 | declare_winner 集中 | 邏輯與表現分離 |
| 擴展性 | 幾乎為零 | 修改 DECKS 即支援多副牌 | 參數化配置 |
| 測試性 | 不可測試 | 斷言測試覆蓋核心邏輯 | 設計即可測試 |
POKER, GAMER 純資料結構,無行為judge_winner 封裝規則,可替換為不同規則集 (歐式/美式/自訂)game_21() 定義遊戲骨架,具體步驟委派給細粒度函數dealcard (命令,修改狀態) vs judge_winner (查詢,無副作用)GAMER 增加 hands[2] 支援多手牌system("cls")disp()/declare_winner() 為 SDL/Qt 繪圖這個 21 點重構系列,不只是修復一段劣質代碼,更完整示範了軟體工程的基本功:
領域建模 → 介面設計 → 純邏輯分離 → 受控副作用 → 可測試架構 → 參數化配置 → 文檔化代碼
原書代碼能跑、能教語法,但不能教工程。透過系統性重構,我們將一段「教學範例」轉化為「生產級原型」——這才是程式設計師進階的必經之路。
(全文完)