概要
PHPの学習内容のアウトプットとして、オブジェクト指向を使用したコンソールアプリのブラックジャックゲームを作成しました。コーディングの回答例等はなく、要件指示を基に設計・コーディング・テストは自力で行いました。躓いた部分はTeratail等の利用やGitHub等を参考にしながら制作しています。
GitHub : https://github.com/jirou6699/blackjack
Twitter : https://twitter.com/manmaru6699
目的
ブラックジャックをオブジェクト指向を使って実装します。機能追加・仕様変更のしやすい構成や読みやすいコードを体感・習得するために、3ステップで制作していきます。各ステップで変更・リファクタリングした内容も記載することにしました。
ステップ1:ディーラーとプレイヤーの2人対戦型のコンソールゲーム作成
ステップ2:Aを1点or11点のどちらかで扱うようプログラムを修正
ステップ3:最大3人までのプレイヤーでプレイできるように修正
ルール・進め方
ブラックジャックはカジノで行われるカードゲームの一種です。
1〜13までの数が書かれたカード52枚を使ってゲームが行われます。指定されているルールは次の通りです。
- 実行開始時、ディーラーとプレイヤー全員に2枚ずつカードが配られる
- 自分のカードの合計値が21に近づくよう、カードを追加するか、追加しないかを決める
- カードの合計値が21を超えてしまった時点で、その場で負けが確定する
- プレイヤーはカードの合計値が21を超えない限り、好きなだけカードを追加できる
- ディーラーはカードの合計値が17を超えるまでカードを追加する
各カードの点数は次のように決まっています。
- 2から9までは、書かれている数の通りの点数
- 10,J,Q,Kは10点
- Aは1点あるいは11点として、手の点数が最大となる方で数える
このゲームには何人かのプレイヤーがおり、カードの合計値を競います。
作成時の意識
- 今実装したい機能のテストを先に書き、その機能を実装する形で進行(テストも適時修正)
- 静的解析ツールは一通り整えた状態で制作開始
- 変更しづらかったり読みづらい箇所は何度もリファクタリング
環境
- DockerでPHPのイメージを使用し、コンテナを生成・起動し制作しています。
- composer.jsonを作成し、3種類(phpcs/phpmd/phpstan)の解析ツールと自動テスト(phpunit)を導入しています。
ステップ1
ディーラーとプレイヤーの2人で対戦するコンソールゲームを作成、一番シンプルな構成です。
- コンソール(ターミナル)上で動作するよう作成
- Aは1点として取り扱う
指示されたコンソールイメージは下記の通りです。
ブラックジャックを開始します。
あなたの引いたカードはハートの7です。
あなたの引いたカードはクラブの8です。
ディーラーの引いたカードはダイヤのQです。
ディーラーの引いた2枚目のカードはわかりません。
あなたの現在の得点は15です。カードを引きますか?(Y/N)
Y
あなたの引いたカードはスペードの5です。
あなたの現在の得点は20です。カードを引きますか?(Y/N)
N
ディーラーの引いた2枚目のカードはダイヤの2でした。
ディーラーの現在の得点は12です。
ディーラーの引いたカードはハートのKです。
あなたの得点は20です。
ディーラーの得点は22です。
あなたの勝ちです!
ブラックジャックを終了します。
ユースケース図
プレーヤーは21点以上になるまでカードを引くか引かないか選択ができるものの、その他はディーラーと動きは同じだと考えユースケース図を作成しました。
プレーヤー・ディーラーそれぞれの動きを繰り返して実装できそうなイメージでした。
シーケンス図
クラス図(ステップ1)
- UserTypeをインターフェースとして設計し、PlayerとDealerに分割しました
- Gameクラスにechoの出力処理をまとめ、呼び出し処理はMainクラスで実行しました
- HandEvaluatorクラスはインスタンスを生成せず、静的処理(トレイト)で実行しました
ステップ2
ステップ1ではAを1点で計算していましたが、カードの枚数によって1点or11点のどちらかで扱い、Aはカードの合計値が21以内で最大となる方で数えるようにします。プログラムを修正しました。
課題
ステップ1では、getPointメソッドを使った手札の合計点数を下記のように記述しています。
$handにはカード枚数分の配列が入っており、カードを1枚引くごとに配列が追加されていきます。
その配列の数字部分をランクに変換し、変換した変換した配列にarray_sumを使い合計を算出しています。
step1では定数CARD_RANKでAを1点と固定していましたので、Aを1点と11点で処理が分かれるようにはできていませんでした。
// $handの配列
$hand = [['スペード', 7],['ハート', 2], ....]
// 定数カードランク
const CARD_RANK = ['A'=>1, '2'=>2, '3'=>3, ....'K'=> 10]
/**
* @param array<int,array<int,string>> $hand
* @return int
*/
public function getPoint(array $hand): int
{
$convertToRank = [];
foreach ($hand as $card) {
$convertToRank[] = self::CARD_RANK[$card[1]];
}
$totalPoints = array_sum($convertToRank);
return $totalPoints;
}
変更
他のカードの枚数・合計値によって、Aは1点or11点どちらかに条件分岐する必要がありました。
//カードを2枚の場合
$hand = [A, 4] //Aを11点とカウントし、合計15点とする
// カードを3枚の場合
$hand = [4, A, 10,] //Aを1点とカウントして、合計15点とした方がいい
21点に近い方が勝ちなので、Aを除く合計が10点以下であればAは11点として計算します。
カードが2枚であれば比較的簡単ですが、カードが3枚・4枚...と増えた時に処理に困りました。そこで定数Aを11点に変更し、asort()を使って手札を昇順に並べて順番に足した時に条件分岐を実行することにしました。
# asort($convertToRanks)を実行,Aが11なので最後になる。
$convertToRanks = [2, 5, 7, 11];
$score = 0;
foreach($convertToRanks as $rank) {
//処理を実行
}
- 2つの条件に応じて1点or11点を変更できるようにしました。
- 合計値が10点以下はAを11点として計算 又は $rankがA以外ならその数値を計算
- $rankがAの時で合計値($totalPoint)が10以上であればAを1点に変換して計算
// 合計値が10以下
private const RANK_AMOUNT = 10;
// 定数カードランク
private const CARD_RANK = ['2' => 2, '3' => 3, ...'K' => 10, 'A' => 11,];
/**
* @param array<int,array<int,int|string>> $hand
* @return int
*/
public function getPoint(array $hand): int
{
$convertToRanks = [];
foreach ($hand as $card) {
$convertToRanks[] = self::CARD_RANK[$card[1]];
}
asort($convertToRanks);
$totalPoint = 0;
foreach ($convertToRanks as $rank) {
if ($totalPoint <= self::RANK_AMOUNT || $rank !== self::CARD_RANK['A']) {
$totalPoint += $rank;
} elseif ($rank === self::CARD_RANK['A']) {
$totalPoint += 1;
}
}
return $totalPoint;
}
Step1,2動作
ステップ3
最大3人までのプレイヤーでプレイできるように変更しました。
増えたプレイヤーはCPUが自動的に操作しています。
課題
- インターフェースを使用したことでプレーヤークラスとディーラークラスで同じ処理を記述し、コードのボリュームが多い
- ステップ1、ステップ2と進めていく中で不要なコードが多く・クラス名・変数名・メソッド名等が理解しずらい状態
- プレーヤーが増えるとそれぞれの手札、追加カード、スコアの管理・呼び出し方に迷い、クラス内変数を外部から直接呼び出す状態
変更
- プレーヤー、ディーラー、サブプレーヤーで同じ処理を記述するので、インターフェースから抽象クラス(abstract)へ変更。そうすることで親クラス(UserTypeクラス)に処理を記載し、子クラス(player、dealer、subPlayer)は継承するのみにし、コード量を減らすよう調整しました
- クラス名・変数名の付け方は「リーダブルコード(書籍)」、オブジェクト指向はTechpitや「オブジェクト指向でなぜつくるのか(書籍)」等を参考に修正しました
- プレーヤー毎にクラス内変数$handを持たせ、set,getなどのアクセサメソッドを使いながら追加カードやスコアをメソッド内で計算・処理するように設計しあmした
クラス図(ステップ3)
不要なコードを削除し、クラス名・変数名・メソッド名等を見直しました
ステップ3動作
感想・今後の課題
実際に設計・コーディング・テストを自分で考えながら取り組んだことで、php・オブジェクト指向の理解が深まったと思います。反省としてテスト駆動開発という目的でしたが、先にテストより先にコードを書いてしまう事もありました。今後はオブジェクト指向のデザインパターンをしっかりと学習し、仕様変更としてダブルダウン、スプリット、サレンダーのルールを追加しより忠実に再現していきたいです。
ここまでご覧いただき、ありがとうございました。