7268 字
36 分鐘

興大物件導向程式設計 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

Fig-6-8_1 Fig-6-8_2 Fig-6-8_3 Fig-6-8_4 Fig-6-8_5 Fig-6-8_6 Fig-6-8_7 Fig-6-8_8

7.18 參考解答#

Craps 遊戲介紹#

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

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

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

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

獲取 7.18 程式碼#

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

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

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. *
*************************************************************************/

把這個程式拿去編譯執行得到的結果如下:

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 方法裡用迴圈乎叫:

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 輪獲勝,進行統計
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;
}
}

執行結果:

7.18 b 選項#

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

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

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

執行結果:

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{獲勝局數}{總遊玩局數}

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

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

執行結果只有一行:

勝率 = 0.492374

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


那麼這代表什麼呢?

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

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

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

7.18 d 選項#

What is the average length of a game of craps?

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

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

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

寫成 Java 程式:

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

執行結果只有一行:

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

7.18 e 選項#

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

 在第 i 局贏=一直玩到第 i1 局都未結束,並且玩家贏在第 i 局在第 i 局輸=一直玩到第 i1 局都未結束,並且玩家輸在第 i 局 P(第 i 局玩家勝利第 i1 局未結束遊戲)=第 i 局贏第 i 局贏+第 i 局輸\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 選項的程式做修改:

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

執行的結果:

題目 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-11_1 Fig-7-11_2

Fig.7.10

Fig-7-10_1 Fig-7-10_2 Fig-7-10_3 Fig-7-10_4

Fig.7.9

Fig-7-9_1

7.30 參考解答#

撲克牌#

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

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

獲取 7.30 程式碼#

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

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. *
*************************************************************************/
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. *
*************************************************************************/
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 檔放到同一個專案裡面後,執行的結果如下:

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 類備用:

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 方法的部分。

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 方法的部分。

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 方法的部分。

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 方法的部分。

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 方法的部分。

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 方法。

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 方法的部分。

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 執行結果截圖#

題目 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 類裡面:

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 方法,方便輸出:

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 執行結果截圖#

Reference#

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

興大物件導向程式設計 Assignment Ch07 參考詳解
https://blog.pytreedao.com/posts/nchu-java-oop-assignment-ch07-solution/
作者
Pytree
發布於
2024-04-21
許可協議
CC BY-NC-SA 4.0
最後更新於 2024-04-21,距今已過 637 天

部分內容可能已過時

評論區

目錄