【在主画面加入捷径】
       
【选择语系】
繁中 简中

[C 语言] 程序设计教学:(案例) 花旗骰 (Craps)

【赞助商连结】

    我们先暂停一般的教学文,来做一个好玩的小东西,这篇文章不影响本系列文的教学,读者可自行视需求选读。

    如果已经熟悉这个主题,想直接观看程序代码,可到这里

    游玩方式

    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 由于守则简单,相当适合作为程序设计的练习题。如果读者要自我练习,建议在读完本游戏的游戏守则后,不要看本文的程序代码,自己试着重新实现一次。即使这种程序看似简单,仍然可以从实现的过程中学到一些些经验。

    【赞助商连结】