位元詩人 [C 語言] 程式設計教學:以 C 語言實作花旗骰 (Craps)

C 語言
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

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

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

遊玩方式

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

接下來我們會分段說明。

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

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

在觀看程式碼前,我們先簡介一下 C 語言如何看待命令列參數。在主函式中,我們時常會看到兩個參數 argcargv

int main(int argc, char *argc[])
{
    /* Implement your code here. */
}

C 語言把命令列參數看成 C 字串陣列,參數儲存在 argv 陣列中。但 C 陣列本身沒有長度的資訊,所以會額外傳入 argc 參數,否則無法處理陣列。

接著,我們來看本範例程式中有關解析命令列參數的參考實作:

char bet;                                                          /*  1 */
bool verbose = true;  // Flag for verbose message.                 /*  2 */

// Parse command-line arguments without any library.               /*  3 */
// Run without any argument. Default to *pass* bet.                /*  4 */
if (argc == 1) {                                                   /*  5 */
    bet = PASS;                                                    /*  6 */
}                                                                  /*  7 */
// Run with one or more argument.                                  /*  8 */
else if (argc >= 2) {                                              /*  9 */
    // Print version info and exit.                                /* 10 */
    if (strcmp(argv[1], "-v") == 0 ||                              /* 11 */
        strcmp(argv[1], "--version") == 0) {                       /* 12 */
        printf("%s\n", VERSION);                                   /* 13 */
        return 0;                                                  /* 14 */
    }                                                              /* 15 */
    // Print help message and exit.                                /* 16 */
    else if (strcmp(argv[1], "-h") == 0 ||                         /* 17 */
        strcmp(argv[1], "--help") == 0) {                          /* 18 */
        printHelp();                                               /* 19 */
        return 0;                                                  /* 20 */
    }                                                              /* 21 */
    // Run in quiet mode.                                          /* 22 */
    else if (strcmp(argv[1], "-q") == 0 ||                         /* 23 */
        strcmp(argv[1], "--quiet") == 0) {                         /* 24 */
        verbose = false;                                           /* 25 */

        // Default to *pass* bet.                                  /* 26 */
        if (argc == 2) {                                           /* 27 */
            bet = PASS;                                            /* 28 */
        }                                                          /* 29 */
        // Choose either *pass* or *no pass* bet.                  /* 30 */
        else if (argc >= 3) {                                      /* 31 */
            // Choose *pass* bet.                                  /* 32 */
            if (strcmp(argv[2], "right") == 0) {                   /* 33 */
                bet = PASS;                                        /* 34 */
            }                                                      /* 35 */
            // Choose *no pass* bet.                               /* 36 */
            else if (strcmp(argv[2], "wrong") == 0) {              /* 37 */
                bet = NOT_PASS;                                    /* 38 */
            }                                                      /* 39 */
            // Invalid argument.                                   /* 40 */
            // Exit the program with both error and help message.  /* 41 */
            else {                                                 /* 42 */
                fprintf(stderr, "Wrong arguments\n");              /* 43 */
                printHelp();                                       /* 44 */
                return 1;                                          /* 45 */
            }                                                      /* 46 */
        }                                                          /* 47 */
    }                                                              /* 48 */
    // Choose *pass* bet.                                          /* 49 */
    else if (strcmp(argv[1], "right") == 0) {                      /* 50 */
        bet = PASS;                                                /* 51 */
    }                                                              /* 52 */
    // Choose *no pass* bet.                                       /* 53 */
    else if (strcmp(argv[1], "wrong") == 0) {                      /* 54 */
        bet = NOT_PASS;                                            /* 55 */
    }                                                              /* 56 */
    // Invalid argument.                                           /* 57 */
    // Exit the program with both error and help message.          /* 58 */
    else {                                                         /* 59 */
        fprintf(stderr, "Wrong arguments\n");                      /* 60 */
        printHelp();                                               /* 61 */
        return 1;                                                  /* 62 */
    }                                                              /* 63 */
}                                                                  /* 64 */

第 5 行至第 7 行為沒有額外參數的情形,這時候就使用內定的預設值。

第 9 行至第 64 行為解析一至三個參數的過程,我們分段來看。

當使用者輸入 -v--version 時,印出程式的版本號後離開本程式。這段程式碼在第 11 行至第 15 行間。

當使用者輸入 -h--help 時,印出幫助訊息後離開本程式。這段程式在第 17 行至第 21 行間。在此段程式中用到一個函式 printHelp(),只是為了減少重覆輸入程式碼,沒有用到什麼複雜的語法機制,讀者不用太擔心。我們於後續文章會介紹函式。

當使用者輸入 -q 時,有可能是:

  • -q
  • -q right
  • -q wrong
  • 誤輸入其他參數

所以我們分四個子情境來處理即可。本段程式位於第 23 行至第 48 行間。

當使用者輸入 right 時,代表使用者押 pass 。這段程式碼位於第 50 行至第 52 行間。

當使用者輪入 wrong 時,代表使用者押 no pass 。這段程式碼位於第 54 行至第 56 行間。

當使用者誤輸其他參數時,秀出幫助文件。由於我們把 printHelp() 實作成函式,就不用重新輸入相同的內容。這段程式位於第 59 行至 63 行間。

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

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

short a, b;                                                  /*  3 */
short result;                                                /*  4 */
bool over = false;                                           /*  5 */

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

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

// Craps: *no pass*. End the game.                           /* 13 */
if (comeOut== 2 || comeOut == 3 || comeOut == 12) {          /* 14 */
    if (verbose) {                                           /* 15 */
        printf("Craps\n");                                   /* 16 */
    }                                                        /* 17 */
    result = NOT_PASS;                                       /* 18 */
    over = true;                                             /* 19 */
}                                                            /* 20 */
// Natural: *pass*. End the game.                            /* 21 */
else if (comeOut == 7) {                                     /* 22 */
    if (verbose) {                                           /* 23 */
        printf("Natural\n");                                 /* 24 */
    }                                                        /* 25 */
    result = PASS;                                           /* 26 */
    over = true;                                             /* 27 */
}                                                            /* 28 */

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

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

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

用電腦模擬擲骰子的程式碼的敘述在第 7 行及第 8 行。一開始會得到介於 0 到 5 的數字,再加 1 後即會平移到 1 至 6 之間。

接下來的程式碼就是將 Craps 的 come-out roll 規則以 C 實作。我們分段來看。

如果兩個骰子加起來為 2、3、12,代表 craps ,這時候是 no pass 一方獲勝。這段程式碼位於第 14 行至第 19 行。

如果兩個骰子加起來為 7,代表 natural ,這時候是 pass 一方獲勝。這段程式碼位於第 22 行至第 28 行。

要注意我們在符合特定條件時會將 over 的狀態設為 false,這會影響到接下來的迴圈。

如果在 come-out roll 無法分勝負,就要進入 point roll 階段。我們接著實作 point roll 的部分:

short sum;                                                          /*  1 */
// Point roll                                                       /*  2 */
while (!over) {                                                     /*  3 */
    a = rand() % 6 + 1;                                             /*  4 */
    b = rand() % 6 + 1;                                             /*  5 */
    sum = a + b;                                                    /*  6 */
    // Hit: *pass*. End the game.                                   /*  7 */
    if (sum == comeOut) {                                           /*  8 */
        if (verbose) {                                              /*  9 */
            printf("Hit: %d + %d = %d\n", a, b, sum);               /* 10 */
        }                                                           /* 11 */
        result = PASS;                                              /* 12 */
        over = true;                                                /* 13 */
    }                                                               /* 14 */
    // Seven-out: *no pass*. End the game.                          /* 15 */
    else if (sum == 7) {                                            /* 16 */
        if (verbose) {                                              /* 17 */
            printf("Seven-out: %d + %d = %d\n", a, b, sum);         /* 18 */
        }                                                           /* 19 */
        result = NOT_PASS;                                          /* 20 */
        over = true;                                                /* 21 */
    }                                                               /* 22 */
    // Keep rolling.                                                /* 23 */
    else {                                                          /* 24 */
        if (verbose) {                                              /* 25 */
            printf("Got %d + %d = %d. Try again...\n", a, b, sum);  /* 26 */
        }                                                           /* 27 */
    }                                                               /* 28 */
}                                                                   /* 29 */

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

當兩個骰子的和等於 come out 時,為 pass 方獲勝。這段程式碼位於第 8 行至第 14 行。

當兩個骰子的和等於 seven out (7) 時,為 no pass 方獲勝。這段程式碼位於第 16 行至第 22 行間。

除此之外,繼續擲下一輸骰子。這段程式碼位於第 24 行至第 28 行間。

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

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

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。