LoginSignup
7
8

More than 3 years have passed since last update.

『ブラックジャック』を実装した。

Last updated at Posted at 2019-06-13

きっかけ

プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし

「プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし」という記事に感化されて、Javaでブラックジャックを作成してみた。
意外と難しい…。設計スキルの低さが顕著に出ますね。
実装の難易度はちょうどよかったです。実装時間は3時間くらいかな?

きれいにしていきたいのでぜひご指摘お願いします。

ソース

ソースコードはgithubにアップしました。
https://github.com/akiyu33333/blackjack

開発環境

  • windows10
  • Java8
  • IntelliJ IDEA
  • Lombok 1.18.8

ソース全文(06/22時点)

仕様は Qiitaの元記事 or githubのREADMEにあるので参照ください。

※指摘を受けてリファクタリングしました。
さらにリファクタリング & Aの点数計算を実装しました。

06/22 追記
@watashi_da さんの指摘を受けてさらに修正しました。
CPUフラグを持たせればよいとアドバイスをいただきまして、リファクタリングの結果、最終的にUserDealerでクラスを分けることにしました。

Main.java

Main.java
import blackjackgame.BlackJackGame;

public class Main {
    public static void main(String[] args) {
        new BlackJackGame().start();
    }
}

BlackJackGame.java

クラス名とパッケージ名を変更した。

BlackJackGame.java
package blackjackgame;

import card.Deck;
import player.Dealer;
import player.AbstractPlayer;
import player.User;

public class BlackJackGame {

    /**
     *  ゲーム開始
     */
    public void start() {
        System.out.println("★☆★☆★☆★☆★☆★☆ ブラックジャックにようこそ! ★☆★☆★☆★☆★☆★☆\n");
        System.out.println("ゲームを開始します。\n");

        Deck deck     = new Deck();
        AbstractPlayer user   = new User("あなた");
        AbstractPlayer dealer = new Dealer("ディーラー");

        user.initCardList(deck);
        dealer.initCardList(deck);

        user.drawCard(deck);
        if(!user.isBust()) dealer.drawCard(deck);

        printGameResult(user, dealer);

        System.out.println("\nブラックジャック終了!また遊んでね★");
    }

    /**
     * ゲームの結果を表示
     * @param player1 プレイヤー1
     * @param player2 プレイヤー2
     */
    private void printGameResult(AbstractPlayer player1, AbstractPlayer player2) {
        if (player1.calcScore() == player2.calcScore()) {
            System.out.println("引き分けです。");
            return;
        }
        AbstractPlayer winner = !player1.isBust() && (player2.isBust() || player1.calcScore() > player2.calcScore())
                ? player1
                : player2;
        System.out.println( winner.getName() + "の勝ちです!" );
    }
}

Player.java → AbstractPlayer.java

lombokは必要なところだけ生成するように意識した。
最初は抽象クラスにしていたのだが、いらなくなったので具象クラスにした。
interface作って実装した方がいいのかな。
getPoint() calcScore()のラムダを書けたことに個人的に成長を感じた(笑)

getPointの名前をcalcScoreに変えた。
リーダブルコードの以下を失念していた。

多くのプログラマは、getで始まるメソッドはメンバの値を返すだけの「軽量アクセサ」であるという規約に慣れ親しんでいる。
この規約を守らなければ、誤解を招く可能性がある。

ここは計算処理が入るのでgetXXXではだめですね。

Aの計算ロジックを追加。
A以外を計算し、その後Aの枚数より計算して加算する方式にした。

06/22 追記
抽象クラスにしたため、名前も合わせて変更。

AbstractPlayer.java
package player;

import card.Card;
import card.Deck;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

public abstract class AbstractPlayer {
    protected static final int BUST_POINT = 21;

    @Getter
    private final String name;
    private List<Card> cardList = new ArrayList<>();
    @Getter
    @Setter
    private boolean isBust = false;

    public AbstractPlayer(String name) {
        this.name = name;
    }

    private void addCardList(Card card){
        cardList.add(card);
    }

    public int calcScore(){
        int score = cardList.stream().filter(card -> card.getPoint() > 1 ).mapToInt(card -> card.getPoint()).sum();
        int aceCardCount = (int) cardList.stream().filter(card -> card.getPoint() == 1 ).count();
        if (aceCardCount == 0) return score;
        int borderScore = 11 - aceCardCount;
        return score > borderScore ? score + aceCardCount : score + 10 + aceCardCount ;
    }

    public void draw(Deck deck) {
        draw(deck,false);
    }

    /**
     * 山札からカードを引く
     * @param deck     山札
     * @param isHidden 引いたカードをを隠すか
     */
    public void draw(Deck deck, boolean isHidden) {
        Card card = deck.draw();
        addCardList(card);
        if (calcScore() > BUST_POINT) setBust(true);
        String msg = isHidden
                ? this.name + "の引いたカードはわかりません。"
                : this.name + "の引いたカードは" + card.toString() + "です。";
        System.out.println( msg );
    }

    /**
     * 初期手札の作成
     * @param deck 山札
     */
    public abstract void initCardList(Deck deck);

    /**
     * 山札からカードを引く
     * @param deck 山札
     */
    public abstract void drawCard(Deck deck);
}

User.java

06/22 追記
プレイヤーとCPUを区別するため、新たに作成。

User.java
package player;

import card.Deck;

import java.util.Objects;
import java.util.Scanner;

public class User extends AbstractPlayer {
    public User(String name) {
        super(name);
    }

    @Override
    public void initCardList(Deck deck) {
        draw(deck);
        draw(deck);
    }

    @Override
    public void drawCard(Deck deck) {
        System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
        try (Scanner sc = new Scanner(System.in)) {
            String line = null;
            while (!isBust() && !Objects.equals(line, "N")) {
                System.out.println("カードを引きますか?引く場合はYを引かない場合はNを入力してください。");
                line = sc.nextLine();
                if (Objects.equals(line, "Y")) {
                    draw(deck);
                    System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
                } else if (!Objects.equals(line, "N")) {
                    System.out.println("Y/N以外が入力されました。");
                }
            }
        }
    }
}

Dealer.java

06/22 追記
プレイヤーとCPUを区別するため、新たに作成。

Dealer.java
package player;

import card.Deck;

public class Dealer extends AbstractPlayer {

    public Dealer(String name) {
        super(name);
    }

    @Override
    public void initCardList(Deck deck) {
        draw(deck);
        draw(deck, true);
    }
    @Override
    public void drawCard(Deck deck) {
        System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
        while (calcScore() < 17){
            draw(deck);
            System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
        }
    }
}

Card.java

フィールドをfinalにした。
switchと三項演算子がなんかダサい。

Card.java
package card;

import lombok.AllArgsConstructor;

@AllArgsConstructor
public class Card {

    private final Suit suit;
    private final int rank;

    private String toDisplayValue() {
        switch (this.rank) {
            case 1:
                return "A";
            case 11:
                return "J";
            case 12:
                return "Q";
            case 13:
                return "K";
            default:
                return String.valueOf(this.rank);
        }
    }
    public int getPoint() {
        return this.rank > 10 ? 10 : this.rank;
    }
    @Override
    public String toString() {
        return this.suit.getMark() + "の" + this.toDisplayValue();
    }

}

Deck.java

イニシャライズメソッドを使ってみた。
ここのラムダも気分がよかった(笑)
new ArrayList<>() を使わずに ラムダの戻り値から bill にそのまま代入したいんだけど書き方がわからなかった。
ラムダで書けたので修正。
言語は違うが、この記事を読んでなんでもかんでもforeachを使うべきではないと思った。
JavaScript で forEach を使うのは最終手段
実際JavaScriptではないので記事の内容がそのまま使えるわけではないが、考え方に感銘を受けた。
Collections.shuffle これも初めて知った。便利なメソッドがあるんだね。

Deck.java
package card;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Deck {
    private List<Card> bill;
    {
        bill = Arrays.stream(Suit.values()).flatMap(s -> IntStream.rangeClosed(1,13).mapToObj(i -> new Card(s,i))).collect(Collectors.toList());
        Collections.shuffle(bill);
    }

    public Card draw(){
        Card card = bill.get(0);
        bill.remove(0);
        return card;
    }
}

Suit.java

大好きなenum
enumでもlombokを使えることを失念していたので修正。

Suit.java
package card;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
public enum Suit {
    SPADE("スペード"),
    HEART("ハート"),
    DIAMOND("ダイヤ"),
    CLUB("クラブ");

    @Getter
    private final String mark;
}

まとめ

楽しかった。
IntelliJってJavadoc作るショートカットないんだね。
Aを1と11で振り分ける対応と Aはできたので、複数プレイヤー対応とかしていきたい。

7
8
8

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
7
8