首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

劣質代碼評析:《C語言入門經典》附錄B 21點遊戲(八)—— 完整最終版代碼與架構總結

     編輯:關於C語言

完整重構版 21 點遊戲 (單檔案、標準 C99、可直接編譯執行)


/* 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 即支援多副牌參數化配置
測試性不可測試斷言測試覆蓋核心邏輯設計即可測試

核心設計模式應用

  1. 資料傳輸物件 (DTO):POKER, GAMER 純資料結構,無行為
  2. 策略模式雛形:judge_winner 封裝規則,可替換為不同規則集 (歐式/美式/自訂)
  3. 模板方法:game_21() 定義遊戲骨架,具體步驟委派給細粒度函數
  4. 命令查詢分離 (CQS):dealcard (命令,修改狀態) vs judge_winner (查詢,無副作用)

遺留改進方向 (留給讀者練習)

  • 雙倍下注:在玩家首兩張牌後增加選項
  • 分牌:GAMER 增加 hands[2] 支援多手牌
  • 保險:莊家明牌為 A 時提供保險選項
  • 計分牌策略:實作 Hi-Lo 計數法 AI
  • 跨平台終端:用 ANSI 轉義碼替代 system("cls")
  • 圖形介面:替換 disp()/declare_winner() 為 SDL/Qt 繪圖

結語

這個 21 點重構系列,不只是修復一段劣質代碼,更完整示範了軟體工程的基本功:

領域建模 → 介面設計 → 純邏輯分離 → 受控副作用 → 可測試架構 → 參數化配置 → 文檔化代碼

原書代碼能跑、能教語法,但不能教工程。透過系統性重構,我們將一段「教學範例」轉化為「生產級原型」——這才是程式設計師進階的必經之路。

(全文完)

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved