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

# きっかけ
[プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)

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

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

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

# 開発環境
- windows10
- Java8
- IntelliJ IDEA
- Lombok 1.18.8

# ソース全文(06/22時点)
仕様は [Qiitaの元記事](https://qiita.com/hirossyi73/items/cf8648c31898216312e5) or [githubのREADME](https://github.com/akiyu33333/blackjack/blob/master/README.md)にあるので参照ください。

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

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

## Main.java

```java:Main.java
import blackjackgame.BlackJackGame;

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

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

```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 追記
抽象クラスにしたため、名前も合わせて変更。

```java: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を区別するため、新たに作成。

```java: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を区別するため、新たに作成。

```java: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`と三項演算子がなんかダサい。

```java: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 を使うのは最終手段](https://qiita.com/diescake/items/70d9b0cbd4e3d5cc6fce)
実際JavaScriptではないので記事の内容がそのまま使えるわけではないが、考え方に感銘を受けた。
`Collections.shuffle` これも初めて知った。便利なメソッドがあるんだね。

```java: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`を使えることを失念していたので修正。

```java: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はできたので、複数プレイヤー対応とかしていきたい。