本文詳細剖析發牌、顯示、計分四個核心函數的重構過程,展示如何將原始代碼中糅雜在 dispCard()、dealCard()、playerGetsCard() 等函數中的邏輯,重組為高內聚、低耦合的模組。
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;
}
設計亮點:
原始代碼對比:原書 dealCard() 使用 subDraw = rand() % *numCards,但後續 (*numCards)-- 與陣列移動邏輯分散、易錯。
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;
}
}
原則:無副作用。不修改任何狀態,不計算分數,只做「牌面值 → 字串」的映射。極易測試、複用 (如圖形界面只需替換此函數)。
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;
}
}
改良點:
int points[2] 對應 GAMER 的 low/high,語意明確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() 亦可,視專案規模決定抽象層級。
// 莊家首張
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);
流程清晰度:每行代碼語意自明,無需註解即可理解遊戲規則。
// 原書 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;
}
}
缺陷清單:
ACELOW/ACEHIGH,函數簽名不自描述playerGetsCard、dealerGetsCard 呼叫,隱含「誰的 points」語境若需支援多副牌 (賭場常用 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() **完全無需修改** —— 這就是封裝與介面設計的威力。
| 函數 | 輸入 | 輸出/副作用 | 可獨立測試 | 單一職責 |
|---|---|---|---|---|
| dealcard | POKER* | int (牌面), 修改牌堆 | 是 | 是 (只發牌) |
| disp | int | stdout | 是 | 是 (只顯示) |
| update/getcard | GAMER*, int | 修改 GAMER, stdout | 是 | 是 (抽牌邏輯) |
| declare_winner | GAMER, GAMER | stdout | 是 | 是 (判斷勝負) |
將複雜邏輯分解為可驗證的最小單元,是從「寫代碼」進化為「工程設計」的標誌。下一篇將展示玩家決策迴圈與勝負判斷的最終優化版。