LoginSignup
10
6

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-11

はじめに(概要編と同じ内容)

コンソールで遊ぶブラックジャックです。

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デッキ)_概要編

パッケージ配置

パッケージエクスプローラーは、以下のように作成。
スクリーンショット 2022-03-07 140049.png

クラス設計

全然UMLとかでは無いので、理解しづらいかもしれませんが、
「頭の中のクラス設計を図にしたら」ってのが、以下のイメージです。
スクリーンショット 2022-03-09 011347.png

ソース全文

諸事情により、gitでのuploadはしてません。

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

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

bean

ちなみにJavaBeansではありません。
beanをバイト列として扱う予定がないので、implements java.io.Serializable してません。

Card.java

・トランプ柄と数字情報の列挙型クラスを持ったbean
  -> 列挙型は凄く便利!!
・toString()で、各クラスに応じた文字列表現をしよう(コメントは分かりやすく例を書く)
・オーバーライドする場合は、@￰Overrideも忘れずに書くようにしよう
・setNumberは、Aの値(1と11)を切りかえるときに使うので存在させております
・環境文字が表示できないIDEを使ってるなら、Suiteのコメントを切り替えてください

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

・トランプのListを持つデッキ(山札)bean
  -> 仕様上、検索(ランダムアクセス)が多い かつ 作成・削除が頻繁では無いので、
   LinkedListではなくArrayListを使用する
・複数デッキを作成する時は、ディープコピー(×シャローコピー)を行う
・以下のようだと、格納しているのが参照型の場合、ディープコピーにならないので注意
 new ArrayList<Obj>(コピー元List);

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

	/**
	 * 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で持たせる

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

・ディーラークラス(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

・プレイヤークラス(Humanクラスを継承してる)
・Singletonパターンじゃないので、メンバ変数を持たせてもOK
・ディーラーのsimpleな動きを親クラスで記載しているため、@￰Override(再定義)多め
・Splitやdouble down時の手札の動きもこっちに記載してる

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

・ここからスタート
・GameManageクラス生成する際に、軍資ベルを渡してあげている
・Mainクラスは、ただただ大枠の処理フローを流すイメージで、軽くなるように実装

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

・ブラックジャックのあれこれ(gameの開始、ユーザーの行動取得、勝負の判定後の処理、終了等)をするクラス
・GameManageクラスのコンストラクタで、軍資ベルの設定、ディーラー、プレイヤー、デッキの生成を行ってる

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()){
			//半数以上カードが使用されている場合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つにしているため、コンソールに文字を出さない変更もすぐ可能

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

・共通関数クラス
・トランプのドローとか、Aの11->1への変更とか、手札が21を超えているか調べたり等してる
・Humanクラスにドローとか書きたかったけど、splitがあるせいであきらめた
・引数に手札を渡すようにして、共通関数に切り出した

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

・定数クラスです
・特にコメントはありません

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が設定することで色々な環境を変える子ができるクラス(所持ベル、デッキ数、シャッフルを行うデッキの使用率等)

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

最後に

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

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6