Javaでブラックジャックを作成してみた(betあり, splitあり, double downあり, 8デック)_ソース編

# はじめに(概要編と同じ内容)
コンソールで遊ぶブラックジャックです。

Qiitaで参考になる記事を検索しました。hit,stay(stand)のみの記事はいっぱいあったのですが、
以下内容のブラックジャック記事は見つけきれませんでした。多分まだ無いです。
　-> ブラックジャックで検索すると186件ぐらいあったのに。。(2022/03/06現在の情報)

ルールは下記のとおりです。

|項目 |内容 |補足 |
|---|---|---|
|デッキ数 |8デッキ(計416枚) |１デッキ＝52枚(１～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をくれると作者が喜びます。。
ぜひよろしくお願いします =)

<strong><font color="red">
概要は、概要編参照してください！
</strong></font>
[Javaでブラックジャックを作成してみた(betあり, splitあり, double downあり, 8デッキ)_概要編](https://qiita.com/copper_dog/items/4d204344b8dd3fe1fb11/)

# パッケージ配置

パッケージエクスプローラーは、以下のように作成。
![スクリーンショット 2022-03-07 140049.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508154/b12cce37-7f8c-e43a-ac30-5eba4af9c967.png)

# クラス設計
全然UMLとかでは無いので、理解しづらいかもしれませんが、
「頭の中のクラス設計を図にしたら」ってのが、以下のイメージです。
![スクリーンショット 2022-03-09 011347.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508154/059d6d54-260d-8c6c-5998-bb775ccc0fe2.png)

# ソース全文
諸事情により、gitでのuploadはしてません。

ラムダ式も使っているので、Java SE8以上で使用してください。

新人プログラマ応援記事なので、解説もちょくちょく書きながら説明します。
Singletonパターンとか、enumクラスとか、ラムダ式とか
軽くだけでも触れるように補足書いてきます。

## bean
ちなみにJavaBeansではありません。
beanをバイト列として扱う予定がないので、<font color="Navy">implements java.io.Serializable</font> してません。

### Card.java
:::note info
・トランプ柄と数字情報の列挙型クラスを持ったbean
　　-> 列挙型は凄く便利!!
・toString()で、各クラスに応じた文字列表現をしよう(コメントは分かりやすく例を書く)
・オーバーライドする場合は、@&#xFFF0;Overrideも忘れずに書くようにしよう
・setNumberは、Aの値(1と11)を切りかえるときに使うので存在させております
・環境文字が表示できないIDEを使ってるなら、Suiteのコメントを切り替えてください
:::
```java:Card.java
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
:::note info
・トランプのListを持つデッキ(山札)bean
　　-> 仕様上、検索(ランダムアクセス)が多い かつ 作成・削除が頻繁では無いので、
　　　LinkedListではなくArrayListを使用する
・複数デッキを作成する時は、ディープコピー(×シャローコピー)を行う
・以下のようだと、格納しているのが参照型の場合、ディープコピーにならないので注意
 　new ArrayList\<Obj>(コピー元List);
:::

```java:Deck.java
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;
	}

	/**
	 * １デッキ＝52枚(１～13のトランプ、ジョーカー抜き)を作成
	 * @return １デッキ
	 */
	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
:::note info
・ディーラーとプレイヤーの抽象クラス
　　-> 次リファクタリングするなら、Template Methodパターンにしたい..
・手札は、ディーラとプレイヤー各自に持たせるのではなく、Human classで持たせる
:::
```java:Human.java
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
:::note info
・ディーラークラス(Humanクラスを継承してる)
・Singletonパターンにしている(複数存在しないようにするため)
・ただのグローバル変数にならないように、Singletonパターン作成時の注意する箇所
　　-> 注1)状態を持たないこと
　　-> 注2)ポリモーフィズムが絡むこと
・以下、ラムダ式は「手札の1枚目のカードが、存在していたらStringBuilderに文字列を追加」するようになっているのでとても便利～
super.getHand().stream().findFirst().ifPresent(c -> sb.append(c.toString()));
:::
```Dealer.java
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
:::note info
・プレイヤークラス(Humanクラスを継承してる)
・Singletonパターンじゃないので、メンバ変数を持たせてもOK
・ディーラーのsimpleな動きを親クラスで記載しているため、@&#xFFF0;Override(再定義)多め
・Splitやdouble down時の手札の動きもこっちに記載してる
:::
```java:Player.java
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
:::note info
・ここからスタート
・GameManageクラス生成する際に、軍資ベルを渡してあげている
・Mainクラスは、ただただ大枠の処理フローを流すイメージで、軽くなるように実装
:::
```java:Main.java
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
:::note info
・ブラックジャックのあれこれ(gameの開始、ユーザーの行動取得、勝負の判定後の処理、終了等)をするクラス
・GameManageクラスのコンストラクタで、軍資ベルの設定、ディーラー、プレイヤー、デッキの生成を行ってる
:::
```java:GameManage.java
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()){
			//半数以上カードが使用されている場合ｈ、シャッフルする
			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
:::note info
・コンソールに出力する処理を一元管理するメソッド
・gameInfoPrint()で、出力を1つにしているため、コンソールに文字を出さない変更もすぐ可能
:::
```java:ConsoleManager.java
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
:::note info
・共通関数クラス
・トランプのドローとか、Aの11->1への変更とか、手札が21を超えているか調べたり等してる
・Humanクラスにドローとか書きたかったけど、splitがあるせいであきらめた
・引数に手札を渡すようにして、共通関数に切り出した
:::
```java:CommonFunc.java
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
:::note info
・定数クラスです
・特にコメントはありません
:::
```java: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
:::note info
・システム定数クラス
・Userが設定することで色々な環境を変える子ができるクラス(所持ベル、デッキ数、シャッフルを行うデッキの使用率等)
:::

```java:SettingConst.java
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;
}
```
# 最後に

デバックでブラックジャックやりすぎたので、完成したころには遊ぶ気が失せてました（笑）
ソースコード見て「もっといい実装パターンあるよ」とか、「勉強になった!」とか何でもいいので、感情が動いてくれることを祈ります。
皆さんもリアルなカジノをイメージしたブラックジャックを実装してみて下さいね!!