はじめに(概要編と同じ内容)
コンソールで遊ぶブラックジャックです。
Qiitaで参考になる記事を検索しました。hit,stay(stand)のみの記事はいっぱいあったのですが、
以下内容のブラックジャック記事は見つけきれませんでした。多分まだ無いです。
-> ブラックジャックで検索すると186件ぐらいあったのに。。(2022/03/06現在の情報)
ルールは下記のとおりです。
項目 | 内容 | 補足 |
---|---|---|
デッキ数 | 8デッキ(計416枚) | 1デッキ=52枚(1~13のトランプ、ジョーカー抜き) |
カードのシャッフルタイミング | 総数の50%を消費した場合 | ゲーム途中の交換は不自然なため、交換タイミングはゲームの切れ目 |
ベッティング | あり | ベットベルの種類 -> 1~1000(ベル) |
ディーラーの行動原則 | 17以上になるまでhit | - |
プレイヤーのできる行動 | hit, stand, double down, split | - |
double down | 最初の行動でのみ選択可能 | - |
double down後 | 追加は、1枚のみ | ベットベル2倍 |
split | 同じ数字が最初の手札で揃った場合のみ選択可能 | 10, J, Q, Kは同じ数字(10)のため可能 |
split後 | double downは禁止 | 手札を2つに分けるためベットベルが2倍 |
プレイヤーが21で勝ち | 1.5倍の配当 | 端数は切り捨て |
無いなら作りましょうということで、作りました。
1からちゃんと作成したのですが、完成までにかかった時間は
設計・実装・リファクタリング・簡易テストで、多分20時間ぐらい!?
ながら作成とはいえ、結構時間かかりました。
特にsplitの手札分ける処理の設計を考えて実装するのが1番時間かかった箇所です。
リファクタリングしたりコメント書いたりと、
新人プログラマ向けに見やすいように作成したつもりです。
いいなと思ったらLooks Good To Meをくれると作者が喜びます。。
ぜひよろしくお願いします =)
概要は、概要編参照してください!
Javaでブラックジャックを作成してみた(betあり, splitあり, double downあり, 8デッキ)_概要編
パッケージ配置
クラス設計
全然UMLとかでは無いので、理解しづらいかもしれませんが、
「頭の中のクラス設計を図にしたら」ってのが、以下のイメージです。
ソース全文
諸事情により、gitでのuploadはしてません。
ラムダ式も使っているので、Java SE8以上で使用してください。
新人プログラマ応援記事なので、解説もちょくちょく書きながら説明します。
Singletonパターンとか、enumクラスとか、ラムダ式とか
軽くだけでも触れるように補足書いてきます。
bean
ちなみにJavaBeansではありません。
beanをバイト列として扱う予定がないので、implements java.io.Serializable してません。
Card.java
・トランプ柄と数字情報の列挙型クラスを持ったbean
-> 列挙型は凄く便利!!
・toString()で、各クラスに応じた文字列表現をしよう(コメントは分かりやすく例を書く)
・オーバーライドする場合は、@Overrideも忘れずに書くようにしよう
・setNumberは、Aの値(1と11)を切りかえるときに使うので存在させております
・環境文字が表示できないIDEを使ってるなら、Suiteのコメントを切り替えてください
package bean;
/**
* カード用のbean
* @author copper_dog
*
*/
public class Card {
/**
* コンストラクタ
* @param トランプの柄情報(列挙型)
* @param トランプの数字情報(列挙型)
*/
public Card(Suite suite, Number number) {
this.suite = suite;
this.number = number;
}
/**
* トランプの数字
*/
private Number number;
/**
* トランプのマーク
*/
private Suite suite;
public Suite getSuite() {
return suite;
}
public Number getNumber() {
return number;
}
public void setNumber(Number number) {
this.number = number;
}
/**
* トランプの柄の列挙クラス
* @author copper_dog
*/
public enum Suite {
SPADE("♠"), CLUB("♣"), DIAMOND("♦"), HEART("♥");
//文字化けするなら、上を消して下を使ってね!!
//SPADE("S "), CLUB("C "), DIAMOND("D "), HEART("H ");
/** トランプの柄 **/
private String label;
Suite(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
/**
* トランプの数字の列挙クラス
* @author copper_dog
*/
public enum Number {
n1_1(11,"A ",true),n1_2(1,"A ",false),n2(2,"2 ",true),n3(3,"3 ",true),
n4(4,"4 ",true),n5(5,"5 ",true),n6(6,"6 ",true),
n7(7,"7 ",true),n8(8,"8 ",true),n9(9,"9 ",true),
n10(10,"10",true),n11(10,"J ",true),n12(10,"Q ",true),n13(10,"K ",true);
/** トランプの数字(計算する値) **/
private int num;
/** トランプの数字(表示する値) **/
private String displayNum;
/** デッキを作る際に含めるか **/
private boolean startCreate;
Number(int num, String displayNum, boolean startCreate) {
this.num = num;
this.displayNum = displayNum;
this.startCreate = startCreate;
}
public int getNum() {
return num;
}
public String getDisplayNum() {
return displayNum;
}
public boolean getStartCreate() {
return startCreate;
}
}
/**
* インスタンスの文字列化
* @return (♦A )とか(♥6 )とか(♣10)の文字
*/
@Override
public String toString() {
//例 : (♦A )とか(♥6 )とか(♣10)
return "(" + suite.getLabel() + number.getDisplayNum() + ")";
}
}
Deck.java
・トランプのListを持つデッキ(山札)bean
-> 仕様上、検索(ランダムアクセス)が多い かつ 作成・削除が頻繁では無いので、
LinkedListではなくArrayListを使用する
・複数デッキを作成する時は、ディープコピー(×シャローコピー)を行う
・以下のようだと、格納しているのが参照型の場合、ディープコピーにならないので注意
new ArrayList<Obj>(コピー元List);
package bean;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import Constant.SettingConst;
/**
* デッキクラス
* @author copper_dog
*
*/
public class Deck {
//デッキ
private List<Card> deck = null;
//次に引くカードの要素位置
private int nextDrawIndex = 0;
/**
* コンストラクタ
* デッキの生成を行う(シャッフルもする)
*/
public Deck(){
this.deck = createDeck();
//デッキの状態をリセットする
reset();
}
/**
* 交換するべきかどうか判定する
* @return カードをリセットするべきか
*/
public boolean shouldResetDeck(){
//交換するべきデッキの使用率
int changeDeckPer = 50;
//範囲内に指定が無い場合は、使用率50%で交換
if(1 <= SettingConst.CHANGE_DECK_PER && SettingConst.CHANGE_DECK_PER <= 100)
changeDeckPer = SettingConst.CHANGE_DECK_PER;
//(デッキ数 * 数字数 * マーク数) < 現在引いているカードのindex * (100 / 交換すべきデッキの使用率)
return ((SettingConst.DECK_NUM * 13 * 4) <= nextDrawIndex * (100 / changeDeckPer));
}
/**
* デッキの状態をリセットする
*/
public void reset(){
//デッキのシャッフルをする
Collections.shuffle(this.deck);
//次に引くカードの要素位置を0に戻す
nextDrawIndex = 0;
}
/**
* カードを1枚引く
* @return 引いたカード
*/
public Card draw(){
if(this.deck.size() <= nextDrawIndex){
//***********カードが足りないため、引くカードがありません***********
//デッキの状態をリセットする
reset();
}
//代入と後置インクリメント
int drawIndex = nextDrawIndex++;
return deck.get(drawIndex);
}
/**
* 設定されたデッキ数setする
* @return デッキ
*/
@SuppressWarnings("unchecked")
private List<Card> createDeck(){
List<Card> allDeck = new ArrayList<>();
//1デッキ作成
ArrayList<Card> oneDeck = createOneDeck();
//設定されたデッキ数setする
for(int i = 0; i < SettingConst.DECK_NUM; i++){
allDeck.addAll((List<Card>)oneDeck.clone());
}
return allDeck;
}
/**
* 1デッキ=52枚(1~13のトランプ、ジョーカー抜き)を作成
* @return 1デッキ
*/
private ArrayList<Card> createOneDeck(){
ArrayList<Card> deck = new ArrayList<>();
//numberのenum loop 1~13 (注:filterあり)
Arrays.stream(Card.Number.values()).filter(e -> e.getStartCreate()).forEach(number -> {
//suiteのenum loop
Arrays.stream(Card.Suite.values()).forEach(suite -> {
deck.add(new Card(suite, number));
});
});
return deck;
}
}
Human.java
・ディーラーとプレイヤーの抽象クラス
-> 次リファクタリングするなら、Template Methodパターンにしたい..
・手札は、ディーラとプレイヤー各自に持たせるのではなく、Human classで持たせる
package bean;
import java.util.ArrayList;
import java.util.List;
import Constant.Constant;
import blackjack.ConsoleManager;
import common.CommonFunc;
/**
* 人間の抽象クラス
* @author copper_dog
*
*/
public abstract class Human {
/**
* 手札
*/
protected List<Card> hand = new ArrayList<>();
/**
* 手札を返す
* @return 手札
*/
public List<Card> getHand() {
return hand;
}
/**
* 手札をセットする
* @param hand 手札
*/
public void setHand(List<Card> hand) {
this.hand = hand;
}
/** 手札を開示する **/
public abstract void open();
/**
* 手札のカード全て見せる
* @param humanName 手札を所持している人名
*/
public void allOpen(String humanName){
StringBuilder sb = new StringBuilder();
hand.forEach(card -> {
sb.append(card.toString());
sb.append(Constant.SPACE);
});
//手札が複数ある時のために半角スペースを追加する -> splitの時用の対策
if(humanName.equals(Constant.DEALER) || humanName.equals(Constant.PLAYER)) humanName = humanName + " ";
ConsoleManager.gameInfoPrint(humanName + " ("+CommonFunc.getScore(true, hand)+"): " + sb.toString().trim(), true);
}
/**
* メッセージを話す
* @param humanName 話している人名
* @param message メッセージ
*/
public void say(String humanName, String message){
ConsoleManager.gameInfoPrint(humanName + "「" + message + "」", true);
}
/**
* 手札の初期化
*/
public void handClear() {
hand = new ArrayList<>();
}
/**
* 最初の行動を行う
* @param deck デッキ
*/
public void firstAction(Deck deck){
//手札の初期化後、カードを2枚引いて手札に加える
CommonFunc.firstAction(deck, this);
};
}
Dealer.java
・ディーラークラス(Humanクラスを継承してる)
・Singletonパターンにしている(複数存在しないようにするため)
・ただのグローバル変数にならないように、Singletonパターン作成時の注意する箇所
-> 注1)状態を持たないこと
-> 注2)ポリモーフィズムが絡むこと
・以下、ラムダ式は「手札の1枚目のカードが、存在していたらStringBuilderに文字列を追加」するようになっているのでとても便利~
super.getHand().stream().findFirst().ifPresent(c -> sb.append(c.toString()));
package bean;
import Constant.Constant;
import blackjack.ConsoleManager;
import common.CommonFunc;
/**
* Singletonパターン ディーラーのbean
* 注1)状態を持たないこと
* 注2)ポリモーフィズムが絡むこと
* @author copper_dog
*
*/
public class Dealer extends Human {
private static final Dealer instance = new Dealer();
/**
* 唯一のインスタンスを返す
* @return このクラスの唯一のインスタンス
*/
public static Dealer getInstance() {
return instance;
}
/**
* 外部からインスタンス化できないよう、コンストラクタをprivateで宣言する
*/
private Dealer() {
}
/**
* 手札のカード1枚見せる
*/
@Override
public void open(){
StringBuilder sb = new StringBuilder();
//手札の最初の要素が存在した場合は、文字を追加する
super.getHand().stream().findFirst().ifPresent(c -> sb.append(c.toString()));
sb.append(Constant.SPACE);
sb.append(Constant.MYSTERIOUS_CARD);
ConsoleManager.gameInfoPrint(Constant.DEALER + " ("+getOpenOneScore()+"): " + sb.toString(), true);
}
/**
* 現在表示している点数を取得する
* @return 手札の合計点
*/
private int getOpenOneScore() {
return hand.get(0).getNumber().getNum();
}
/**
* 17を超えるまで引く
* @param deck デッキ
*/
public void drawOver17(Deck deck){
int score = CommonFunc.getScore(true, super.hand);
while(score < 17){
CommonFunc.draw(deck, super.hand);
score = CommonFunc.getScore(true, super.hand);
}
}
/**
* 発言相手によって、メッセージを変える
* @param playerName 発言する人名
* @param message メッセージ
* @return 変換後のメッセージ
*/
private String changeMsg(String playerName, String message) {
if(playerName.equals(Constant.PLAYER)){
return message;
}else{
return message.replace(Constant.YOU_ARE, playerName + " is");
}
}
/**
* 勝ち(nomal)をたたえる
* @param playerName 発言している人の名前
*/
public void sayYouWin(String playerName) {
say(Constant.DEALER, changeMsg(playerName, Constant.YOU_ARE_WIN));
}
/**
* 勝ち(BJ)をたたえる
* @param playerName 発言している人の名前
*/
public void sayYouWinBj(String playerName) {
say(Constant.DEALER, changeMsg(playerName, Constant.YOU_ARE_WIN_BJ));
}
/**
* 負けを煽る
* @param playerName 発言している人の名前
*/
public void sayYouLose(String playerName) {
say(Constant.DEALER, changeMsg(playerName, Constant.YOU_ARE_LOSE));
}
/**
* 引き分けだと伝える
* @param playerName 発言している人の名前
*/
public void sayYouDraw(String playerName) {
say(Constant.DEALER, changeMsg(playerName, Constant.YOU_ARE_DRAW));
}
}
Player.java
・プレイヤークラス(Humanクラスを継承してる)
・Singletonパターンじゃないので、メンバ変数を持たせてもOK
・ディーラーのsimpleな動きを親クラスで記載しているため、@Override(再定義)多め
・Splitやdouble down時の手札の動きもこっちに記載してる
package bean;
import java.util.ArrayList;
import java.util.List;
import Constant.Constant;
import blackjack.ConsoleManager;
import common.CommonFunc;
/**
* プレイヤーのbean
* @author copper_dog
*
*/
public class Player extends Human {
/** 手持ちのベル(残金) */
private Integer pocketMoney = 0;
/** ゲームでベットしたベル */
private Integer betMoney = 0;
/** スタンドしたかどうか */
private boolean isStand = false;
/** バーストしたかどうか */
private boolean isBurst = false;
/** splitを選択したかどうか */
private boolean isSplit = false;
/** split後の手札をスタンドしたか */
private boolean isSplitStand = false;
/** split後の手札がバーストしたかどうか */
private boolean isSplitBurst = false;
/** Split時に分けた2つ目の手札 */
private List<Card> secondHand = new ArrayList<>();
/**
* コンストラクタ
* @param pocketMoney 初期で持たせる所持ベル
*/
public Player(int pocketMoney) {
this.pocketMoney = pocketMoney;
}
/**
* 手札の初期化後、カードを2枚引いて手札に加える
* @param deck デッキ
*/
@Override
public void firstAction(Deck deck){
//初期状態に戻す
betMoney = 0;
setStand(false);
setBurst(false);
setSplit(false);
setSplitStand (false);
setSplitBurst(false);
super.firstAction(deck);
}
/**
* 手札の初期化
*/
@Override
public void handClear() {
this.secondHand = new ArrayList<>();
super.handClear();
}
/**
* 手札のカードを見せる
*/
@Override
public void open(){
allOpen(Constant.PLAYER);
}
/**
* 手札のカード全て見せる
* @param humanName 手札を持っている人名
*/
@Override
public void allOpen(String humanName){
//second用の手札がある場合
if(!secondHand.isEmpty()){
super.allOpen(Constant.PLAYER1);
StringBuilder sb2 = new StringBuilder();
secondHand.forEach(card -> {
sb2.append(card.toString());
sb2.append(Constant.SPACE);
});
ConsoleManager.gameInfoPrint(Constant.PLAYER2 + " ("+CommonFunc.getScore(true, secondHand)+"): " + sb2.toString().trim(), true);
}else{
super.allOpen(humanName);
}
}
/**
* splitが可能かどうか判定する
* @return splitが可能かどうか
*/
public boolean possibleSplit(){
boolean possibleSplit = false;
//手札が二枚かどうか && スプリットが未選択か && ベットベルの二倍のベルを持っているか
if(super.hand.size() == 2 && !isSplit && betMoney * 2 <= pocketMoney){
Card.Number firstCardNum= super.hand.get(0).getNumber();
Card.Number secondCardNum = super.hand.get(1).getNumber();
//同じ数字(J,Q,Kは10扱い)かどうか(Aの1と11の考慮も入れる)
if(firstCardNum.getNum() == secondCardNum.getNum()
|| firstCardNum.getDisplayNum().equals(secondCardNum.getDisplayNum())){
possibleSplit = true;
}
}
return possibleSplit;
}
/**
* double downが可能かどうか判定する
* @return double downが可能かどうか
*/
public boolean possibleDoubleDown(){
boolean possibleDoubleDown = false;
//手札が二枚かどうか && スプリットが未選択か && ベットベルの二倍のベルを持っているか
if(super.hand.size() == 2 && !isSplit && betMoney * 2 <= pocketMoney){
possibleDoubleDown = true;
}
return possibleDoubleDown;
}
/**
* splitを選択した時の行動をする
* @param deck デッキ
*/
@SuppressWarnings("unchecked")
public void split(Deck deck){
if(super.hand.size() == 2){
//ローカル変数にディープコピーを行う
List<Card> localHand = (List<Card>)((ArrayList<Card>)super.hand).clone();
//手札を二つに分ける
super.hand = new ArrayList<>();
super.hand.add(localHand.get(0));
secondHand.add(localHand.get(1));
//各一枚だけ引く
CommonFunc.draw(deck, secondHand);
CommonFunc.draw(deck, super.hand);
};
}
/**
* @return 所持ベル
*/
public Integer getPocketMoney() {
return pocketMoney;
}
/**
* @param pocketMoney セットする pocketMoney
*/
public void setPocketMoney(Integer pocketMoney) {
this.pocketMoney = pocketMoney;
}
/**
* @return betMoney ベットするベル額
*/
public Integer getBetMoney() {
return betMoney;
}
/**
* @param betMoney セットする betMoney
*/
public void setBetMoney(Integer betMoney) {
this.betMoney = betMoney;
}
/**
* @return isStand satand状態か
*/
public boolean isStand() {
return isStand;
}
/**
* @param isStand セットする isStand
*/
public void setStand(boolean isStand) {
this.isStand = isStand;
}
/**
* @return isBurst バーストしているか
*/
public boolean isBurst() {
return isBurst;
}
/**
* @param isBurst セットする isBurst
*/
public void setBurst(boolean isBurst) {
this.isBurst = isBurst;
}
/**
* @return secondHand 分けた手札を取得する
*/
public List<Card> getSecondHand() {
return secondHand;
}
/**
* @param secondHand セットする secondHand
*/
public void setSecondHand(List<Card> secondHand) {
this.secondHand = secondHand;
}
/**
* @return isSplit splitを選択したか
*/
public boolean isSplit() {
return isSplit;
}
/**
* @param isSplit セットする isSplit
*/
public void setSplit(boolean isSplit) {
this.isSplit = isSplit;
}
/**
* @return isSplitStand 分けた手札をstandしたか
*/
public boolean isSplitStand() {
return isSplitStand;
}
/**
* @param isSplitStand セットする isSplitStand
*/
public void setSplitStand(boolean isSplitStand) {
this.isSplitStand = isSplitStand;
}
/**
* @return isSplitBurst 分けた手札がバーストしたかどうか
*/
public boolean isSplitBurst() {
return isSplitBurst;
}
/**
* @param isSplitBurst セットする isSplitBurst
*/
public void setSplitBurst(boolean isSplitBurst) {
this.isSplitBurst = isSplitBurst;
}
}
blackjack(controller)
ブラックジャックの根幹をなすclass達
Main.java
・ここからスタート
・GameManageクラス生成する際に、軍資ベルを渡してあげている
・Mainクラスは、ただただ大枠の処理フローを流すイメージで、軽くなるように実装
package blackjack;
import Constant.Constant;
import Constant.SettingConst;
/**
* main class
* @author copper_dog
*
*/
public class Main {
/**
* ここからスタート
* @param arges
*/
public static void main(String[] arges) {
GameManage gm = new GameManage(SettingConst.POCKET_MONEY);
boolean finishGame = false;
do{
//ゲーム開始
boolean exeFnish = gm.play();
//ゲームを強制終了するか
if(exeFnish){
finishGame = true;
}else{
finishGame = !needRetryPlay();
//見やすいように区切りを入れる
ConsoleManager.printSeparatorEnd();
}
}while(!finishGame);
//ゲーム終了
gm.close();
}
/**
* もう一度プレイするか確認する
* @return もう一度プレイするか
*/
private static boolean needRetryPlay(){
Boolean needRetryPlay = null;
//userの選択の取得
do{
String userInputStr = ConsoleManager.choiceUserAction();
switch (userInputStr.toUpperCase()) {
case Constant.YES_1:
case Constant.YES_2:
needRetryPlay = true;
break;
case Constant.NO_1:
case Constant.NO_2:
needRetryPlay = false;
break;
default:
break;
}
}while(needRetryPlay == null);
return needRetryPlay.booleanValue();
}
}
GameManage.java
・ブラックジャックのあれこれ(gameの開始、ユーザーの行動取得、勝負の判定後の処理、終了等)をするクラス
・GameManageクラスのコンストラクタで、軍資ベルの設定、ディーラー、プレイヤー、デッキの生成を行ってる
package blackjack;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import Constant.Constant;
import bean.Card;
import bean.Dealer;
import bean.Deck;
import bean.Player;
import common.CommonFunc;
/**
* ブラックジャックを管理するクラス
* @author copper_dog
*
*/
public final class GameManage {
/** ディーラーの作成 **/
private Dealer dealer;
/** プレイヤーの作成 **/
private Player player;
/** デッキ **/
private Deck deck;
/**
* Gameに必要なものを作成
* @param pocketMoney 所持するベル
*/
public GameManage(int pocketMoney){
//挨拶をおこなう
ConsoleManager.gameStartUp();
/** ディーラーの作成 **/
this.dealer = Dealer.getInstance();
/** プレイヤーの作成 **/
this.player = new Player(pocketMoney);
/** デッキの作成 **/
this.deck = new Deck();
}
/**
* ゲームを終了する
*/
public void close() {
//儲けの表示
ConsoleManager.resultMoney(player.getPocketMoney());
//挨拶をおこなう
ConsoleManager.gameFinish();
}
/**
* ブラックジャックを始める
* @return ゲームを強制終了するか
*/
public boolean play(){
int gameStatus = Constant.BATTLE_NOW;
//ゲーム開始の合図
ConsoleManager.gameStart(player.getPocketMoney());
//ブラックジャックの最初の行動を行う
firstAction();
//ベット金を選択させる
boolean exeEnd = choiceBetMoneyOrEnd();
if(exeEnd) return exeEnd;
//今の手札を表示する
ConsoleManager.handOpen(dealer, player, false);
//プレイヤーに次の行動を選択させる
boolean nowSplitTurn = false;
do{
String userInputStr = ConsoleManager.choicePlayerAction(player, nowSplitTurn);
if(playerAction(userInputStr, nowSplitTurn) &&
(player.isStand() || player.isBurst())){
//splitしている場合
if(player.isSplit()){
nowSplitTurn = true;
if(player.isSplitStand() || player.isSplitBurst()) break;
}else{
break;
}
}
}while(true);
//プレイヤーの手札によって、ディーラーの行動を決める
if(player.isBurst() && player.isSplitBurst()){
gameStatus = Constant.DEALER_WIN;
}else{
//ディーラーが行動する
dealer.drawOver17(deck);
}
//勝敗を判定と、それによる行動を行う
judgementAction(gameStatus, player);
return player.getPocketMoney() == 0;
}
/**
* ベットするベルを選択してもらうメソッド
* @return ゲームを強制終了するか
*/
private boolean choiceBetMoneyOrEnd(){
do{
String userInputStr = ConsoleManager.choiceBetMoney();
if(Constant.END.equals(userInputStr.toUpperCase())) return true;
try{
int betMoney = Integer.parseInt(userInputStr);
if(betMoney <= 0){
//0円以下をベット場合
ConsoleManager.gameInfoPrint(Constant.ONE_MORE_INPUT_UNDER0, true);
}else if(betMoney > player.getPocketMoney()){
//持っているベルより、多くをベット場合
ConsoleManager.gameInfoPrint(Constant.ONE_MORE_INPUT_LESS_MONEY, true);
}else{
player.setBetMoney(betMoney);
player.setPocketMoney(player.getPocketMoney() - betMoney);
break;
}
}catch(NumberFormatException e){
ConsoleManager.gameInfoPrint(Constant.ONE_MORE_INPUT, true);
}
}while(true);
return false;
}
/**
* ブラックジャックの最初の行動を行う
*/
private void firstAction(){
//カードを引く前に、シャッフルの必要性があるか確認
if(deck.shouldResetDeck()){
//半数以上カードが使用されている場合h、シャッフルする
deck.reset();
}
//プレイヤーとディーラーの手札初期化し、カードを二枚ひく
player.firstAction(deck);
dealer.firstAction(deck);
}
/**
* プレイヤーの行動を分岐する
* @param userInputStr userが入力した文字列
* @param nowSplitTurn splitの手札での行動か
* @return errorが無い場合はtrue
*/
private boolean playerAction(String userInputStr, Boolean nowSplitTurn){
boolean isNotError = true;
switch (userInputStr.toUpperCase()) {
case Constant.HIT:
List<Card> localHand = nowSplitTurn ? player.getSecondHand() : player.getHand();
//手札を一枚引く
CommonFunc.draw(deck, localHand);
//21を超えていないか判定する
if(CommonFunc.overHand21(localHand)){
//バースト状態に変更する
if(nowSplitTurn){
player.setSplitBurst(true);
}else{
player.setBurst(true);
//バースト && スプリット選択済み && スプリットターン以外
if(player.isSplit()){
//今の手札を表示する
ConsoleManager.handOpen(dealer, player, false);
}
}
}else{
//今の手札を表示する
ConsoleManager.handOpen(dealer, player, false);
}
break;
case Constant.STAND:
//stand状態に変更する
if(nowSplitTurn){
player.setSplitStand(true);
}else{
player.setStand(true);
}
break;
case Constant.DOUBLE_DOWN:
//DOUBLE_DOWNが可能かcheck
if(player.possibleDoubleDown()){
//所持ベルを減らして、bet増加
player.setPocketMoney(player.getPocketMoney() - player.getBetMoney());
player.setBetMoney(player.getBetMoney() + player.getBetMoney());
//double downを行う
CommonFunc.draw(deck, player.getHand());
player.setStand(true);
}else{
//もう一度選択させる
isNotError = false;
ConsoleManager.gameInfoPrint(Constant.ONE_MORE_INPUT, true);
}
break;
case Constant.SPLIT:
//splitが可能かcheck
if(player.possibleSplit()){
//ベットベルを減らす
player.setPocketMoney(player.getPocketMoney() - player.getBetMoney());
//splitを行う
player.split(deck);
//splitFlgを立てる
player.setSplit(true);
//今の手札を表示する
ConsoleManager.handOpen(dealer, player, false);
}else{
//もう一度選択させる
isNotError = false;
ConsoleManager.gameInfoPrint(Constant.ONE_MORE_INPUT, true);
}
break;
default:
//もう一度引かせる
isNotError = false;
ConsoleManager.gameInfoPrint(Constant.ONE_MORE_INPUT, true);
break;
}
return isNotError;
}
/**
* 勝敗を決める
* @param gameStatus ゲームの状態 (勝負中:0, ディーラーの勝ち : 1, プレイヤー勝ち(21以外) : 2, プレイヤー勝ち(21で勝ち) : 3, 引き分け : 4, split mode:5)
* @param player プレイヤーオブジェクト
*/
private void judgementAction(int gameStatus, Player player){
int betMoney = player.getBetMoney();
//Modeによって出力する文字を変える
boolean isSplitMode = player.isSplit();
Map<String, Integer> resultList = new LinkedHashMap<>();
//勝敗を決める
int winnerNum = gameStatus == Constant.BATTLE_NOW ? CommonFunc.judgementWinner(dealer, player.getHand()) : gameStatus;
resultList.put(isSplitMode ? Constant.PLAYER1 : Constant.PLAYER, winnerNum);
//splitの場合
if(isSplitMode){
int winnerNum2 = gameStatus == Constant.BATTLE_NOW ? CommonFunc.judgementWinner(dealer, player.getSecondHand()) : gameStatus;
resultList.put(Constant.PLAYER2, winnerNum2);
}
//今の手札を表示する
ConsoleManager.handOpen(dealer, player, true);
//勝敗によって行動を変える
resultList.forEach((key, val) -> {
switch (val) {
case Constant.DEALER_WIN:
dealer.sayYouLose(key);
break;
case Constant.PLAYER_WIN:
player.setPocketMoney(player.getPocketMoney() + (betMoney * 2));
dealer.sayYouWin(key);
break;
case Constant.PLAYER_WIN_BJ:
//端数は切り捨て
player.setPocketMoney(player.getPocketMoney() + (int)(betMoney * 2.5));
dealer.sayYouWinBj(key);
break;
case Constant.NOTTING_WIN:
player.setPocketMoney(player.getPocketMoney() + (betMoney));
dealer.sayYouDraw(key);
break;
}
});
//見やすいように区切りを入れる
ConsoleManager.printSeparatorEnd();
}
}
ConsoleManager.java
・コンソールに出力する処理を一元管理するメソッド
・gameInfoPrint()で、出力を1つにしているため、コンソールに文字を出さない変更もすぐ可能
package blackjack;
import java.util.Scanner;
import Constant.Constant;
import Constant.SettingConst;
import bean.Dealer;
import bean.Player;
/**
* コンソール処理関連をまとめてる
* @author copper_dog
*
*/
public final class ConsoleManager {
private static final Scanner sc = new Scanner(System.in);
/** インスタント化させない **/
private ConsoleManager(){throw new AssertionError();}
/**
* コンソールに文字を出力する
* @param printStr 出力する文字
* @param writeLine 一行記載するか
*/
public static final void gameInfoPrint(String printStr, boolean writeLine) {
if(SettingConst.SHOULD_OPERATE){
if(writeLine){
System.out.println(printStr);
}else{
System.out.print(printStr);
}
}
}
/**
* 手札を表示する
* @param dealer プレイヤー
* @param player ディーラー
* @param dealerAllOpen ディーラーが手札を全て表示するか
*/
public static final void handOpen(Dealer dealer, Player player, boolean dealerAllOpen) {
if(SettingConst.SHOULD_OPERATE){
if(dealerAllOpen){
dealer.allOpen(Constant.DEALER);
}else{
dealer.open();
}
player.open();
printSeparator();
}
}
/**
* コンソールに長めの横線を引く(途中用)
*/
public static final void printSeparator() {
gameInfoPrint(Constant.SEPARATOR_STR, true);
}
/**
* コンソールに長めの横線を引く(終わり用)
*/
public static final void printSeparatorEnd() {
gameInfoPrint(Constant.EMPTY, true);
gameInfoPrint(Constant.SEPARATOR_STR_END, true);
gameInfoPrint(Constant.EMPTY, true);
}
/**
* ベット金を選択させるための文字を表示する
* @return ユーザーが入力した文字
*/
public static final String choiceBetMoney() {
gameInfoPrint(Constant.CHOICE_USER_BET, false);
String inputStr = sc.nextLine();
return inputStr;
}
/**
* プレイヤーの行動を促す文字を表示する
* @param player プレイヤー
* @param nowSplitTurn 現在Split中の手札か
* @return プレイヤーが入力した文字
*/
public static final String choicePlayerAction(Player player, boolean nowSplitTurn) {
String playerName = Constant.PLAYER;
//splitを選択した場合
if(player.isSplit()){
playerName = nowSplitTurn ? Constant.PLAYER2 : Constant.PLAYER1;
}
//文字を作成する
StringBuilder sb = new StringBuilder();
sb.append(Constant.CHOICE_PLAYER_ACTION1);
if(player.possibleDoubleDown()) sb.append(Constant.CHOICE_PLAYER_ACTION2);
if(player.possibleSplit()) sb.append(Constant.CHOICE_PLAYER_ACTION3);
sb.append(Constant.CHOICE_PLAYER_ACTION_END);
gameInfoPrint(playerName + sb.toString(), false);
String inputStr = sc.nextLine();
return inputStr;
}
/**
* ユーザーの行動を諭す文字を表示する
* @return ユーザーが入力した文字
*/
public static final String choiceUserAction() {
gameInfoPrint(Constant.CHOICE_USER_ACTION, false);
String inputStr = sc.nextLine();
return inputStr;
}
/**
* ゲーム起動の文字を表示する
*/
public static final void gameStartUp() {
ConsoleManager.gameInfoPrint(Constant.SEPARATOR_STR_END, true);
ConsoleManager.gameInfoPrint(Constant.SEPARATOR_STR_END, true);
ConsoleManager.gameInfoPrint(Constant.HELLO, true);
ConsoleManager.gameInfoPrint(Constant.SEPARATOR_STR_END, true);
}
/**
* ゲーム開始の文字を表示する
* @param pocketMoney 所持ベル
*/
public static final void gameStart(int pocketMoney) {
ConsoleManager.gameInfoPrint(Constant.SEPARATOR_STR, true);
ConsoleManager.gameInfoPrint(Constant.GAME_START, true);
ConsoleManager.gameInfoPrint(Constant.SEPARATOR_STR, true);
ConsoleManager.gameInfoPrint(" " + Constant.PLAYER + Constant.TEACH_POKET_MONEY + pocketMoney, true);
}
/**
* ゲームでの儲けを表示する
* @param pocketMoney 所持ベル
*/
public static final void resultMoney(int pocketMoney) {
ConsoleManager.gameInfoPrint(" " + Constant.PLAYER + Constant.TEACH_POKET_MONEY + pocketMoney, true);
if(pocketMoney >= SettingConst.POCKET_MONEY){
ConsoleManager.gameInfoPrint(" " + (pocketMoney - SettingConst.POCKET_MONEY) + Constant.RESULT_EARNED, true);
}else{
ConsoleManager.gameInfoPrint(" " + (SettingConst.POCKET_MONEY - pocketMoney) + Constant.RESULT_LOST, true);
}
}
/**
* ゲーム終了の文字を表示する
*/
public static final void gameFinish() {
ConsoleManager.gameInfoPrint(Constant.THANKS, true);
}
}
common
小規模すぎて1つしかクラスないけど、共通関数パッケージ
CommonFunc.java
・共通関数クラス
・トランプのドローとか、Aの11->1への変更とか、手札が21を超えているか調べたり等してる
・Humanクラスにドローとか書きたかったけど、splitがあるせいであきらめた
・引数に手札を渡すようにして、共通関数に切り出した
package common;
import java.util.List;
import java.util.Optional;
import Constant.Constant;
import bean.Card;
import bean.Dealer;
import bean.Deck;
import bean.Human;
/**
* 共通関数
* @author copper_dog
*/
public class CommonFunc {
/**
* 誰が勝ったかを判断する
* @param dealer ディーラー
* @param playerHand プレイヤーの手札
* @return 1-> dealer, 2 -> player(nomal), 3 -> player(BJ), 4 -> draw(引き分け)
*/
public static int judgementWinner(Dealer dealer, List<Card> playerHand) {
int winnerNum = Constant.DEALER_WIN;
if(overHand21(playerHand)){
//playerがバーストしてるため、ディーラーの勝ち
winnerNum = Constant.DEALER_WIN;
}else if(overHand21(dealer.getHand())){
//ディーラーがバーストしてるため、playerの勝ち
int playerScore = getScore(false, playerHand);
winnerNum = playerScore == 21 ? Constant.PLAYER_WIN_BJ : Constant.PLAYER_WIN;
}else{
int dealerScore = getScore(false, dealer.getHand());
int playerScore = getScore(false, playerHand);
if(playerScore > dealerScore){
//playerの勝ち
winnerNum = playerScore == 21 ? Constant.PLAYER_WIN_BJ : Constant.PLAYER_WIN;
}else if(playerScore == dealerScore){
//引き分け
winnerNum = Constant.NOTTING_WIN;
}else{
//ディーラー勝ち
winnerNum = Constant.DEALER_WIN;
}
}
return winnerNum;
}
/**
* カードを引いて手札に加える
* @param deck デッキ
* @param hand 手札
*/
public static void draw(Deck deck, List<Card> hand){
hand.add(deck.draw());
}
/**
* 手札の初期化後、カードを2枚引いて手札に加える
* @param deck デッキ
* @param human humanオブジェクト
*/
public static void firstAction(Deck deck, Human human){
human.handClear();
human.getHand().add(deck.draw());
human.getHand().add(deck.draw());
}
/**
* 得点を取得する
* @param aceChange エースの変換制御を行うかどうか
* @param hand 手札
* @return 手札の合計点
*/
public static int getScore(boolean aceChange, List<Card> hand) {
//Aの動きを制御する
if(aceChange && haveAce11(hand))aceChange(hand);
return hand.stream().mapToInt(c -> c.getNumber().getNum()).sum();
}
/**
* バーストしている場合は、A11をA1に変更する
* @param hand
*/
public static void aceChange(List<Card> hand) {
int haveAceInt = getManyAce11(hand);
for(int i=0; i<haveAceInt;i++){
if(getScore(false, hand) > 21){
Optional<Card> opt = hand.stream()
.filter(c -> Card.Number.n1_1 == c.getNumber())
.findFirst();
if(opt.isPresent()){
Card c = opt.get();
c.setNumber(Card.Number.n1_2);
}
}else{
break;
}
}
}
/**
* 11と数えているAceの数を取得する
* @param hand 手札
* @return 11と数えているAceの数
*/
public static int getManyAce11(List<Card> hand){
return (int) hand.stream().filter(c -> Card.Number.n1_1 == c.getNumber()).count();
}
/**
* 11と数えているAceを持っているか
* @param hand 手札
* @return 11と数えているAceを持っているか
*/
public static boolean haveAce11(List<Card> hand){
return getManyAce11(hand) != 0;
}
/**
* 手札が21を超えているか調べる
* @param hand
* @return 手札が21を超えているか
*/
public static boolean overHand21(List<Card> hand){
boolean over21 = false;
int total = getScore(true, hand);
if(total > 21){
over21 = true;
}
return over21;
}
}
Constant
Constant.java
・定数クラスです
・特にコメントはありません
package Constant;
/**
* 定数クラス
* @author copper_dog
*
*/
public class Constant {
/** 勝負中 **/
public static final int BATTLE_NOW = 0;
/** ディーラーの勝ち **/
public static final int DEALER_WIN = 1;
/** プレイヤー勝ち(21以外で勝ち) **/
public static final int PLAYER_WIN = 2;
/** プレイヤー勝ち(21で勝ち) **/
public static final int PLAYER_WIN_BJ = 3;
/** 引き分け **/
public static final int NOTTING_WIN = 4;
/** split mode **/
public static final int SPLIT_MODE = 5;
/** yes1 **/
public static final String YES_1 = "YES";
/** yes2 **/
public static final String YES_2 = "Y";
/** NO1 **/
public static final String NO_1 = "NO";
/** NO2 **/
public static final String NO_2 = "N";
/** END **/
public static final String END = "END";
/** ディーラー **/
public static final String DEALER = "DEALER";
/** プレイヤー **/
public static final String PLAYER = "Player";
/** プレイヤー1 **/
public static final String PLAYER1 = "PlayerR";
/** プレイヤー2 **/
public static final String PLAYER2 = "PlayerL";
/** 空文字 **/
public static final String EMPTY = "";
/** 区切り文字(途中用) **/
public static final String SEPARATOR_STR = "-----------------------------------";
/** 区切り文字(終わり用) **/
public static final String SEPARATOR_STR_END = "****************************************";
/** あなたの文字列定数 **/
public static final String YOU_ARE = "You are";
/** プレイヤーの勝ち(nomal)をたたえる **/
public static final String YOU_ARE_WIN = "congratulation. You are win";
/** プレイヤーの勝ち(BJ)をたたえる **/
public static final String YOU_ARE_WIN_BJ = "congratulation. You are BLACK JACK!!";
/** プレイヤーの負けを煽る **/
public static final String YOU_ARE_LOSE = "You are lose.";
/** 引き分けだと教える **/
public static final String YOU_ARE_DRAW = "You are draw.";
/** 謎のカード **/
public static final String MYSTERIOUS_CARD = "(??)";
/** スペース **/
public static final String SPACE = " ";
/** ユーザーにbet金を選ばせる文字 **/
public static final String CHOICE_USER_BET = "\nベットするベルを入力してください(end -> 終了)\n >";
/** ユーザーに次の行動を選ばせる文字 **/
public static final String CHOICE_USER_ACTION = "もう一回、遊べるドン。\n y: yes n:no\n >";
/** プレイヤーに次の行動を選ばせる文字1 **/
public static final String CHOICE_PLAYER_ACTION1 = "の行動を選択してください。\n h: hit s:stay";
/** プレイヤーに次の行動を選ばせる文字2 **/
public static final String CHOICE_PLAYER_ACTION2 = " d:double down";
/** プレイヤーに次の行動を選ばせる文字3 **/
public static final String CHOICE_PLAYER_ACTION3 = " p:split";
/** プレイヤーに次の行動を選ばせる文字end **/
public static final String CHOICE_PLAYER_ACTION_END = "\n >";
/** hit **/
public static final String HIT = "H";
/** stand **/
public static final String STAND = "S";
/** double down **/
public static final String DOUBLE_DOWN = "D";
/** Split **/
public static final String SPLIT = "P";
/** ゲーム起動の挨拶 **/
public static final String HELLO = " ブラックジャックへようこそ!! ";
/** 稼いだ報告 **/
public static final String RESULT_EARNED = "ベル勝ちました。 ";
/** 損した報告 **/
public static final String RESULT_LOST = "ベル負けました。 ";
/** ゲーム終了の挨拶 **/
public static final String THANKS = "\n copper_dog「Thank you for playing!! :)」 ";
/** ゲーム開始の挨拶 **/
public static final String GAME_START = " ゲーム開始!! ";
/** ゲーム開始の挨拶 **/
public static final String TEACH_POKET_MONEY = "の所持ベル -> ";
/** もう一度入力してください **/
public static final String ONE_MORE_INPUT = "\n Error : もう一度入力してください \n";
/** ベルがたりません。もう一度入力してください。 **/
public static final String ONE_MORE_INPUT_LESS_MONEY = "\n Error : ベルがたりません。もう一度入力してください。 \n";
/** 0以下が入力されました。もう一度入力してください。 **/
public static final String ONE_MORE_INPUT_UNDER0 = "\n Error : 0以下が入力されました。もう一度入力してください。 \n";
}
SettingConst.java
・システム定数クラス
・Userが設定することで色々な環境を変える子ができるクラス(所持ベル、デッキ数、シャッフルを行うデッキの使用率等)
package Constant;
/**
* システム定数クラス
* @author copper_dog
*
*/
public class SettingConst {
/** ユーザーが操作するか(コンソールに文字を出力するか) **/
public static final boolean SHOULD_OPERATE = true;
/** デッキ数 **/
public static final int DECK_NUM = 8;
/** カードを交換する際の使用率(1%~99%まで) -> 40なら、40%使用したタイミングでリセット **/
public static final int CHANGE_DECK_PER = 50;
/** 初期の所持ベル **/
public static final int POCKET_MONEY = 1000;
}
最後に
デバックでブラックジャックやりすぎたので、完成したころには遊ぶ気が失せてました(笑)
ソースコード見て「もっといい実装パターンあるよ」とか、「勉強になった!」とか何でもいいので、感情が動いてくれることを祈ります。
皆さんもリアルなカジノをイメージしたブラックジャックを実装してみて下さいね!!