目錄

興大物件導向程式設計 Assignment Ch07 參考詳解

前言與使用說明

  • 請自行消化吸收一遍,能力允許的話對程式做一些修改,不要照抄。
  • 我寫的程式碼不是最佳實踐。會盡可能會用課本給的程式碼「修改」,而不是重寫。
  • 由於程式碼稍微長一點,我會把對程式碼的修改分步驟貼上,修改的部分用 //程式註解 解釋。

題目參考

本篇文章為中興大學物件導向程式設計課程的 Assignment Ch07 的詳解。

題目的部分可參考本站另一篇文章(點擊前往)
不過本篇文章已把題目 + 答案整合在一起了。

題目 7.18 (Game of Craps)

Write an application that runs 1,000,000 games of craps (Fig. 6.8) and answers the following questions:

  • a) How many games are won on the first roll, second roll, …,twentieth roll and after the twentieth roll?

  • b) How many games are lost on the first roll, second roll, …,twentieth roll and after the twentieth roll?

  • c) What are the chances of winning at craps? [Note: You should discover that craps is one of the fairest casino games. What do you suppose this means?]

  • d) What is the average length of a game of craps?

  • e) Do the chances of winning improve with the length of the game?

Fig.6.8(點擊展開)

7.18 參考解答

Craps 遊戲介紹

題目中題到了 Craps,相信品學兼優的各位可能並不了解這個遊戲。

Craps 又稱花旗骰,是賭場中常會出現的一種賭博遊戲。玩家需要擲兩個骰子(點數為 1 到 6),遊戲的勝負由點數和決定:

  • 若第一次兩骰的點數和為 7 或 11:玩家勝。
  • 若第一次兩骰的點數和為 2、3 或 12:玩家輸。
  • 若第一次兩骰的點數和為其它點數,則記錄這個點數和,然後繼續擲骰。
    • 若在之後的擲骰中,玩家擲到點數和等於第一次的點數和:玩家勝。
    • 若玩家此時擲到點數和為 7,玩家輸。
    • 若玩家擲到其它點數,則直接繼續擲。

關於 Craps 這個遊戲的規則與各種程式語言的實作方法,可參考這個網頁(超連結)

獲取 7.18 程式碼

知道基本的遊戲規則後,既然題目已經給我們參考程式碼,那就直接拿來用。

首先我們把 Fig.6.8 中的程式碼找出來。在學校電腦 C 槽的 OOP_class 資料夾中有,我這裡直接放程式碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// Fig. 6.8: Craps.java
// Craps class simulates the dice game craps.
import java.security.SecureRandom;

public class Craps {
    // create secure random number generator for use in method rollDice
    private static final SecureRandom randomNumbers = new SecureRandom();

    // enum type with constants that represent the game status
    private enum Status {
        CONTINUE, WON, LOST
    };

    // constants that represent common rolls of the dice
    private static final int SNAKE_EYES = 2;
    private static final int TREY = 3;
    private static final int SEVEN = 7;
    private static final int YO_LEVEN = 11;
    private static final int BOX_CARS = 12;

    // plays one game of craps
    public static void main(String[] args) {
        int myPoint = 0; // point if no win or loss on first roll
        Status gameStatus; // can contain CONTINUE, WON or LOST

        int sumOfDice = rollDice(); // first roll of the dice

        // determine game status and point based on first roll
        switch (sumOfDice) {
            case SEVEN: // win with 7 on first roll
            case YO_LEVEN: // win with 11 on first roll
                gameStatus = Status.WON;
                break;
            case SNAKE_EYES: // lose with 2 on first roll
            case TREY: // lose with 3 on first roll
            case BOX_CARS: // lose with 12 on first roll
                gameStatus = Status.LOST;
                break;
            default: // did not win or lose, so remember point
                gameStatus = Status.CONTINUE; // game is not over
                myPoint = sumOfDice; // remember the point
                System.out.printf("Point is %d%n", myPoint);
                break;
        }

        // while game is not complete
        while (gameStatus == Status.CONTINUE) { // not WON or LOST
            sumOfDice = rollDice(); // roll dice again

            // determine game status
            if (sumOfDice == myPoint) { // win by making point
                gameStatus = Status.WON;
            } else {
                if (sumOfDice == SEVEN) { // lose by rolling 7 before point
                    gameStatus = Status.LOST;
                }
            }
        }

        // display won or lost message
        if (gameStatus == Status.WON) {
            System.out.println("Player wins");
        } else {
            System.out.println("Player loses");
        }
    }

    // roll dice, calculate sum and display results
    public static int rollDice() {
        // pick random die values
        int die1 = 1 + randomNumbers.nextInt(6); // first die roll
        int die2 = 1 + randomNumbers.nextInt(6); // second die roll

        int sum = die1 + die2; // sum of die values

        // display results of this roll
        System.out.printf("Player rolled %d + %d = %d%n", die1, die2, sum);

        return sum;
    }
}

/**************************************************************************
 * (C) Copyright 1992-2018 by Deitel & Associates, Inc. and *
 * Pearson Education, Inc. All Rights Reserved. *
 * *
 * DISCLAIMER: The authors and publisher of this book have used their *
 * best efforts in preparing the book. These efforts include the *
 * development, research, and testing of the theories and programs *
 * to determine their effectiveness. The authors and publisher make *
 * no warranty of any kind, expressed or implied, with regard to these *
 * programs or to the documentation contained in these books. The authors *
 * and publisher shall not be liable in any event for incidental or *
 * consequential damages in connection with, or arising out of, the *
 * furnishing, performance, or use of these programs. *
 *************************************************************************/

把這個程式拿去編譯執行得到的結果如下:
/nchu-java-oop-assignment-ch07-solution/EX_7_18_book_results.webp

7.18 題幹

Write an application that runs 1,000,000 games of craps (Fig. 6.8) and answers the following questions

注意看剛剛程式碼的執行結果,會發現雖然玩家投了許多次骰子,但最終只會有一個玩家贏/輸的結果,也就是只算是一局遊戲。

根據題幹說明,我們需要讓程式執行 1,000,000 次的 Craps 遊戲,然後進行統計以回答選項的問題。所以我們把原本放在 main 方法裡的大部分邏輯都搬到外面來,寫成一個新的方法叫作 playOnceGame,然後在 main 方法裡用迴圈乎叫:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.security.SecureRandom;

public class EX_7_18 {
    private static final SecureRandom randomNumbers = new SecureRandom();

    private enum Status {
        CONTINUE, WON, LOST
    };

    private static final int SNAKE_EYES = 2;
    private static final int TREY = 3;
    private static final int SEVEN = 7;
    private static final int YO_LEVEN = 11;
    private static final int BOX_CARS = 12;

    // main 方法
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) { // 測試玩三局 Craps 遊戲
            playOnceGame();
        }
    }

    // 把遊戲過程包成 playOnceGame 方法
    public static void playOnceGame() {
        int myPoint = 0;
        Status gameStatus;
        int sumOfDice = rollDice();
        switch (sumOfDice) {
            case SEVEN:
            case YO_LEVEN:
                gameStatus = Status.WON;
                break;
            case SNAKE_EYES:
            case TREY:
            case BOX_CARS:
                gameStatus = Status.LOST;
                break;
            default:
                gameStatus = Status.CONTINUE;
                myPoint = sumOfDice;
                System.out.printf("Point is %d%n", myPoint);
                break;
        }
        while (gameStatus == Status.CONTINUE) {
            sumOfDice = rollDice();
            if (sumOfDice == myPoint) {
                gameStatus = Status.WON;
            } else {
                if (sumOfDice == SEVEN) {
                    gameStatus = Status.LOST;
                }
            }
        }
        if (gameStatus == Status.WON) {
            System.out.println("Player wins");
        } else {
            System.out.println("Player loses");
        }
    }

    public static int rollDice() {
        int die1 = 1 + randomNumbers.nextInt(6);
        int die2 = 1 + randomNumbers.nextInt(6);
        int sum = die1 + die2;
        System.out.printf("Player rolled %d + %d = %d%n", die1, die2, sum);
        return sum;
    }
}

7.18 a 選項

a. How many games are won on the first roll, second roll, …,twentieth roll and after the twentieth roll?

這個選項需要我們統計在這 1,000,000 局遊戲裡面:

  • 多少局遊戲玩家會在第一次擲骰子時獲勝?
  • 多少局遊戲玩家會在第二次擲骰子時獲勝?
  • …以此類推
  • 多少局遊戲玩家會在第二十次擲骰子時獲勝?
  • 多少局遊戲玩家會在第二十次以後(不含)擲骰子才獲勝?

為了進行統計,首先我們要把程式中印出 "Player rolled 2 + 2 = 4" 之類的程式刪除,改為讓 playOnceGame 方法回傳勝負結果及是在第幾輪獲勝(否則畫面會太雜亂)。然後在 main 方法中統計,並在 1,000,000 局遊戲都結束後印出:

  • playOnceGame 回傳
    • 回傳 -1 代表玩家輸,不進行統計
    • 回傳不為 -1 的數字 x 代表玩家在第 x 輪獲勝,進行統計
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.security.SecureRandom;

public class EX_7_18 {
    private static final SecureRandom randomNumbers = new SecureRandom();

    private enum Status {
        CONTINUE, WON, LOST
    };

    private static final int SNAKE_EYES = 2;
    private static final int TREY = 3;
    private static final int SEVEN = 7;
    private static final int YO_LEVEN = 11;
    private static final int BOX_CARS = 12;

    public static void main(String[] args) {
        int[] winTimes = new int[21];
        int winAt = 0;
        for (int i = 0; i < 1000000; i++) { // 執行 1000000 遍
            winAt = playOnceGame(); // winAt 記錄 playOnceGame 回傳的第幾次投骰結束遊戲
            if (1 <= winAt && winAt <= 20) {
                winTimes[winAt - 1]++;
            } else if (winAt > 20) {
                winTimes[20]++; // winTimes[20] 儲存經過 20 次投骰後才結束遊戲的局數
            }
        }
        for (int i = 0; i < 20; i++) {
            System.out.printf("在第 %d 次投骰玩家會獲勝的局數有:%d 局\n", i + 1, winTimes[i]);
        }
        System.out.printf("在第 20 次以後(不含)投骰玩家會獲勝的局數有:%d 局\n", winTimes[20]);
    }

    public static int playOnceGame() {
        int myPoint = 0;
        Status gameStatus;
        int totalPlayTimes = 1; // 統計骰幾次才結束遊戲
        int sumOfDice = rollDice();
        switch (sumOfDice) {
            case SEVEN:
            case YO_LEVEN:
                gameStatus = Status.WON;
                break;
            case SNAKE_EYES:
            case TREY:
            case BOX_CARS:
                gameStatus = Status.LOST;
                break;
            default:
                gameStatus = Status.CONTINUE;
                myPoint = sumOfDice;
                break;
        }
        while (gameStatus == Status.CONTINUE) {
            sumOfDice = rollDice();
            totalPlayTimes++;
            if (sumOfDice == myPoint) {
                gameStatus = Status.WON;
            } else {
                if (sumOfDice == SEVEN) {
                    gameStatus = Status.LOST;
                }
            }
        }
        if (gameStatus == Status.WON) {
            return totalPlayTimes;
        } else {
            return -1;
        }
    }

    public static int rollDice() {
        int die1 = 1 + randomNumbers.nextInt(6);
        int die2 = 1 + randomNumbers.nextInt(6);
        int sum = die1 + die2;
        return sum;
    }
}

執行結果:

/nchu-java-oop-assignment-ch07-solution/EX_7_18_A_results.webp

7.18 b 選項

How many games are lost on the first roll, second roll, …,twentieth roll and after the twentieth roll?

這個選項跟 a 選項問的內容差異不大,只是把要統計的對象從玩家變成玩家

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.security.SecureRandom;

public class EX_7_18 {
    private static final SecureRandom randomNumbers = new SecureRandom();

    private enum Status {
        CONTINUE, WON, LOST
    };

    private static final int SNAKE_EYES = 2;
    private static final int TREY = 3;
    private static final int SEVEN = 7;
    private static final int YO_LEVEN = 11;
    private static final int BOX_CARS = 12;

    public static void main(String[] args) {
        int[] loseTimes = new int[21];
        int loseAt = 0;
        for (int i = 0; i < 1000000; i++) {
            loseAt = playOnceGame(); // loseAt 記錄 playOnceGame 回傳的第幾次投骰結束遊戲
            if (1 <= loseAt && loseAt <= 20) {
                loseTimes[loseAt - 1]++;
            } else if (loseAt > 20) {
                loseTimes[20]++; // loseTimes[20] 儲存經過 20 次投骰後才結束遊戲的局數
            }
        }
        for (int i = 0; i < 20; i++) {
            System.out.printf("在第 %d 次投骰玩家會輸的局數有:%d 局\n", i + 1, loseTimes[i]); // 把輸出的文字改為統計輸的
        }
        System.out.printf("在第 20 次以後(不含)投骰玩家會輸的局數有:%d 局\n", loseTimes[20]); // 把輸出的文字改為統計輸的
    }

    public static int playOnceGame() {
        int myPoint = 0;
        Status gameStatus;
        int totalPlayTimes = 1; // 統計骰幾次才結束遊戲
        int sumOfDice = rollDice();
        switch (sumOfDice) {
            case SEVEN:
            case YO_LEVEN:
                gameStatus = Status.WON;
                break;
            case SNAKE_EYES:
            case TREY:
            case BOX_CARS:
                gameStatus = Status.LOST;
                break;
            default:
                gameStatus = Status.CONTINUE;
                myPoint = sumOfDice;
                break;
        }
        while (gameStatus == Status.CONTINUE) {
            sumOfDice = rollDice();
            totalPlayTimes++;
            if (sumOfDice == myPoint) {
                gameStatus = Status.WON;
            } else {
                if (sumOfDice == SEVEN) {
                    gameStatus = Status.LOST;
                }
            }
        }
        if (gameStatus == Status.WON) {
            return -1; // 改成回傳 -1 代表玩家贏,不統計
        } else {
            return totalPlayTimes; // 若玩家輸,回傳在第幾次投骰結束遊戲
        }
    }

    public static int rollDice() {
        int die1 = 1 + randomNumbers.nextInt(6);
        int die2 = 1 + randomNumbers.nextInt(6);
        int sum = die1 + die2;
        return sum;
    }
}

執行結果:

/nchu-java-oop-assignment-ch07-solution/EX_7_18_B_results.webp

7.18 c 選項

What are the chances of winning at craps? [Note: You should discover that craps is one of the fairest casino games. What do you suppose this means?]

這題要求兩個問題:

  1. Craps 遊戲玩家的勝率為多少?
  2. Craps 屬於相對較公平的遊戲,你認為這代表什麼?

因為我們知道勝率的計算方法:

$$ 勝率 = \frac{獲勝局數}{總遊玩局數} $$

所以我們可以修改一下程式,這次不輸出輸贏的統計結果,而是加總後算出勝率。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.security.SecureRandom;

public class EX_7_18 {
    private static final SecureRandom randomNumbers = new SecureRandom();

    private enum Status {
        CONTINUE, WON, LOST
    };

    private static final int SNAKE_EYES = 2;
    private static final int TREY = 3;
    private static final int SEVEN = 7;
    private static final int YO_LEVEN = 11;
    private static final int BOX_CARS = 12;

    public static void main(String[] args) {
        double totalPlayTimes = 1000000;
        double winTimes = 0;
        for (int i = 0; i < totalPlayTimes; i++) {
            if (playOnceGame()) { // 當回傳為 true 代表贏了
                winTimes++;
            }
        }
        System.out.printf("勝率 = %f", winTimes / totalPlayTimes);
    }

    public static boolean playOnceGame() { // 改為回傳布林值,true 代表贏、false 代表輸
        int myPoint = 0;
        Status gameStatus;
        int sumOfDice = rollDice();
        switch (sumOfDice) {
            case SEVEN:
            case YO_LEVEN:
                gameStatus = Status.WON;
                break;
            case SNAKE_EYES:
            case TREY:
            case BOX_CARS:
                gameStatus = Status.LOST;
                break;
            default:
                gameStatus = Status.CONTINUE;
                myPoint = sumOfDice;
                break;
        }
        while (gameStatus == Status.CONTINUE) {
            sumOfDice = rollDice();
            if (sumOfDice == myPoint) {
                gameStatus = Status.WON;
            } else {
                if (sumOfDice == SEVEN) {
                    gameStatus = Status.LOST;
                }
            }
        }
        if (gameStatus == Status.WON) {
            return true; // 贏就回傳 true
        } else {
            return false; // 輸就回傳 false
        }
    }

    public static int rollDice() {
        int die1 = 1 + randomNumbers.nextInt(6);
        int die2 = 1 + randomNumbers.nextInt(6);
        int sum = die1 + die2;
        return sum;
    }
}

執行結果只有一行:

1
勝率 = 0.492374

從勝率 = 0.492374 可以看出來,作為一個賭博遊戲來說,已經是相當接近 0.5,也就是相對很公平了。


那麼這代表什麼呢?

我認為既然這個遊戲已經是相對很公平的賭場遊戲,勝率卻又還是不到 0.5,這代表其它的賭場遊戲對玩家來說期望值都相當不理想。

我們在高中學習數學的時候都知道許多遊戲都可以計算出期望值、也知道賭場的遊戲都經過精心設計,期望值一定是負的。但當真正進入賭場、進入投資領域時很多人卻忘記這一點,白白把自己的錢送出去。

還是那句話:「十賭九輸」,沒經過精心計算就妄想靠運氣賺錢是很不實際的行為。另外如果你對期望值和資金管理、交易之間的關係有興趣,可以參考我的這篇交易聖經閱讀心得

7.18 d 選項

What is the average length of a game of craps?

這個選項要請我們計算出 Craps 遊戲的平均長度(也就是平均要幾次投骰幾次遊戲會結束)。

那麼我們應該可以把每局遊戲投了幾次骰子全部加起來,再除以總局數得到平均,也就是:

$$ Craps\ 遊戲平均長度 = \frac{\sum{每局遊戲投骰幾次結束}}{遊戲遊玩總局數} $$

寫成 Java 程式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.security.SecureRandom;

public class EX_7_18 {
    private static final SecureRandom randomNumbers = new SecureRandom();

    private enum Status {
        CONTINUE, WON, LOST
    };

    private static final int SNAKE_EYES = 2;
    private static final int TREY = 3;
    private static final int SEVEN = 7;
    private static final int YO_LEVEN = 11;
    private static final int BOX_CARS = 12;

    public static void main(String[] args) {
        double totalPlayTimes = 1000000;
        double totalThrowDiceTimes = 0;
        for (int i = 0; i < totalPlayTimes; i++) {
            totalThrowDiceTimes += playOnceGame();
        }
        System.out.printf("Craps 遊戲平均長度 = %f 次投骰。", totalThrowDiceTimes / totalPlayTimes);
    }

    public static int playOnceGame() { // 改為回傳投了幾次骰子才結束(不論玩家是輸還是贏)
        int playTimes = 1; // 記錄投骰總次數
        int myPoint = 0;
        Status gameStatus;
        int sumOfDice = rollDice();
        switch (sumOfDice) {
            case SEVEN:
            case YO_LEVEN:
                gameStatus = Status.WON;
                break;
            case SNAKE_EYES:
            case TREY:
            case BOX_CARS:
                gameStatus = Status.LOST;
                break;
            default:
                gameStatus = Status.CONTINUE;
                myPoint = sumOfDice;
                break;
        }
        while (gameStatus == Status.CONTINUE) {
            playTimes++; // 還要繼續,總投骰次數++
            sumOfDice = rollDice();
            if (sumOfDice == myPoint) {
                gameStatus = Status.WON;
            } else {
                if (sumOfDice == SEVEN) {
                    gameStatus = Status.LOST;
                }
            }
        }
        return playTimes; // 不論贏或輸都回傳 playTimes
    }

    public static int rollDice() {
        int die1 = 1 + randomNumbers.nextInt(6);
        int die2 = 1 + randomNumbers.nextInt(6);
        int sum = die1 + die2;
        return sum;
    }
}

執行結果只有一行:

1
Craps 遊戲平均長度 = 3.377009 次投骰。

7.18 e 選項

Do the chances of winning improve with the length of the game?

$$ \begin{aligned} \because\ &在第\ i\ 局贏 = 一直玩到第\ i-1\ 局都未結束,並且玩家贏在第\ i\ 局 \newline &在第\ i\ 局輸 = 一直玩到第\ i-1\ 局都未結束,並且玩家輸在第\ i\ 局 \newline \therefore\ &P(第\ i\ 局玩家勝利 \mid 第\ i-1\ 局未結束遊戲) = \frac{第\ i\ 局贏}{第\ i\ 局贏+第\ i\ 局輸} \end{aligned} $$

用這個式子我們可以把每一局玩家的勝率計算出來,然後把計算的結果印出來觀察即可。

這裡用 a 選項的程式做修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import java.security.SecureRandom;

public class EX_7_18 {
    private static final SecureRandom randomNumbers = new SecureRandom();

    private enum Status {
        CONTINUE, WON, LOST
    };

    private static final int SNAKE_EYES = 2;
    private static final int TREY = 3;
    private static final int SEVEN = 7;
    private static final int YO_LEVEN = 11;
    private static final int BOX_CARS = 12;

    public static void main(String[] args) {
        int[] winTimes = new int[21];
        int[] loseTimes = new int[21];
        int endAt = 0; // 遊戲結束在第 endAt 局
        for (int i = 0; i < 1000000; i++) {
            endAt = playOnceGame();
            if (endAt >= 0) { // 玩家贏
                if (endAt > 20) {
                    winTimes[20]++;
                } else {
                    winTimes[endAt - 1]++;
                }
            } else { // 玩家輸
                endAt = -endAt; // 把回傳值翻正
                if (endAt > 20) {
                    loseTimes[20]++;
                } else {
                    loseTimes[endAt - 1]++;
                }
            }
        }
        for (int i = 0; i < 20; i++) {
            System.out.printf("玩家在第 %d 次投骰獲勝的機率 = %f\n", i + 1,
                    (double) winTimes[i] / ((double) winTimes[i] + loseTimes[i]));
        }
    }

    // 回傳值若為正 i,代表在第 i 局玩家贏
    // 回傳值若為負 i,代表在第 i 局玩家輸
    public static int playOnceGame() {
        int myPoint = 0;
        Status gameStatus;
        int totalPlayTimes = 1; // 統計骰幾次才結束遊戲
        int sumOfDice = rollDice();
        switch (sumOfDice) {
            case SEVEN:
            case YO_LEVEN:
                gameStatus = Status.WON;
                break;
            case SNAKE_EYES:
            case TREY:
            case BOX_CARS:
                gameStatus = Status.LOST;
                break;
            default:
                gameStatus = Status.CONTINUE;
                myPoint = sumOfDice;
                break;
        }
        while (gameStatus == Status.CONTINUE) {
            sumOfDice = rollDice();
            totalPlayTimes++;
            if (sumOfDice == myPoint) {
                gameStatus = Status.WON;
            } else {
                if (sumOfDice == SEVEN) {
                    gameStatus = Status.LOST;
                }
            }
        }
        if (gameStatus == Status.WON) {
            return totalPlayTimes;
        } else {
            return -totalPlayTimes;
        }
    }

    public static int rollDice() {
        int die1 = 1 + randomNumbers.nextInt(6);
        int die2 = 1 + randomNumbers.nextInt(6);
        int sum = die1 + die2;
        return sum;
    }
}

執行的結果:

/nchu-java-oop-assignment-ch07-solution/EX_7_18_E_results.webp

題目 7.30 (Card Shuffling and Dealing)

Modify Fig. 7.11 to deal a five-card poker hand. Then modify class DeckOfCards of Fig. 7.10 to include methods that determine whether a hand contains:

  • a) a pair
  • b) two pairs
  • c) three of a kind (e.g., three jacks)
  • d) four of a kind (e.g., four aces)
  • e) a flush (i.e., all five cards of the same suit)
  • f) a straight (i.e., five cards of consecutive face values)
  • g) a full house (i.e., two cards of one face value and three cards of another face value)

[Hint: Add methods getFace and getSuit to class Card of Fig. 7.9.]

Fig.7.11(點擊展開)
Fig.7.10(點擊展開)
Fig.7.9(點擊展開)

7.30 參考解答

撲克牌

face數字A, 2, 3, 4 ...
suit 則為花色Hearts, Diamonds, Clubs, Spades

關於撲克牌的牌型資訊可以參考維基百科

獲取 7.30 程式碼

這題我們會需要用到 Fig.7.11, Fig.7.10, 和 Fig.7.9 這三張圖中的程式碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Fig. 7.11: DeckOfCardsTest.java
// Card shuffling and dealing.

public class DeckOfCardsTest {
   // execute application
   public static void main(String[] args) {
      DeckOfCards myDeckOfCards = new DeckOfCards();
      myDeckOfCards.shuffle(); // place Cards in random order
      
      // print all 52 Cards in the order in which they are dealt
      for (int i = 1; i <= 52; i++) {
         // deal and display a Card
         System.out.printf("%-19s", myDeckOfCards.dealCard());

         if (i % 4 == 0) { // output a newline after every fourth card
            System.out.println();
         } 
      } 
   } 
} 



/**************************************************************************
 * (C) Copyright 1992-2018 by Deitel & Associates, Inc. and               *
 * Pearson Education, Inc. All Rights Reserved.                           *
 *                                                                        *
 * DISCLAIMER: The authors and publisher of this book have used their     *
 * best efforts in preparing the book. These efforts include the          *
 * development, research, and testing of the theories and programs        *
 * to determine their effectiveness. The authors and publisher make       *
 * no warranty of any kind, expressed or implied, with regard to these    *
 * programs or to the documentation contained in these books. The authors *
 * and publisher shall not be liable in any event for incidental or       *
 * consequential damages in connection with, or arising out of, the       *
 * furnishing, performance, or use of these programs.                     *
 *************************************************************************/
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Fig. 7.10: DeckOfCards.java
// DeckOfCards class represents a deck of playing cards.
import java.security.SecureRandom;

public class DeckOfCards {
   // random number generator
   private static final SecureRandom randomNumbers = new SecureRandom();
   private static final int NUMBER_OF_CARDS = 52; // constant # of Cards

   private Card[] deck = new Card[NUMBER_OF_CARDS]; // Card references
   private int currentCard = 0; // index of next Card to be dealt (0-51)

   // constructor fills deck of Cards
   public DeckOfCards() {
      String[] faces = {"Ace", "Deuce", "Three", "Four", "Five", "Six",
         "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"};    
      String[] suits = {"Hearts", "Diamonds", "Clubs", "Spades"};      

      // populate deck with Card objects                   
      for (int count = 0; count < deck.length; count++) {  
         deck[count] =                                     
            new Card(faces[count % 13], suits[count / 13]);
      }                                                    
   } 

   // shuffle deck of Cards with one-pass algorithm
   public void shuffle() {
      // next call to method dealCard should start at deck[0] again
      currentCard = 0; 

      // for each Card, pick another random Card (0-51) and swap them
      for (int first = 0; first < deck.length; first++) {
         // select a random number between 0 and 51 
         int second = randomNumbers.nextInt(NUMBER_OF_CARDS);

         // swap current Card with randomly selected Card
         Card temp = deck[first];   
         deck[first] = deck[second];
         deck[second] = temp;       
      } 
   } 

   // deal one Card
   public Card dealCard() {
      // determine whether Cards remain to be dealt
      if (currentCard < deck.length) {
         return deck[currentCard++]; // return current Card in array
      } 
      else {
         return null; // return null to indicate that all Cards were dealt
      } 
   } 
} 



/**************************************************************************
 * (C) Copyright 1992-2018 by Deitel & Associates, Inc. and               *
 * Pearson Education, Inc. All Rights Reserved.                           *
 *                                                                        *
 * DISCLAIMER: The authors and publisher of this book have used their     *
 * best efforts in preparing the book. These efforts include the          *
 * development, research, and testing of the theories and programs        *
 * to determine their effectiveness. The authors and publisher make       *
 * no warranty of any kind, expressed or implied, with regard to these    *
 * programs or to the documentation contained in these books. The authors *
 * and publisher shall not be liable in any event for incidental or       *
 * consequential damages in connection with, or arising out of, the       *
 * furnishing, performance, or use of these programs.                     *
 *************************************************************************/
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Fig. 7.9: Card.java
// Card class represents a playing card.

public class Card {
   private final String face; // face of card ("Ace", "Deuce", ...)
   private final String suit; // suit of card ("Hearts", "Diamonds", ...)

   // two-argument constructor initializes card's face and suit
   public Card(String cardFace, String cardSuit) {
      this.face = cardFace; // initialize face of card
      this.suit = cardSuit; // initialize suit of card
   } 

   // return String representation of Card
   public String toString() {             
      return face + " of " + suit;        
   }                
} 



/**************************************************************************
 * (C) Copyright 1992-2018 by Deitel & Associates, Inc. and               *
 * Pearson Education, Inc. All Rights Reserved.                           *
 *                                                                        *
 * DISCLAIMER: The authors and publisher of this book have used their     *
 * best efforts in preparing the book. These efforts include the          *
 * development, research, and testing of the theories and programs        *
 * to determine their effectiveness. The authors and publisher make       *
 * no warranty of any kind, expressed or implied, with regard to these    *
 * programs or to the documentation contained in these books. The authors *
 * and publisher shall not be liable in any event for incidental or       *
 * consequential damages in connection with, or arising out of, the       *
 * furnishing, performance, or use of these programs.                     *
 *************************************************************************/

把這三個 .java 檔放到同一個專案裡面後,執行的結果如下:

/nchu-java-oop-assignment-ch07-solution/EX_7_30_book_results.webp

7.30 題幹

Modify Fig.7.11 to deal a five-card poker hand. Then modify class DeckOfCards of Fig.7.10 to include methods that determine whether a hand contains: … 略

[Hint: Add methods getFace and getSuit to class Card of Fig. 7.9.]

題幹告訴我們可以用它給的程式碼做修改,並且還提示我們要在 Card 新增 getFacegetSuit 兩個 method

所以我們首先可以修改 Card 類備用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Fig. 7.9: Card.java

public class Card {

    // face 和 suit 是 private 變數
    private final String face;
    private final String suit;

    public Card(String cardFace, String cardSuit) {
        this.face = cardFace;
        this.suit = cardSuit;
    }

    public String toString() {
        return face + " of " + suit;
    }

    // 新增 getFace 和 getSuit
    public String getFace() {
        return this.face;
    }

    public String getSuit() {
        return this.suit;
    }
}

我們在此處假定程式只需要判斷每一種牌型存在(也就是說,同時具備 pairtwo pair 屬性的話,兩者皆為 true)。

7.30 a 選項

a) a pair

我們實作一個 isPair 方法來檢查牌組是否為 pair

註:pair 的定義是具有兩張以上同樣 face 的牌。
此處只貼上實作 isPair 方法的部分。

1
2
3
4
5
6
7
8
9
public boolean isPair(Card... cards) {
    for (int i = 0; i < cards.length - 1; i++) {
        for (int j = i + 1; j < cards.length; j++) {
            if (cards[i].getFace().equals(cards[j].getFace()))
                return true;
        }
    }
    return false;
}

針對幾行作解釋:

  • Card... cards(你沒看錯,... 不是省略的意思,真的是點點點)

這種語法可以讓 Java 的方法的參數更加靈活,使用這個語法作為參數允許方法:

  • 接收一個類型為 Card[] 的 Array
  • 接收數個(拆開來傳遞)Card 的物件(也就是單張撲克牌卡片)
  • 不傳入任何 Card

如果傳入的資料是數個拆開來傳遞的 Card 物件,Java 會把它們組成一個 Array,名稱是 Card... cards 後方的 cards

  • equals:比較兩陣列中的元素是否完全相等。關於 equals 的用法請點我(超連結)

7.30 b 選項

b) two pairs

我們實作一個 isTwoPairs 方法來檢查牌組是否為 Two Pairs

註:Two Pairs 的定義是具有兩組不同 facepair
此處只貼上實作 isTwoPairs 方法的部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean isTwoPairs(Card... cards) {
    Card firstPair = null;
    Card secondPair = null;

    for (int i = 0; i < cards.length - 1; i++) {
        for (int j = i + 1; j < cards.length; j++) {
            if (cards[i].getFace().equals(cards[j].getFace())) {
                firstPair = cards[i];
                break;
            }
        }
    }

    for (int i = 0; i < cards.length - 1; i++) {
        for (int j = i + 1; j < cards.length; j++) {
            if (cards[i].getFace().equals(cards[j].getFace()) && !(cards[i].getFace().equals(firstPair.getFace())))
                secondPair = cards[i];
        }
    }

    return firstPair != null && secondPair != null;
}

b 選項我們使用 firstPairsecondPair 儲存找到的第一組 pair 跟第二組 pair

在尋找第二組 pair 的時候,我們有指定 !(cards[i].getFace().equals(firstPair.getFace())),所以我們找到的兩個 pair 不可能是同樣的 face

firstPairsecondPair 都不是 null 的時候,代表我們有找到兩組不同 facepair,就會回傳 true

7.30 c 選項

c) three of a kind (e.g., three jacks)

我們實作一個 isThreeOfAKind 方法來檢查牌組是否為 three of a kind

註:three of a kind 的定義是有三個一樣的 face
此處只貼上實作 isThreeOfAKind 方法的部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public boolean isThreeOfAKind(Card... cards) {
    for (int i = 0; i < cards.length - 1; i++) {
        int faceCount = 1;

        for (int j = i + 1; j < cards.length; j++) {
            if (cards[i].getFace().equals(cards[j].getFace()))
                faceCount++;
        }

        if (faceCount >= 3)
            return true;
    }

    return false;
}

這段程式碼在 Reference 的程式碼 中判斷條件是 if (faceCount == 3)。但我認為即使是四張一樣 face 的牌也算是符合定義,故改為 if (faceCount >= 3)

7.30 d 選項

d) four of a kind (e.g., four aces)

我們實作一個 isFourOfAKind 方法來檢查牌組是否為 four of a kind

註:four of a kind 的定義是有四個一樣的 face。 此處只貼上實作 isFourOfAKind 方法的部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public boolean isFourOfAKind(Card... cards) {
    for (int i = 0; i < cards.length - 1; i++) {
        int faceCount = 1;

        for (int j = i + 1; j < cards.length; j++) {
            if (cards[i].getFace().equals(cards[j].getFace()))
                faceCount++;
        }

        if (faceCount == 4)
            return true;
    }

    return false;
}

7.30 e 選項

e) a flush (i.e., all five cards of the same suit)

我們實作一個 isFlush 方法來檢查牌組是否為 flush

註:flush 的定義是五張牌的 suit 皆相同。
此處只貼上實作 isFlush 方法的部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public boolean isFlush(Card... cards) {
    Card firstCard = cards[0];

    int suitCount = 1;

    for (int i = 1; i < cards.length; i++) {
        if (cards[i].getSuit().equals(firstCard.getSuit()))
            suitCount++;
    }

    return suitCount == 5;
}

7.30 f 選項

f) a straight (i.e., five cards of consecutive face values)

我們實作一個 isStraight 方法來檢查牌組是否為 Straight

註:Straight 的定義是牌組中的 face 是連號,而且 Ace 只能在頭、尾。 此處貼上 isStraightisStraightForPartsortCardsvalueOf 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public boolean isStraight(Card... cards) {
    Card[] sortedCards = new Card[cards.length];
    System.arraycopy(cards, 0, sortedCards, 0, cards.length);

    sortCards(sortedCards);
    int currentValue;

    if (sortedCards[0].getFace() == "Ace") {
        // situation1: Ace, Deuce, Three, Four, Five
        // situation2: Ten, Jack, Queen, King, Ace (unsorted) → Ace, Ten, Jack, Queen,
        // King (sorted)
        // 在 situation2 中,可以把 Ace 的 currentValue 當作 8 (跟 Nine 的 currentValue 相同)來判斷。
        boolean situation1 = isStraightForPart(currentValue = 0, sortedCards);
        boolean situation2 = isStraightForPart(currentValue = 8, sortedCards);
        return (situation1 || situation2);
    } else {
        currentValue = valueOf(sortedCards[0].getFace());
        return isStraightForPart(currentValue, sortedCards);
    }
}

private boolean isStraightForPart(int currentValue, Card... cards) {
    for (int i = 1; i < cards.length; i++) {
        if (valueOf(cards[i].getFace()) == currentValue + 1)
            currentValue++;
        else
            return false;
    }
    return true;
}

private void sortCards(Card... cards) {
    for (int i = 0; i < cards.length - 1; i++) {
        for (int j = i + 1; j < cards.length; j++) {
            if (valueOf(cards[i].getFace()) > valueOf(cards[j].getFace())) {
                Card card = cards[i];
                cards[i] = cards[j];
                cards[j] = card;
            }
        }
    }
}

private int valueOf(String face) {
    String[] faces = { "Ace", "Deuce", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack",
            "Queen", "King" };

    for (int i = 0; i < faces.length; i++) {
        if (faces[i].equals(face))
            return i;
    }

    return -1;
}
  • System.arraycopy(cards, 0, sortedCards, 0, cards.length);
    • arraycopy 的用法:
    • System.arraycopy(來源陣列, 複製的起始 index, 目標陣列, 貼上的起始 index, 複製幾個元素);
  • valueOf
    • 取得牌面 face 的「大小」(定義 Ace0, Deuce1, …)
  • sortCards
    • 依照 valueOf 取得的「大小」進行 Bubble Sort
  • isStraightForPart
    • 由於在 Straight 的判定中,Ace 有可能出現在頭也有可能出現在尾,所以遇到有 Ace 的情況時需要分開處理。
    • 將判斷有沒有「連號」的 for 迴圈部分獨立成 isStraightForPart,並在 isStraight 中分成不同情況呼叫,最後回傳。

7.30 g 選項

g) a full house (i.e., two cards of one face value and three cards of another face value)

我們實作一個 isFullHouse 方法來檢查牌組是否為 Full House

註:Full House 的定義是兩個同樣的 face 配上三個一樣的另一種 face。 此處只貼上實作 isFullHouse 方法的部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public boolean isFullHouse(Card... cards) {
    int firstValueCount = 1;
    int firstValue = valueOf(cards[0].getFace());

    for (int i = 1; i < cards.length; i++) {
        if (valueOf(cards[i].getFace()) == firstValue)
            firstValueCount++;
    }

    if (firstValueCount == 2 || firstValueCount == 3) {
        int secondValueCount = 1;
        int secondValue = 0;
        int initialIndex = 0;

        for (int i = 0; i < cards.length; i++) {
            if (valueOf(cards[i].getFace()) != firstValue) {
                secondValue = valueOf(cards[i].getFace());
                initialIndex = i;
                break;
            }
        }

        for (int i = initialIndex + 1; i < cards.length; i++) {
            if (valueOf(cards[i].getFace()) == secondValue)
                secondValueCount++;
        }

        if (secondValueCount == (5 - firstValueCount))
            return true;
    }
    return false;
}
  • firstValue 儲存第一組相同的 face(兩張以上相同) 。
  • firstValueCount 記錄第一組相同的 face 共有幾張牌。
  • firstValueCount 是兩張或三張(才有可能是 Full House),就進行判斷。
    • secondValueCount 需剛好是(5 - firstValueCount),才符合 Full House 的定義(2 配 3)。

7.30 執行結果截圖

/nchu-java-oop-assignment-ch07-solution/EX_7_30_results.webp

題目 7.31 (Card Shuffling and Dealing)

Use the methods developed in Exercise 7.30 to write an application that deals two five-card poker hands, evaluates each hand and determines which is better.

7.31 參考解答

這題要我們從一副牌洗出兩手(各五張),並比較兩者 which is better

謎之語:better… 是要比到多細…

我打算設計一個評分系統,每副牌都可以透過計算出來的分數直接比較輸贏。
(按照牌型大小比較,同牌型者直接判定平手)

一般德州撲克牌型大小依序為:Four of a kind > Full House > Flush > Straight > Three of a kind > Two pairs > A pair > No pair

寫一個 compareHand 方法在 DeckOfCards 類裡面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public int compareHands(Card[] hand1, Card[] hand2) {
    int handPower1 = 0;
    int handPower2 = 0;

    if (isFourOfAKind(hand1))
        handPower1 = 10;
    else if (isFullHouse(hand1))
        handPower1 = 9;
    else if (isFlush(hand1))
        handPower1 = 8;
    else if (isStraight(hand1))
        handPower1 = 7;
    else if (isThreeOfAKind(hand1))
        handPower1 = 6;
    else if (isTwoPairs(hand1))
        handPower1 = 5;
    else if (isPair(hand1))
        handPower1 = 4;

    if (isFourOfAKind(hand2))
        handPower2 = 10;
    else if (isFullHouse(hand2))
        handPower2 = 9;
    else if (isFlush(hand2))
        handPower2 = 8;
    else if (isStraight(hand2))
        handPower2 = 7;
    else if (isThreeOfAKind(hand2))
        handPower2 = 6;
    else if (isTwoPairs(hand2))
        handPower2 = 5;
    else if (isPair(hand2))
        handPower2 = 4;

    if (handPower1 > handPower2)
        return 1;
    else if (handPower2 > handPower1)
        return -1;
    else
        return 0;
}

接著在 DeckOfCardsTest 中取兩手牌,進行比較並輸出。我們順便寫一個 displayHand 方法,方便輸出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Fig. 7.11: DeckOfCardsTest.java

package EX_7_31;

public class DeckOfCardsTest {

    public static void main(String[] args) {
        DeckOfCards myDeckOfCards = new DeckOfCards();
        myDeckOfCards.shuffle();

        Card[] hand1 = new Card[5];
        Card[] hand2 = new Card[5];

        for (int i = 0; i < 5; i++) {
            hand1[i] = myDeckOfCards.dealCard();
            hand2[i] = myDeckOfCards.dealCard();
        }

        System.out.print("Hand 1: ");
        displayHand(hand1);
        System.out.print("Hand 2: ");
        displayHand(hand2);

        System.out.println();

        if (myDeckOfCards.compareHands(hand1, hand2) == 1)
            System.out.println("Hand 1 won!");
        else if (myDeckOfCards.compareHands(hand1, hand2) == -1)
            System.out.println("Hand 2 won!");
        else
            System.out.println("Draw");

    }

    public static void displayHand(Card[] hand) {
        for (Card card : hand)
            System.out.printf("%s - ", card);

        System.out.println();
    }
}

7.31 執行結果截圖

/nchu-java-oop-assignment-ch07-solution/EX_7_31_result.webp

Reference

7.30 和 7.31 的部分程式碼參考了這個 Github Repo 。有些我認為其程式碼有誤的地方有做修改。