ブラックジャックの開発に時間をかけてみたら、報酬として開発スキルがレベルアップした。

# ♣︎. はじめに

コンソール画面で操作するブラックジャックゲームを、PHPで作りました。その個人開発の過程を記事にします。

操作した時のコンソール画面はこんな感じです。

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527226/8a00bf37-8060-b3b8-e124-080b427009ba.gif)

<details><summary>（テキストだとこんな感じです）</summary><div>

```
root@9444efa94780:/var/www/html# php Main.php 
ブラックジャックの設定をします。
プレイヤーの人数を入力してください。（1〜3）
🙋‍ 1
プレイヤー1名でゲームを開始します。

あなたの持っているチップは100ドルです。
ベットする額を入力してください。（1〜1000ドル）
💲 10
10ドルをベットしました。

ブラックジャックを開始します。
あなたの引いたカードはハートの6です。
あなたの引いたカードはスペードのQです。

ディーラーの引いたカードはスペードの10です。
ディーラーの引いた2枚目のカードはわかりません。

あなたの現在の得点は16です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 SR
サレンダーを宣言しました。
ベットしたチップ10ドルの半分5ドルを戻してゲームを降ります。
ディーラーの引いた2枚目のカードはダイヤのAでした。
あなたは負けたため、チップ 5 ドルは没収されます。
あなたのチップ残高は 95 ドルです。
ブラックジャックゲームを続けますか？（Y/N）
👉 Y
あなたの持っているチップは95ドルです。
ベットする額を入力してください。（1〜1000ドル）
💲 10
10ドルをベットしました。

ブラックジャックを開始します。
あなたの引いたカードはクラブのAです。
あなたの引いたカードはクラブの3です。

ディーラーの引いたカードはスペードのJです。
ディーラーの引いた2枚目のカードはわかりません。

あなたの現在の得点は14です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 Y
あなたの引いたカードはハートの6です。
あなたの現在の得点は20です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 N
カードを引きません。

ディーラーの引いた2枚目のカードはスペードの7でした。
ディーラーの現在の得点は17です。
カードを引きません。

ディーラーの得点は17です。
あなたの勝ちです！🎉
あなたは勝ったため、チップ 10 ドルと同額の配当を得られます。
あなたのチップ残高は 105 ドルです。
ブラックジャックゲームを続けますか？（Y/N）
👉 Y
あなたの持っているチップは105ドルです。
ベットする額を入力してください。（1〜1000ドル）
💲 105
105ドルをベットしました。

ブラックジャックを開始します。
あなたの引いたカードはスペードのKです。
あなたの引いたカードはクラブの5です。

ディーラーの引いたカードはスペードの9です。
ディーラーの引いた2枚目のカードはわかりません。

あなたの現在の得点は15です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 Y
あなたの引いたカードはハートの2です。
あなたの現在の得点は17です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 N
カードを引きません。

ディーラーの引いた2枚目のカードはクラブの7でした。
ディーラーの現在の得点は16です。
ディーラーの引いたカードはハートの9です。
ディーラーの得点は25です。
合計値が21を超えたので、ディーラーはバーストしました。

あなたの勝ちです！🎉
あなたは勝ったため、チップ 105 ドルと同額の配当を得られます。
あなたのチップ残高は 210 ドルです。
ブラックジャックゲームを続けますか？（Y/N）
👉 Y
あなたの持っているチップは210ドルです。
ベットする額を入力してください。（1〜1000ドル）
💲 210
210ドルをベットしました。

ブラックジャックを開始します。
あなたの引いたカードはクラブの3です。
あなたの引いたカードはダイヤの4です。

ディーラーの引いたカードはダイヤの8です。
ディーラーの引いた2枚目のカードはわかりません。

あなたの現在の得点は7です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 Y
あなたの引いたカードはスペードの4です。
あなたの現在の得点は11です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 Y
あなたの引いたカードはクラブのKです。
あなたの現在の得点は21です。
カードを引きますか？（Y/N / DD/SP/SR）
※ 特殊ルール（DD: ダブルダウン, SP: スプリット, SR: サレンダー）は、最初に手札が配られたときのみ有効
👉 N
カードを引きません。

ディーラーの引いた2枚目のカードはハートの7でした。
ディーラーの現在の得点は15です。
ディーラーの引いたカードはハートの4です。
ディーラーの現在の得点は19です。
カードを引きません。

ディーラーの得点は19です。
あなたの勝ちです！🎉
あなたは勝ったため、チップ 210 ドルと同額の配当を得られます。
あなたのチップ残高は 420 ドルです。
ブラックジャックゲームを続けますか？（Y/N）
👉 N
ブラックジャックを終了します。
```

</div></details>
    
ゲームの概略です。

- トランプで遊ぶブラックジャックゲームの基本ルールそのままで遊べる
- チップ（架空の賭け金）をベットして、ゲームに勝ってチップを増やして遊べる
- チップに影響する特殊ルール（ダブルダウン、スプリット、サレンダー）を宣言できる

## ♣︎. きっかけ

作成しようと思ったきっかけは、Web開発の動画学習教材『独学エンジニア』の課題の一つだったからです。

https://dokugaku-engineer.com/

「PHPとオブジェクト指向」パートの集大成の課題として用意されています。

ちなみに、受講生が自力で臨むために、教材には回答例は用意されていません。

## ♣︎. 目的

作る目的は、PHP、オブジェクト指向でのコードを書く経験値を得るためです。

現在、私は異業種から転職して9ヶ月経過したところですが、実務でコードを書く機会を得られていません。そのため、いつか来る実務でコードを書く機会に備え、継続的にコードと向き合うための題材として取り組みました。

ちなみに、ブラックジャックゲームの作成にかけた時間としては、ざっと 100 時間はかけています。ブラックジャックに時間をベットして報酬として得られた経験値はとても大きかったと感じています。

## ♣︎. 環境

- macOS Big Sur バージョン: 11.6
- Visual Studio Code バージョン: 1.70.2
- Docker バージョン: 20.10.14
- PHP バージョン: 8.1.7

※ 言語フレームワークは使いません

## ♣︎. 目次

記事の前半で、開発過程を振り返り、後半で、開発過程で守るようにしたことを書きます。

[♠︎. 開発過程の振り返り](#%EF%B8%8E-開発過程の振り返り)
　[♠︎. 開発の進め方](#%EF%B8%8E-開発の進め方)
　[♠︎. 開発環境の用意](#%EF%B8%8E-開発環境の用意)
　[♠︎A. 基本ルールの実装（ステップ1）](#%EF%B8%8Ea-基本ルールの実装ステップ1)
　[♠︎2. カードAのルール追加（ステップ2）](#%EF%B8%8E2-カードaのルール追加ステップ2)
　[♠︎3. プレイヤー人数設定の追加（ステップ3）](#%EF%B8%8E3-プレイヤー人数設定の追加ステップ3)
　[♠︎4. チップ機能、特殊ルールの追加（ステップ4）](#%EF%B8%8E4-チップ機能特殊ルールの追加ステップ4)

[♦︎. 開発過程で守るようにしたこと](#%EF%B8%8E-開発過程で守るようにしたこと)
　[♦︎A. UML を書く](#%EF%B8%8Ea-uml-を書く)
　[♦︎2. テストを先に書きながら進める](#%EF%B8%8E2-テストを先に書きながら進める)
　[♦︎3. デバッグしながらコーディングする](#%EF%B8%8E3-デバッグしながらコーディングする)
　[♦︎4. 静的解析ツールは全て通る状態にする](#%EF%B8%8E4-静的解析ツールは全て通る状態にする)
　[♦︎5. 何度もリファクタリングする](#%EF%B8%8E5-何度もリファクタリングする)

# ♠︎. 開発過程の振り返り

## ♠︎. 開発の進め方

個人開発なので実装するのは自分だけですが、ブランチ作成、コード変更、プルリク、マージしながら、開発を進めました。

https://docs.github.com/ja/get-started/quickstart/github-flow

GitHub 上で Public リポジトリとして公開し、今回記事にすることも想定していたため、開発の履歴がなるべく見やすくなるようにブランチを切りながら進めることを心がけました。

https://github.com/ryamate/php-oop-cli-blackjack

今回の記事は、プルリクの履歴をベースに開発プロセスを振り返っていきます。

## ♠︎. 開発環境の用意

まずは、開発環境を用意しました。

https://github.com/ryamate/php-oop-cli-blackjack/commit/4bc51f87eb335835ce960ca11f93343db1641fac

用意すると言っても、一からではなく講師の山浦さんが講義用に作成された Docker 環境（php 8 系, apache ）に、静的解析ツールなどのライブラリを追加して使用しました。

https://github.com/dokugaku-engineer/server-side/tree/master/part3

<br><br>

## ♠︎A. 基本ルールの実装（ステップ1）

最初の課題「ステップ1」は以下の内容でした。

> **◯ステップ1**
> 
> 
> ディーラーとプレイヤーの2人で対戦するコンソールゲームを作成しましょう。
> 
> 以下のルールの元、コンソール（ターミナル）上で動作するようにします。
> 
> - プレイヤーは実行者、ディーラーはCPUが自動実行する
> - 実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
> - その後、先にプレイヤーがカードを引く。プレイヤーのカードの合計値が21を超えたらプレイヤーの負け
> - プレイヤーはカードを引くたびに次のカードを引くか選択できる
> - プレイヤーがカードを引き終えたら、ディーラーは自分のカードの合計値が17以上になるまで引き続ける
> - プレイヤーとディーラーが引き終えたら勝負。カードの合計値が21により近い方が勝ち
> - Aは1点として取り扱う
> 
> コンソール画面のイメージです。
> 
> ```
> ブラックジャックを開始します。
> あなたの引いたカードはハートの7です。
> あなたの引いたカードはクラブの8です。
> ディーラーの引いたカードはダイヤのQです。
> ディーラーの引いた2枚目のカードはわかりません。
> あなたの現在の得点は15です。カードを引きますか？（Y/N）
> Y
> あなたの引いたカードはスペードの５です。
> あなたの現在の得点は20です。カードを引きますか？（Y/N）
> N
> ディーラーの引いた2枚目のカードはダイヤの2でした。
> ディーラーの現在の得点は12です。
> ディーラーの引いたカードはハートのKです。
> あなたの得点は20です。
> ディーラーの得点は22です。
> あなたの勝ちです！
> ブラックジャックを終了します。
> ```
> 

ステップ 1 の実装は、以下の順序で進めました。
- UML の作成
- ゲームスタート部分の実装
- 基本ルールの実装完了
- 静的解析ツールでのリファクタリング
- テストコードの追記

### UML の作成

実装前にまずは PlantUML で、ユースケース図、クラス図、シーケンス図を書きました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/1

<br>

### ゲームスタート部分の実装

実装については、ここからです。

クラス図をもとにファイル作成し、シーケンス図をもとにゲームをスタートする処理を書き始めました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/2

:::note 
この段階で心がけていたことは、 **タスクを小さくばらしながら作業を進める** ことです。
タスクを分解することで、コードを書き始めるハードルを下げたり、途中で手が止まって時間だけが経過することがないよう、詰まったらタスクばらしをするようにしました。
:::

<br>

### 基本ルールの実装完了

このブランチで、ディーラーとプレイヤーの2人で対戦するコンソールゲームとして、基本的な実装が完成しました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/3

:::note 
いきなり良いコードを書こうと気負わず、 **まずはコードが汚くてもとりあえず動く状態にしよう** というスタンスで進めました。この時点では書いた自分でも読みにくいコードと感じます。
:::

<br>

### 静的解析ツールでのリファクタリング

ステップ1の仕様を満たすよう動くようになったところで、続いて、静的解析ツール（PHP_CodeSniffer, PHPMD, PHPStan）でのリファクタリングをしました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/4

### テストコードの追記

書けていないテストコードを追記しました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/5

<br><br>

## ♠︎2. カードAのルール追加（ステップ2）

課題の「ステップ2」は以下の内容でした。

> **◯ステップ2**
> 
> 
> Aを1点あるいは11点のどちらかで扱うようにプログラムを修正しましょう。
> 
> Aはカードの合計値が21以内で最大となる方で数えるようにします。
> 

### カードAのルール追加完了

機能追加として、A（エース）を1点 or 11点の都合の良い方でカウントする変更をしました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/6

```php
/**
 * A の点数については、デフォルト 11 でカウントされており、
 * 得点が21点を超えている場合は、 1 でカウントする
 *
 * @return void
 */
private function calcAceScore(): void
{
    for ($i = 0; $i < $this->countAce; $i++) {
        if ($this->scoreTotal > 21) {
            $this->scoreTotal -= 10;
        }
    }
}
```

`calcAceScore`メソッドの計算例は以下のとおりです。（「Aと5」→「A」→「7」の場合）
- 最初の手札→「Aと5」
  - `scoreTotal` は、`16`（11+5）
- 1枚引く→「A」
  - `scoreTotal` は、`27`（11+5+11）
    - Aの枚数(`countAce`)は `2` 枚
    - 21を超えない範囲で最大になるように、Aは1か11か切り替わる
    - for文は2回
      - 1回目は21を超えているので27-10=17
      - 2回目は21を超えていないので17のまま
        - `scoreTotal` は、`17`
- もう1枚引く →「7」
  - `scoreTotal` は、`34`（11+5+11+7）
  - Aの枚数(`countAce`)は `2` 枚
    - for文は2回
      - 1回目は21を超えているので34-10=24
      - 2回目も21を超えているので24-10=14
        - `scoreTotal` は、`14`

<br><br>

## ♠︎3. プレイヤー人数設定の追加（ステップ3）

課題の「ステップ3」は以下の内容でした。

> **◯ステップ3**
> 
> 
> 最大3人までのプレイヤーでプレイできるようにしましょう（ディーラーと合わせて合計4人）。増えたプレイヤーはCPUが自動的に操作します。
> 

### プレイヤー人数設定の機能追加完了

プレイヤー人数設定の機能を追加しました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/7

 「最大3人までのプレイヤーでプレイできるよう変更する」という機能追加としてはシンプルなもので、動作としては5日間（10hくらい）で完了しましたが、コードの変更がしづらいな…という感触でした。

<br>

### 静的解析ツールでのリファクタリング

静的解析ツール（PHP_CodeSniffer, PHPMD, PHPStan）でのリファクタリングを実施しました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/9

２回目の静的解析ツールでのリファクタリングですが、 PHPMD での解析で、コードがかなり複雑ですよー、と指摘があったため、大幅に書き換えました。

具体的には、`Game` クラスの `start` メソッドに処理が集中していたため、ゲームの進行ごとに処理を分散しました。分散した上で、各プレイヤーの行動や、ディーラーの行動はそれぞれのクラスへ処理を移しました。

その他、`Message` クラスを作成して、ゲーム中のメッセージをまとめました。

<br>

### UML の修正

現状のコードと合うように、 UML（クラス図、シーケンス図）を修正しました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/10

<br>

### SOLID 原則に則っての修正

クラス図を整理していて、図を見ているだけでも分かりにくい点があると感じたため、SOLID 原則の「単一責任の原則」に則って、 複数の責務を負っているクラスを切り離すにはどうすれば良いかを考えて、コードを修正をしました。

https://github.com/ryamate/php-oop-cli-blackjack/pull/11

https://github.com/ryamate/php-oop-cli-blackjack/pull/16

- `Dealer` クラスについては、カードを配る役割、プレイヤーとしての役割、勝敗を判定する役割といった複数の役割を持っている状態であったため、まずはプレイヤーとしての責務を別のクラスを作成して委譲しようとしました。
- `Dealer` クラス、`Player` クラス、`NonPlayerCharacter` クラスについては、カードをひいて 21 点以内でより高い点数を目指すという行動が同じですが、共通化できていなかったため、その点を修正しました。
- 修正方法としては、`Player` クラスを抽象クラス化し（Template Method パターン）、共通しているプレイヤーとしてのプロパティはまとめました。
- まだ、`Dealer` メソッドは、カードを配る役割、勝敗を判定する役割を持っているため、次のステップ4 実装で修正しました。
- 参考：この時点のクラス図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527226/1a649066-eec2-4c27-08b1-f7ed4af98984.png)

<br><br>

## ♠︎4. チップ機能、特殊ルールの追加（ステップ4）

課題の「ステップ4」は以下の内容でした。

> **◯ステップ4（任意）**
> 
> 
> ダブルダウン、スプリット、サレンダーのルールを追加しましょう。ルールは各自調べてみてください。
> 

### チップ機能、特殊ルールの追加完了

ブラックジャックゲームのことを元々知らなかったため、ルールをそれぞれ調査することから始めました。
調べた結果、チップ（架空の賭け金）をベットして、ゲームに勝ってチップを増やして遊べる要素を追加するところから必要ということがわかりました。ざっくり下記の追加が必要と整理し、実装しました。

- プレイヤーにチップ（賭け金）を持たせる。
- チップをベットする（賭ける）。
- 勝敗に応じて、チップ残高を計算する。
- 特殊ルール（ダブルダウン、スプリット、サレンダー）を追加する。

https://github.com/ryamate/php-oop-cli-blackjack/pull/17

ダブルダウンとサレンダーの２つは楽に実装できましたが、スプリットの実装が難しかったです。

ちなみにスプリットは、「最初に配られたカードの値が同数の場合、カードを2つに分けてそれぞれ別の手札とすることができる（最初に賭けたチップと同額がさらに必要となる）」というルールです。

ステップ4については、機能としては動いたけど、リファクタリングやテストコードの漏れなど確認できていないため、引き続き作業していきます。

「開発過程の振り返り」は以上です。

# <font color="Red">♦︎</font>. 開発過程で守るようにしたこと

ブラックジャックゲームを作る過程では、以下のことを守るようにしました。

- **UML** を書く
- **テスト** を、先に書きながら進める
- **デバッグ** しながらコーディングする
- **静的解析ツール** は、全て通る状態にする
- 何度も **リファクタリング** する

## <font color="Red">♦︎A</font>. UML を書く

スムーズに実装できるよう、設計時に UML を書き、 UML を手直しながら、実装しました。

### PlantUML の利用

下記の3つの図を PlantUML で作成しました。

- ユースケース図
- クラス図
- シーケンス図

https://plantuml.com/ja/

ブラウザ上でも上記ページ内で UML を編集できますが、 VSCode の拡張機能としてインストールして使用しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527226/2b9e3394-59b1-2779-fd61-5c7e4a0d5787.png)

:::note
作るものを明確にするのが、UMLの目的 なので、下記3点に注意しました。
- **完璧を求めない**
    - 図をきれいに整えることに価値があるわけじゃなくて、図がわかりやすいことで、見た人の理解が捗り開発がスムーズにいくことに価値がある。
- **図を整えるために時間をかけすぎない**
    - 最初にコードを書き始める前に UML を作成するときは、仕様を明確に図にするのは難しいので、コードを書いていて要件についての理解が深まったときに更新する。
    - 時間をかける費用対効果を考える。
- **UML図の間で整合性が取れているようにする**
    - クラス図とシーケンス図で整合性が取れていない、というようなことがないようにはする。
:::

「クラス図」については、実装をし始めてからも、何度も図を見返しながら実装内容を考えました。「シーケンス図」も同様に、全体の流れの把握や、どのクラスのメソッドとして実装すれば良いかなど、何度も図を見返しました。 UML を作成したり見返したりすることで、どうすれば良いか行き詰まってしまうことが少なくなると感じます。作るものが明確になる UML 作成の価値を感じられました。

作成した UML は下記のとおりです。

### ユースケース図

```plantuml
@startuml Blackjack
skinparam actorStyle awesome
left to right direction
actor Dealer as d
actor Player <<Manual>> as mp #AliceBlue
actor Player1 <<Auto>> as ap1 #LavenderBlush
actor Player2 <<Auto>> as ap2 #LavenderBlush

rectangle Blackjack {
  usecase "ゲームスタートする" as UC1
  usecase "デッキからカードを引く" as UC2
  usecase "勝敗を判定する" as UC3
  usecase "ゲームを終了する" as UC4
  UC1 --> UC2
  UC2 --> UC3
  UC3 --> UC4
}
  mp --> UC2
  ap1 -> UC2
  d -> UC2
  UC2 <-- ap2
  d --> UC3

@enduml
```
    
### クラス図

```plantuml
@startuml Blackjack

class Game {
  -string CONTINUE
  -string STOP
  -array GAME_STATUS

  -int MAX_NUM_OF_PLAYERS
  -int MAX_BET
  -Deck deck
  -Dealer dealer
  -array<int,Player> players
  -int status

  +__construct()
  +getDeck()
  +getDealer()
  +getPlayers()
  +play()
  -set()
  -placeYourBets()
  -startGame()
  -action()
  -resultGame()
  -resultCalcChips()
  -selectContinueOrStop()
  -end()
  +addPlayerAsSecondHand()
  +removeSplitPlayer()
}

class Deck {
  -array<int,array<string,int|string>> deck

  +__construct()
  +getDeck()
  +initDeck()
  +takeACard()
}

class Card {
  -array CARD_SCORE
  -array SUITS

  +createNewDeck()
}

class Dealer {
  +int NUM_OF_FIRST_HAND
  -DealerPlayer dealerPlayer
  -Judge judge
  -ChipCalculator chipCalculator
  -SpecialRule specialRule

  +__construct()
  +getDealerPlayer()
  +getJudge()
  +getChipCalculator()
  +getSpecialRule()
  +dealOutFirstHand()
  +dealOneCard()
}

class Judge {
  +checkBurst()
  +judgeWinOrLose()
  -hasStand()
  -compareScoreTotal()
}

class ChipCalculator {
  +calcChips()
  +calcChipsSplitFirstHand()
  +calcChipsSplitSecondHand()
}

class SpecialRule {
  +applySpecialRule()
  -doubleDown()
  -split()
  -surrender()

}

abstract Player {
  +HIT
  +STAND
  +BURST
  +WIN
  +LOSE
  +DRAW
  +NO_SPLIT
  +SPLIT_FIRST
  +SPLIT_SECOND

  -string name
  -int chips
  -int bets
  -array<int,array<string,int|string>> hand
  -int scoreTotal
  -int countAce
  -string status
  -int splitStatus

  +__construct()
  +getName()
  +getChips()
  +getBets()
  +getHand()
  +getScoreTotal()
  +getStatus()
  +getSplitStatus()
  +addACardToHand()
  +calcScoreTotal()
  -calcAceScore()
  +changeBets()
  +changeChips()
  +changeHand()
  +changeStatus()
  +changeSplitStatus()
  +reset()
}

interface PlayerAction {
  +action()
  +selectHitOrStand()
}

interface PlayerBet {
  +bet()
  +selectBets()
}

class DealerPlayer {
  +action()
  +selectHitOrStand()
}

class ManualPlayer {
  +bet()
  +selectBets()
  +action()
  +selectHitOrStand()
}

class AutoPlayer {
  +bet()
  +selectBets()
  +action()
  +selectHitOrStand()
}

class Validator <<trait>> {
  +validateInputYesOrNo()
  +validateInputNumOfPlayer()
  +validateInputBets()
  +validateSplitConditions()
}

class Message {
  +getPlaceYourBetsMessage()
  +getDealerFirstHandMessage()
  +getLoseByBurstMessage()

  +getScoreTotalMessage()
  +getProgressQuestionMessage()
  +getCardDrawnMessage()
  +getInputErrorMessage()

  +getScoreTotalResultMessage()
  +getStandMessage()
  +getDealerBurstMessage()
  +getWinByBurstMessage()
  +getResultMessage()
}

Game *-- Deck
Deck *-- Card
Game *-- Dealer
Deck - Dealer
Dealer - Player
Dealer *--- Judge
Dealer *---- ChipCalculator
Dealer -- SpecialRule
Game -- SpecialRule
SpecialRule -- ManualPlayer
SpecialRule -- Validator
Dealer *-- DealerPlayer
Game -- Player
Game --- ManualPlayer
Game --- AutoPlayer
Player <|--- DealerPlayer
Player <|--- ManualPlayer
Player <|--- AutoPlayer
PlayerAction <|.. DealerPlayer
PlayerAction <|.. ManualPlayer
PlayerAction <|.. AutoPlayer
PlayerBet <|.. ManualPlayer
PlayerBet <|.. AutoPlayer
ManualPlayer -- Validator
AutoPlayer -- Validator
Game ----- Validator
Game -- Message

@enduml

@enduml
```
 
### シーケンス図

```plantuml
@startuml Blackjack

title ブラックジャックゲーム全体の流れ

skinparam responseMessageBelowArrow true
skinparam actorStyle awesome

participant Main order 10
participant Game order 20
actor Dealer order 30
participant Deck order 40
participant Card order 50
actor "<<Manual>>\nPlayer" as mp order 60 #AliceBlue
actor "<<Auto>>\nPlayer" as ap order 70 #LavenderBlush


Main -> Game : Game を new する

group #LightGoldenRodYellow Game を new する
  activate Game
  Game ->  Dealer : Dealer を new する
  activate  Dealer
  Game -> Deck : Deck を new する
  activate Deck
  Deck -> Card : カード52枚を取得する
  activate Card
  Card --> Deck : カード52枚を返却する
  deactivate Card

  Game -> mp : Player を new する
  activate mp #AliceBlue
end

Main -> Game : ブラックジャックを\n開始する

group #LightGoldenRodYellow ゲームの設定をする
  note right Game #Black
  <font color="white">ブラックジャックの設定をします。
  <font color="white">プレイヤーの人数を入力してください。（1〜3）
  <font color="white">2
  <font color="white">プレイヤー2名でゲームを開始します。
  end note

  Game -> ap : 決めた人数の NPC を new する
  activate ap #LavenderBlush
end

group #LightGoldenRodYellow ベットする額を決める
  note right Game #Black
  <font color="white">あなたの持っているチップは100ドルです。
  <font color="white">ベットする額を入力してください。（1〜1000ドル）
  <font color="white">10
  <font color="white">10ドルをベットしました。
  end note
  Game -> mp : Player の ベットする額 を決めた額に変更する
end

ref over Game
  別添「１ゲームの流れ」
  ・ゲームを開始する
  ・ゲームを進行する
  ・結果を判定する
end ref

group #LightGoldenRodYellow チップを計算する
    Game -> Dealer : ディーラーはチップを計算する
    Dealer -> mp : Player の合計チップ数を変更する

    alt 勝った人（ステータス → win）
      note right Game #Black
        <font color="white">勝ったので 5 チップと同額の配当を得られます。
        <font color="white">あなたのチップ残高は 105 ドルです。
      end note

    else 負けた人（ステータス → lose）
      note right Game #Black
        <font color="white">負けたため、チップ 5 ドルは没収されます。
        <font color="white">あなたのチップ残高は 95 ドルです。
      end note
    else 引き分けた人（ステータス → draw）
      note right Game #Black
        <font color="white">引き分けたため、チップ 5 ドルはそのままです。
        <font color="white">あなたのチップ残高は 100 ドルです。
      end note
    end
end

group #LightGoldenRodYellow ゲームを続けるか、やめるかを選択する

    alt チップ残高がある場合
      note right Game #Black
        <font color="white">ブラックジャックゲームを続けますか？（Y/N）
      end note
    else チップ残高が 0 の場合
      note right Game #Black
        <font color="white">あなたは、チップの残高がなくなりました。
      end note
    end
end
alt ゲーム続行
  note over Game
    次のゲームへ
    （「ベットする額を決める」まで戻る）
  end note
else ゲーム終了
  Game --> Main : ゲーム終了
end
  deactivate Dealer
  deactivate mp
  deactivate ap
  deactivate Deck
deactivate Game
@enduml
```

```plantuml
@startuml Blackjack

title 別添「１ゲームの流れ」

skinparam responseMessageBelowArrow true
skinparam actorStyle awesome

participant Game order 20
actor Dealer order 30
participant Deck order 40
actor "<<Manual>>\nPlayer" as mp order 50 #AliceBlue
actor "<<Auto>>\nPlayer" as ap order 60 #LavenderBlush


group #LightGoldenRodYellow ゲームを開始する
  Game -> Dealer : デッキをシャッフルする
  Dealer -> Deck : デッキをシャッフルする

  Dealer -> Deck : デッキからプレイヤーに手札2枚を配る
  Deck --> mp

  Dealer -> Deck : デッキから NPC に手札2枚を配る
  Deck --> ap

  Dealer -> Deck : デッキから ディーラー に手札2枚を配る
  Deck --> Dealer

  note right Game #Black
    <font color="white">ブラックジャックを開始します。</font>
    <font color="white">あなたの引いたカードはハートの7です。</font>
    <font color="white">あなたの引いたカードはクラブの8です。</font>

    <font color="white">ディーラーの引いたカードはダイヤのQです。</font>
    <font color="white">ディーラーの引いた2枚目のカードはわかりません。</font>

    <font color="white">あなたの現在の得点は15です。カードを引きますか？（Y/N）</font>
  end note

end

group #LightGoldenRodYellow ゲームを進行する

  activate Dealer
  activate mp #AliceBlue
  activate ap #LavenderBlush
  activate Deck
  activate Game

  group #AliceBlue Player(あなた)は Hit か Stand かを選択する
    alt Y (Hit)
      Game -> mp : プレイヤーは1枚カードを引く
      mp -> Deck : 1枚カードを引く
      Deck --> mp : 1枚カードを返す
      mp -> mp : 得点を計算する

      Game -> Dealer : ディーラーはバーストか否かチェックする
      Dealer -> mp : バーストか否かチェックする

      alt カードの合計値が 21 を超えていない場合
        Dealer --> Game :

        note right Game #Black
          <font color="white">あなたの引いたカードはスペードの５です。</font>
          <font color="white">あなたの現在の得点は20です。カードを引きますか？（Y/N）</font>
        end note
      else Burst (カードの合計値が 21 を超えていた場合)
        Dealer -> mp : ステータスを\n「burst」に変更する
        Dealer --> Game :

      end
    else N (Stand)

      note right Game #Black
        <font color="white">あなたの引いた2枚目のカードはダイヤの2でした。</font>
        <font color="white">あなたの現在の得点は12です。</font>
      end note

      Game -> mp : ステータスを\n「stand」に変更する

    else 再入力
      note right Game #Black
        <font color="white">Y/N で入力してください。カードを引きますか？（Y/N）</font>
      end note
      Game -> Game : 再入力
    end

    alt Burst (プレイヤーのカードの合計値が 21 を超えていた場合)

      note right Game #Black
        <font color="white">あなたの引いたカードはダイヤのJです。</font>
        <font color="white">あなたの現在の得点は25です。</font>
        <font color="white">合計値が21を超えたので、あなたの負けです。</font>
      end note
    end
  end

  group #LavenderBlush NPC(NPC1, NPC2)がいれば Hit か Stand かを選択する（自動）
    alt Y (Hit)
      Game -> ap : NPC は1枚カードを引く
      ap -> Deck : 1枚カードを引く
      Deck --> ap : 1枚カードを返す
      ap -> ap : 得点を計算する

      Game -> Dealer : ディーラーはバーストか否かチェックする
      Dealer -> ap : バーストか否かチェックする

      alt カードの合計値が 21 を超えていない場合
        Dealer --> Game :
        note right Game #Black
          <font color="white">NPC の引いたカードはスペードの５です。</font>
          <font color="white">NPC の現在の得点は20です。カードを引きますか？</font>（Y/N）
        end note
      else Burst (カードの合計値が 21 を超えていた場合)
        Dealer -> ap : ステータスを\n「burst」に変更する
        Dealer --> Game :

      end
    else N (Stand)

      note right Game #Black
        <font color="white">NPC の引いた2枚目のカードはダイヤの2でした。</font>
        <font color="white">NPC の現在の得点は12です。</font>
      end note

      Game -> ap : ステータスを\n「stand」に変更する

    else 再入力
      note right Game #Black
        <font color="white">Y/N で入力してください。カードを引きますか？（Y/N）</font>
      end note
      Game -> Game : 再入力
    end

    alt Burst (NPC カードの合計値が 21 を超えていた場合)
      note right Game #Black
        <font color="white">NPC の引いたカードはダイヤのJです。</font>
        <font color="white">NPC の現在の得点は25です。</font>
        <font color="white">合計値が21を超えたので、 NPC の負けです。</font>
      end note
    end
  end

end

group #LightGoldenRodYellow 結果を判定する
  alt Stand (カードの合計値が 21 以下) のプレイヤーが残っていた場合

      Game -> Dealer : ディーラーは自分のカードの合計値が\n 17 以上になるまで引き続ける
    loop
      Dealer -> Deck : 1枚カードを引く
      Deck --> Dealer : 1枚カードを返す
      Dealer -> Dealer : 得点を計算する
      Dealer -> Dealer : ステータスを変更する
      note right Game #Black
        <font color="white">ディーラーの引いたカードはハートのKです。</font>
      end note
    end

    alt Burst (ディーラーのカードの合計値が 21 を超えていた場合)
      note right Game #Black
        <font color="white">ディーラーの得点は22です。</font>
        <font color="white">合計値が21を超えたので、ディーラーはバーストしました。</font>
        <font color="white">あなたの勝ちです！</font>
        <font color="white">（NPCの勝ちです！）</font>
      end note

    else ディーラーのカードの合計値が 21 を超えていない場合

      Dealer -> Dealer : 勝敗を判定する
      Dealer -> Dealer : スコアを取得する
      Dealer -> mp : スコアを取得する
      Dealer -> mp : ステータスを\nwin, lose, draw に変更する
      Dealer -> ap : ステータスを\nwin, lose, draw に変更する

      note right Game #Black
        <font color="white">あなたの勝ちです！</font>
        <font color="white">（NPCの勝ちです！）</font>
      end note
    end
  end
end

deactivate Dealer
deactivate mp
deactivate ap
deactivate Deck
deactivate Game
@enduml
```

<br><br>    

## <font color="Red">♦︎2</font>. テストを先に書きながら進める

テストを実装より先に書きながら進めようとしました。

### PHPUnit での自動テスト

テストには、 PHPUnit を使いました。

https://phpunit.readthedocs.io/ja/latest/#

:::note warn
**実装したい機能のテストを先に書いてからその機能を実装する** 形で進めるつもりでしたが、全然守れませんでした…。

テストの肝をわかっていないが故、どうしても手が止まるため、後回しになりがちで、気づいたら後で書くようになっていました。書きながら学ぶという段階にすらないと感じました。体系的なインプットが必要だと思っています。
:::

<br><br>

## <font color="Red">♦︎3</font>. デバッグしながらコーディングする

### デバッグツール Xdebug の利用

PHP のデバッグツールである Xdebug で、必要に応じてステップ実行して、デバッグしながらコーディングしました。

https://xdebug.org/

Xdebug を利用する方法としては、 Visual Studio Code の拡張機能として導入した Remote Development を利用して、 Docker コンテナ内でデバッグ機能を設定する方法で使用しています。

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack

開発過程で守るようにしたことのリストですが、デバッグツールはむしろ便利すぎて手放せないくらいです。

<br><br>

## <font color="Red">♦︎4</font>. **静的解析ツール**は全て通る状態にする

静的解析ツールは、以下の３つを導入して、チェックを通る（エラーが出ない）状態にしました。

- PHP_CodeSniffer
- PHPMD
- PHPStan

### PHP_CodeSniffer

PHP_CodeSniffer は、実装したコードがコーディング規約に則っているかをチェックしてくれます。

https://github.com/squizlabs/PHP_CodeSniffer

例えば、実行時このようなことをチェックしてくれました。

```
root@9444efa94780:/var/www/html# composer phpcs
> ./vendor/bin/phpcs --standard=phpcs.xml

FILE: /var/www/html/lib/Deck.php
--------------------------------------------------------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
--------------------------------------------------------------------------------------------------------------------------------
 41 | ERROR | [x] Parentheses must be used when instantiating a new class
    |       |     (PSR12.Classes.ClassInstantiation.MissingParentheses)
--------------------------------------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
--------------------------------------------------------------------------------------------------------------------------------
```

```php
新しいクラスをインスタンス化する際には、括弧を使用する必要があります
```

このような、コーディング規約（PSR12）に則っていない箇所を教えてくれます。

```diff_php
- $card = new Card;
+ $card = new Card();
```

<br>

### PHPMD

PHPMD は、考えられるバグや最適でないコード、複雑なコード、未使用のパラメーター、メソッド、プロパティなど、問題のありそうなコードを検知して教えてくれるツールで、良いコードに近づけてくれます。

https://github.com/phpmd/phpmd

例えば、実行時このようなことをチェックしてくれました。

```bash
root@9444efa94780:/var/www/html# composer phpmd
> ./vendor/bin/phpmd . text rulesets.xml --suffixes php --exclude node_modules,resources,storage,vendor
/var/www/html/lib/Game.php:13   ExcessiveClassComplexity  The class Game has an overall complexity of 55 which is very high. The configured complexity threshold is 50.
```

```
クラスGameの全体的な複雑度は55で、非常に高いです。設定されている複雑さの閾値は50です。
```

「複雑度」とは `if` , `while`, `for` などを足し合わせて算出されるようで、複雑さの閾値を超えるとチェックに引っ掛かります。

<br>

### PHPStan

PHPStan も、バグになりそうなところを見つけてくれるツールです。

https://github.com/phpstan/phpstan

例えば、実行時このようなことをチェックしてくれました。

```
------ -------------------------------------------------------------------------------------------------------------------- 
  Line   lib/Player.php                                                                                                      
 ------ -------------------------------------------------------------------------------------------------------------------- 
  96     Variable $message might not be defined.                                                                             
 ------ -------------------------------------------------------------------------------------------------------------------- 
```

```
変数 $message が定義されていない可能性があります。
```

という指摘があり、下記を追記して定義してから使うと解消されました。

```php
$message = '';
```

<br><br>

## <font color="Red">♦︎5</font>. 何度も**リファクタリングする**

リファクタリングについては、変更しづらかったり読みづらい箇所は読みやすいコードになるよう試行錯誤して、臆さず何度も修正しまくろうという気持ちで実装しました。

また、リファクタリングするためのチェックの観点としては、以下のとおりです。

- **SOLID原則** に則っているか
- **デザインパターン** で、適用できるものはないか
- 静的解析ツールで、複雑度のチェックをクリアしているか（→♦︎4）

### SOLID原則

SOLID原則は下記の5つの原則ですが、それぞれをチェック項目として、確認しました。

- 単一責任の原則
- オープン・クローズドの原則
- リスコフの置換原則
- インターフェース分離の原則
- 依存性逆転の原則

主に、「単一責任の原則」に則っていないと考えた箇所を書き換えました。

### デザインパターンの利用

これまでに学んだデザインパターンは以下の3つです。

- Adapterパターン
- Abstract Factoryパターン
- Compositeパターン

この3つのうちで、少なくとも、使えるパターンがないかを見比べて、導入できそうな箇所がないか照合しました。またその他利用できるパターンがないか調べて参考にしました。

<br><br>

# <font color="Red">♡</font>. おわりに

第三者にレビューしていただくことなく、ああでもないこうでもないと、ひとりで試行錯誤して書いたため、過程は泥臭く悩みながら書いたボロボロのコードです。

ただ、完成したものが少しでも良いコードになるよう、これまで学んできたことを振り返りながら、まだわからないことは新たに学びながら進んでいこう！という気概で臨みました。

機能としては完成させて、やり切った思いはあります。ただ、もっとこうしたら良くなるという、自分では気付かないことが多々あるはず...という状態なので、第三者にコードレビューをしてもらう機会を得ようと思います。（コメント欄やGitHubでのレビュー大歓迎です:bow:）

多くの指摘があるとしても、成長の機会としてありがたく受け取ります。

ブラックジャックに時間をベットして報酬として得た経験値はとても大きかったと感じています。

ありがとうございました。

## 追記：2022/12/09

コードレビューをしてもらう機会を得ましたので記事を書きました。

https://qiita.com/ryamate/items/94349b77499c61d20ec9