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

劣質代碼評析:《C語言入門經典》附錄B 21點遊戲(五)—— 發牌、顯示與計分模組化

     編輯:關於C語言

核心模組:dealcard、disp、update、getcard

本文詳細剖析發牌、顯示、計分四個核心函數的重構過程,展示如何將原始代碼中糅雜在 dispCard()、dealCard()、playerGetsCard() 等函數中的邏輯,重組為高內聚、低耦合的模組。

1. 發牌函數 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;
}

設計亮點:

  • 無需洗牌:每次從剩餘牌中等概率抽取,統計特性等同完美洗牌
  • O(1) 移除:用末尾元素覆蓋被取走位置,避免陣列位移
  • 單一職責:只負責「從牌堆取一張牌」,不涉及顯示、計分

原始代碼對比:原書 dealCard() 使用 subDraw = rand() % *numCards,但後續 (*numCards)-- 與陣列移動邏輯分散、易錯。

2. 顯示函數 disp() —— 純輸出無副作用


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;
    }
}

原則:無副作用。不修改任何狀態,不計算分數,只做「牌面值 → 字串」的映射。極易測試、複用 (如圖形界面只需替換此函數)。

3. 計分函數 update() —— 規則集中化


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;
    }
}

改良點:

  • 合併 J/Q/K 判斷,減少重複代碼
  • 參數用 int points[2] 對應 GAMER 的 low/high,語意明確
  • 無輸出、無條件判斷,純數學運算

4. 綜合抽牌 getcard() —— 編排顯示與計分


void getcard(GAMER *p, int card) {
    disp(card);                          // 1. 顯示牌面
    update(card, (int[2]){p->low, p->high}); // 2. 更新點數 (C99 複合字面量)
    // 3. 計算最終有效分數
    p->score = (p->high <= 21) ? p->high : p->low;
    printf("總分:%d\n", p->score);
}

關鍵技術:(int[2]){p->low, p->high} 建立臨時陣列傳給 update(),更新後再同步回 p->low/high (需配合指標或分兩步)。更清晰的寫法:


void getcard(GAMER *p, int card) {
    disp(card);
    // 直接操作結構體成員
    if (card >= 11 && card <= 13) {
        p->low += 10; p->high += 10;
    } else if (card == 1) {
        p->low += 1; p->high += 11;
    } else {
        p->low += card; p->high += card;
    }
    p->score = (p->high <= 21) ? p->high : p->low;
    printf("總分:%d\n", p->score);
}

將 update() 內聯進 getcard() 亦可,視專案規模決定抽象層級。

5. 主流程中的優雅調用


// 莊家首張
puts("莊家拿牌:");
getcard(&dealer, dealcard(&poker));

// 玩家首張
puts("\n你拿牌:");
getcard(&player, dealcard(&poker));

// 玩家回合
do {
    getcard(&player, dealcard(&poker));
} while (again("繼續要牌(Y/N)?") == YES);

// 莊家回合
puts("\n莊家繼續拿牌:");
do {
    getcard(&dealer, dealcard(&poker));
} while (dealer.score < 17);

流程清晰度:每行代碼語意自明,無需註解即可理解遊戲規則。

6. 原始代碼對比:dispCard 的混亂


// 原書 dispCard 片段
void dispCard(int cardDrawn, int points[]) {
    switch(cardDrawn) {
        case 11: puts("Jack"); points[ACELOW]+=10; points[ACEHIGH]+=10; break;
        case 12: puts("Queen"); points[ACELOW]+=10; points[ACEHIGH]+=10; break;
        case 13: puts("King"); points[ACELOW]+=10; points[ACEHIGH]+=10; break;
        case 1:  puts("Ace"); points[ACELOW]+=1; points[ACEHIGH]+=11; break;
        default: printf("%d\n",cardDrawn); points[ACELOW]+=cardDrawn; points[ACEHIGH]+=cardDrawn; break;
    }
}

缺陷清單:

  1. J/Q/K 重複代碼 3 次
  2. 顯示與計分強耦合,無法單獨測試
  3. 依賴全域常數 ACELOW/ACEHIGH,函數簽名不自描述
  4. 被 playerGetsCard、dealerGetsCard 呼叫,隱含「誰的 points」語境

7. 進階:多副牌支援

若需支援多副牌 (賭場常用 6-8 副),只需修改 POKER 初始化:


#define DECKS 6
typedef struct {
    int cards[52 * DECKS];
    int num_cards;
} POKER;

void init_poker(POKER *p) {
    p->num_cards = 52 * DECKS;
    for (int i = 0; i < p->num_cards; i++) {
        p->cards[i] = i % 13 + 1;
    }
}

dealcard()、getcard()、declare_winner() **完全無需修改** —— 這就是封裝與介面設計的威力。

總結:模組化檢查表

函數輸入輸出/副作用可獨立測試單一職責
dealcardPOKER*int (牌面), 修改牌堆是是 (只發牌)
dispintstdout是是 (只顯示)
update/getcardGAMER*, int修改 GAMER, stdout是是 (抽牌邏輯)
declare_winnerGAMER, GAMERstdout是是 (判斷勝負)

將複雜邏輯分解為可驗證的最小單元,是從「寫代碼」進化為「工程設計」的標誌。下一篇將展示玩家決策迴圈與勝負判斷的最終優化版。

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