原書代碼中 initCardsScreen() 負責初始化牌堆、玩家點數、總分陣列,並提示輸入玩家姓名,職責過重且耦合全域變數。本文展示如何拆解並現代化這兩個函數。
// 原始宣告 (簡化)
void initCardsScreen(int cards[], int playerPoints[], int dealerPoints[], int total[], int *numCards);
缺陷:
init_poker()
void init_poker(POKER *p) {
p->num_cards = sizeof(p->cards) / sizeof(p->cards[0]); // 52
for (int i = 0; i < p->num_cards; i++) {
p->cards[i] = i % 13 + 1; // 1-13 循環 4 次 (4 套牌)
}
}
只做一件事:建立有序牌堆。洗牌由 dealcard() 隨機抽取隱含完成。
reset_gamer()
void reset_gamer(GAMER *g) {
g->low = g->high = g->score = 0;
}
在發第一張牌前呼叫,或直接用結構體初始化 GAMER player = {0,0,0};。
void get_player_name(char *buf, int size) {
printf("請輸入您的姓名: ");
if (fgets(buf, size, stdin)) {
buf[strcspn(buf, "\n")] = '\0'; // 移除換行
}
}
fgets() 替代 scanf(),安全讀取含空格姓名strcspn() 移除尾端換行,無殘留緩衝區問題char name[32]; get_player_name(name, sizeof(name));
// 原始
void dispTitle() {
int i;
for (i = 0; i < 40; i++) printf("*");
printf("\n Blackjack Game\n");
for (i = 0; i < 40; i++) printf("*");
}
void print_title(const char *title, int width, char border) {
for (int i = 0; i < width; i++) putchar(border);
putchar('\n');
int pad = (width - strlen(title)) / 2;
for (int i = 0; i < pad; i++) putchar(' ');
printf("%s\n", title);
for (int i = 0; i < width; i++) putchar(border);
putchar('\n');
}
// 呼叫
print_title("21點遊戲", 40, '=');
優點:可複用於不同標題、寬度、邊框字元;支援多語言標題 (UTF-8 環境下)。
// 原始片段
char getAns(char *prompt) {
char ans;
printf("%s", prompt);
scanf("%c", &ans); // 會讀到殘留換行符!
return ans;
}
char get_yes_no(const char *prompt) {
int c;
while (1) {
printf("%s", prompt);
c = getchar();
while (getchar() != '\n'); // 清空緩衝區至換行
if (c == 'y' || c == 'Y') return 'Y';
if (c == 'n' || c == 'N') return 'N';
printf("請輸入 Y 或 N\n");
}
}
// 通用單字元輸入 (可指定合法字元集)
char get_char(const char *prompt, const char *valid) {
int c;
while (1) {
printf("%s", prompt);
c = getchar();
while (getchar() != '\n');
if (strchr(valid, c)) return c;
printf("無效輸入,合法選項: %s\n", valid);
}
}
關鍵點:
getchar() 讀單字元,while(getchar()!='\n') 清理整行'Y'/'N'get_char() 可用於 H/S (Hit/Stand) 等選單
int main() {
POKER poker;
GAMER player = {0}, dealer = {0};
char name[32];
init_poker(&poker);
get_player_name(name, sizeof(name));
print_title("21點遊戲", 40, '=');
printf("歡迎, %s!\n\n", name);
srand((unsigned)time(NULL));
do {
reset_gamer(&player);
reset_gamer(&dealer);
// ... 遊戲主流程 ...
} while (get_yes_no("\n再玩一局 (Y/N)? ") == 'Y');
return 0;
}
| 函數 | 原始問題 | 重構後 |
|---|---|---|
| initCardsScreen | 5 參數、多職責、全域耦合 | 拆為 init_poker, reset_gamer, get_player_name,各司其職 |
| dispTitle | 硬編碼、不可複用 | print_title 參數化、支援國際化 |
| getAns | scanf 殘留緩衝區、無驗證 | get_yes_no/get_char 循環驗證、清理緩衝區 |
= {0}),避免單獨初始化函數get_*() 內部循環直到合法輸入,主流程無需防禦性檢查getchar() + 清理迴圈,或 fgets() + 解析,杜絕 scanf() 留坑這些看似瑣碎的改進,正是將「能跑的代碼」轉化為「可維護的軟體」的關鍵細節。下一篇將完整呈現最終可編譯執行的 21 點遊戲代碼。