C 語言程式設計教學:(案例) 花旗骰 (Craps)

PUBLISHED ON JUL 11, 2018 — PROGRAMMING

我們先暫停一般的教學文,來做一個好玩的小東西,這篇文章不影響本系列文的教學,讀者可自行視需求選讀。

如果已經熟悉這個主題,想直接觀看程式碼,可到這裡

遊玩方式

Craps (花旗骰) 是一種使用骰子 (dice) 的賭博遊戲,由於其規則簡單,常出現在基礎的程式設計教材中。一般的程式設計教材僅使用其規則,但沒有娛樂的成份,本文加入一點選邊站的功能。

本文的 Craps 玩法如下:

  • 玩家選擇 passno pass
  • 由電腦自動擲骰
  • 第一輪是 come-out roll
    • 若擲出 2、3、12,算是 *no pass*,遊戲結束 (Craps)
    • 若擲出 7,算是 *pass*,遊戲結束 (Natural)
    • 若擲出其他數字,這個數字就是我們的 point
  • 第二輪開始是 point roll
    • 若擲出 *point*,算是 *pass*,遊戲結束 (Hit)
    • 若擲出 7,算是 *no pass*,遊戲結束 (Seven-out)
    • 若擲出其他數字,則重新再擲
  • 若玩家的選擇和遊戲結果相同,則玩家勝;反之則負

一開始 no pass 的機率大一點,但隨著遊戲進行,兩邊的機率會趨於一致,所以這個遊戲還算公平。

程式展示

我們的程式是終端機程式,這類程式易於實作,適合初學者,也是我們目前為止學會的程式類型。

直接執行程式時代表我們選擇 *pass*:

$ ./craps
Come-out roll: 6 + 4 = 10
Got 5 + 6 = 11. Try again...
Got 2 + 6 = 8. Try again...
Got 3 + 5 = 8. Try again...
Got 4 + 5 = 9. Try again...
Got 5 + 6 = 11. Try again...
Got 2 + 1 = 3. Try again...
Got 1 + 2 = 3. Try again...
Got 6 + 3 = 9. Try again...
Hit: 4 + 6 = 10
The player wins

我們在這裡不採用互動式的 scanf 函式取得使用者輸入,而直接從命令列參數來操作程式,這是承襲 Unix 文化的思維。

我們也可以選擇 *no pass*:

$ ./craps wrong
Come-out roll: 2 + 6 = 8
Got 3 + 2 = 5. Try again...
Got 6 + 4 = 10. Try again...
Seven-out: 1 + 6 = 7
The player wins

本程式還提供寧靜模式 (quiet mode),僅提供最少量的訊息:

$ ./craps -q
lose

由於程式內部實作的緣故,如果要批次賭搏,每次間隔要至少一秒:

$ for i in `seq 1 10`; do ./craps -q wrong; sleep 1; done
lose
lose
win
lose
win
lose
lose
win
lose
lose

抽象思維

由於本實作的程式碼略長,我們會先用虛擬碼 (pseudocode) 來展示其觀念。虛擬碼是一種半結構化的語言,用來表示程式實作的高階抽象概念 (可看這裡)。本遊戲的虛擬碼如下:

Pass and NotPass are two game result symbols.
gameStart and gameOver are two game state symbols.

bet <- choose from either Pass or NotPass

result <- Pass
state <- gameStart

// Come-out roll.
comeOut <- roll two dices
if comeOut == 2 or comeOut == 3 or comeOut == 12 then
    // Craps.
    result <- NotPass
    state <- gameOver
else if comeOut == 7 or comeOut == 11 then
    // Natural.
    result <- Pass
    state <- gameOver
end if

// Point roll.
while state != gameOver do
    pt <- roll two dices
    
    if pt == comeOut then
        // Hit.
        result <- Pass
        state <- gameOver
    else if pt == 7 then
        // Seven-out.
        result <- NotPass
        state <- gameOver
    end if
end while

if bet == result then
    The player wins.
else
    The player loses.
end if

實作

由於程式碼略長,我們會分段展示,讀者可到這裡觀看完整版,和本文相互對照。

本文的 C 虛擬碼如下:

int main(int argc, char *argv[])
{
    // Parse command-line arguments.
    
    // Get the player's bet.
    
    // Come-out roll.
    
    // Point roll.
    
    // Report final result.
}

接下來我們會分段說明。

在 C (或 C++) 中,處理命令列參數的方式是依靠 argcargv 兩個參數;前者是一個整數,代表參數的數量,後者是一個指向 char * (pointer to char) 型別的陣列。在 C (或 C++) 中,argv 第一個值代表程式本身的名稱,第二個以後的值才代表傳入的參數。

在本實作中,由於命令列參數很少,我們這裡不用函式庫,直接操作命令列參數。我們處理命令列參數的規則如下:

  • 無參數:賭 pass
  • 一個參數
    • -v--version:印出版本訊息後離開程式
    • -h--help:印出 help 訊息後離開程式
    • -q:以寧靜模式賭 pass
    • right:賭 pass
    • wrong:賭 no pass
  • 兩個參數
    • -q right:以寧靜模式賭 pass
    • -q wrong:以寧靜模式賭 no pass

將我們的想法轉為程式碼如下:

short bet;
bool verbose = true;  // Flag for verbose message.

// Parse command-line arguments without any library.
// Run without any argument. Default to *pass* bet.
if (argc == 1) {
    bet = PASS;
}
// Run with one or more argument.
else if (argc >= 2) {
    // Print version info and exit.
    if (strcmp(argv[1], "-v") == 0 ||
        strcmp(argv[1], "--version") == 0) {
        printf("%s\n", VERSION);
        return EXIT_SUCCESS;
    }
    // Print help message and exit.
    else if (strcmp(argv[1], "-h") == 0 ||
        strcmp(argv[1], "--help") == 0) {
        printHelp();
        return EXIT_SUCCESS;
    }
    // Run in quiet mode.
    else if (strcmp(argv[1], "-q") == 0 ||
        strcmp(argv[1], "--quiet") == 0) {
        verbose = false;

        // Default to *pass* bet.
        if (argc == 2) {
            bet = PASS;
        }
        // Choose either *pass* or *no pass* bet.
        else if (argc >= 3) {
            // Choose *pass* bet.
            if (strcmp(argv[2], "right") == 0) {
                bet = PASS;
            }
            // Choose *no pass* bet.
            else if (strcmp(argv[2], "wrong") == 0) {
                bet = NOT_PASS;
            }
            // Invalid argument.
            // Exit the program with both error and help message.
            else {
                fprintf(stderr, "Wrong arguments\n");
                printHelp();
                return EXIT_FAILURE;
            }
        }
    }
    // Choose *pass* bet.
    else if (strcmp(argv[1], "right") == 0) {
        bet = PASS;
    }
    // Choose *no pass* bet.
    else if (strcmp(argv[1], "wrong") == 0) {
        bet = NOT_PASS;
    }
    // Invalid argument.
    // Exit the program with both error and help message.
    else {
        fprintf(stderr, "Wrong arguments\n");
        printHelp();
        return EXIT_FAILURE;
    }
}

在此段程式中用到一個函式 printHelp,只是為了減少重覆輸入程式碼,沒有用到什麼複雜的語法機制,讀者不用太擔心。我們於後續文章會介紹函式。

在解析命令列參數後,我們也可以得知玩家所要賭的方式。接著,實作 come-out roll:

// Init a rand seed by current system time.
srand((unsigned) time(NULL));

short a, b;
short result;
bool over = false;

// Come-out roll.
a = rand() % 6 + 1;
b = rand() % 6 + 1;
short comeOut = a + b;

if (verbose) {
    printf("Come-out roll: %d + %d = %d\n", a, b, comeOut);
}

// Craps: *no pass*. End the game.
if (comeOut== 2 || comeOut == 3 || comeOut == 12) {
    if (verbose) {
        printf("Craps\n");
    }
    result = NOT_PASS;
    over = true;
}
// Natural: *pass*. End the game.
else if (comeOut == 7) {
    if (verbose) {
        printf("Natural\n");
    }
    result = PASS;
    over = true;
}

由於實作亂數演算法相對困難,我們這裡直接使用 stdlib.h 所提供的亂數產生函式。其實電腦內沒有什麼小精靈在產生亂數,而是使用亂數演算法來產生看起來隨機的數字。一般來說,亂數函式庫的使用方式如下:

  • 設立初始種子 (seed)
  • 將該種子經亂數演算法得到一個新的數字
  • 某需另一個數字,將前一個數字做為新的種子重新計算

在本例中,我們產生種子的敘述是 srand((unsigned) time(NULL));,就是使用程式執行時的時間做為種子,由於每次執行程式的時間皆不同,故種子也會不同。如果程式要除錯時,可將亂數種子設為固定值,每次的結果就會相同。

用電腦模擬擲骰子的程式碼是 rand() % 6 + 1,一開始會得到介於 0 到 5 的數字,再加 1 後即會平移到 1 至 6 之間。

接下來的程式碼就是將 Craps 的 come-out roll 規則以 C 實作,讀者可自行閱讀。只是要注意我們在符合特定條件時會將 over 的狀態設為 false,這會影響到接下來的迴圈。

接著實作 point roll:

short sum;
// Point roll
while (!over) {
    a = rand() % 6 + 1;
    b = rand() % 6 + 1;
    sum = a + b;
    // Hit: *pass*. End the game.
    if (sum == comeOut) {
        if (verbose) {
            printf("Hit: %d + %d = %d\n", a, b, sum);
        }
        result = PASS;
        over = true;
    }
    // Seven-out: *no pass*. End the game.
    else if (sum == 7) {
        if (verbose) {
            printf("Seven-out: %d + %d = %d\n", a, b, sum);
        }
        result = NOT_PASS;
        over = true;
    }
    // Keep rolling.
    else {
        if (verbose) {
            printf("Got %d + %d = %d. Try again...\n", a, b, sum);
        }
    }
}

Point-roll 這部分的程式碼相對單純,基本上就是以 over 旗標控制程式的進行。當 overtrue 時,程式會自動結束。要注意在先前的 come-out roll 時,若符合某些特定的條件,over 會設成 true,這時迴圈不會運作。

最後則是向玩家回報遊戲成果:

// Report the game result
if (bet == result) {
    if (verbose) {
        printf("The player wins\n");
    } else {
        printf("win\n");
    }
} else {
    if (verbose) {
        printf("The player loses\n");
    } else {
        printf("lose\n");
    }
}

這部分程式碼很單純,請讀者自行閱讀。

小結

Craps 由於規則簡單,相當適合作為程式設計的練習題。如果讀者要自我練習,建議在讀完本遊戲的遊戲規則後,不要看本文的程式碼,自己試著重新實作一次。即使這種程式看似簡單,仍然可以從實作的過程中學到一些些經驗。

TAGS: C 語言, CRAPS
comments powered by Disqus