はじめに
独学エンジニアという教材を使用してプログラミングの学習をしています。
今回のブラックジャックゲーム開発は、【PART3 PHPとオブジェクト指向】の学習内容を集大成とするものです。その個人開発の過程を記録しました。
なぜブラックジャックなのか
- ルールがシンプルで、基本的なプログラム構造や条件分岐、ループなどの概念を学ぶのに適している。
- 実際のカードゲームとして広く知られており、多くの人にとって馴染み深い。
- プロジェクトが小規模から始められる。
- 基本的なバージョンを作成した後、さまざまな拡張や改善を加えることができる。
上記の理由から、初学者が『PHPの基礎とオブジェクト指向の習得』を証明するプロジェクトにブラックジャックはちょうど良いと言えそうです。
目的
- これまで学んだ『PHPの基礎とオブジェクト指向』の理解を深めるため。
- 学習が身についていることを証明するため。
開発をはじめる
2023年8月、子どもの幼稚園が夏休み中、ブラックジャックの開発にとりかかりました。
私にとっても夏休みの宿題だなと思って取り組んでいます。
開発環境
- Windows11 Home 16GB バージョン: 22H2
- Visual Studio Code バージョン: 1.81.0
- Docker Desktop for Windows v4.19.0
- PHP バージョン: 8.2.9
開発にあたってのポイント
- 回答例はないので自力で実装すること。
- ほかの人に見られることを意識してコードを書く。
- 講義を見直して何度もコードをリファクタリングする。
- 実務を見越してGitHub上でIssueを立てて開発を進める。
環境構築
講義用に作成されていたDockerfileを元に、静的解析ツールなどのライブラリを追加していきました。
使用したライブラリ
- Composer(PHPの依存管理ツール)
- Xdebug(デバッガ)
- PHP_CodeSniffer(コーディング規約に準拠しているかを検証)
- PHPMD(コード品質を向上)
- PHPStan(バグやエラーの検出)
基本ルールの実装(ステップ1)
◯ステップ1
ディーラーとプレイヤーの2人で対戦するコンソールゲームを作成しましょう。
以下のルールの元、コンソール(ターミナル)上で動作するようにします。
- プレイヤーは実行者、ディーラーはCPUが自動実行する
- 実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
- その後、先にプレイヤーがカードを引く。プレイヤーのカードの合計値が21を超えたらプレイヤーの負け
- プレイヤーはカードを引くたびに次のカードを引くか選択できる
- プレイヤーがカードを引き終えたら、ディーラーは自分のカードの合計値が17以上になるまで引き続ける
- プレイヤーとディーラーが引き終えたら勝負。カードの合計値が21により近い方が勝ち
- Aは1点として取り扱う
- 2から9までは、書かれている数の通りの点数
- 10,J,Q,Kは10点
UMLの作成
PlantUMLを用いて クラス図、シーケンス図、ユースケース図 を書きました。
下記は何度か修正を加え、ステップ1の最後に完成したものです。
クラス図
シーケンス図
ユースケース図
テストの作成
実装前にテストを書くと良いとされていますが、コードが増えてくると後回しになっていました。
テストしやすいようにコードを書くことは意識できたものの、『実装前にテストを書く』ということを練習する必要性を感じました。
テストにはPHPUnitを使用しています。
ゲームスタート部分の実装
最初に実装したコードがこちらになります。
UMLを先に作っていたことでコードがとても書きやすかったです。
下記を意識することも実装のしやすさに繋がったと実感しています。
・タスクばらしをする。
・コメントアウトで実装したいことを先に書く。
静的解析ツールを用いた修正
大まかな区切りの良いところで PHP_CodeSniffer, PHPMD, PHPStan を用い、エラーをちょっとずつ潰していきました。大量のエラーが減っていくと気持ちがスッキリします。
リファクタリング
「実装できたぞ~!」と書いたコードを見直してみると、なんともゴチャゴチャしているではありませんか。自分で気付いた箇所+GhatGPTにも指摘してもらいながら見直しました。
- 単一責任の原則による見直し
モジュールの中で異なるアクターのコードを分割し、新しくクラスを作りました。
例えば、
メッセージを表示するメソッドの中で勝者を決めるロジックを書いていたので
新しくHandJudgerクラスを作成しました。
- 関数名の見直し
実際のロジック内容と関数名に隔たりがある場合があったので修正しました。
ルールの追加(ステップ2)
A(エース)を1点あるいは11点のどちらかで扱うようにプログラムを修正します。
◯ステップ2
Aを1点あるいは11点のどちらかで扱うようにプログラムを修正しましょう。
Aはカードの合計値が21以内で最大となる方で数えるようにします。
Aルールの実装
思ったよりすぐに実装できた!と思ったのですが、改めてコンソール画面を実行してみると致命的なバグが発生していました。
致命的なバグとは、持ち札の点数やAの枚数によってAルールが適応されないことです。
バグの最たる原因は、実装してからテストを書いていたことです。
(誤った実装なのにテストが都合よく通ってしまう…。)
始めにテストを書くことが大事だなと実感しました。
Aルールの実装例
Aルールの実装を考え直し、以下の関数を使用することに落ち着きました。
- Aの出現回数を調べる関数
- Aの出現回数内で減算分を計算する関数
// Aの出現回数を調べる
function countAce(array $cards): int
{
$aceCount = 0;
foreach ($cards as $card) {
if ($card->getNumber() == 'A') {
$aceCount++;
}
}
return $aceCount;
}
// Aの出現回数内で減算分を計算
function subtractAceScore($participantRule, int $totalScore, array $cards): int
{
$subtractedTotalScore = $totalScore;
$aceCount = $this->countAce($cards);
// Aの出現回数内で減算分を計算
for ($i = 0; $i < $aceCount; $i++) {
if ($participantRule->isBust($subtractedTotalScore)) {
$subtractedTotalScore -= 10;
}
}
return $subtractedTotalScore;
}
コンソール画面
ステップ2(Aルールの追加)まで実装した時点での実行画面です。
ブラックジャックを開始します。
あなたの引いたカードはハートの2です。
あなたの引いたカードはハートの7です。
ディーラーの引いたカードはクラブの5です。
ディーラーの引いた2枚目のカードはわかりません。
あなたの現在の得点は9です。カードを引きますか?(y/N)
y
あなたの引いたカードはスペードのAです。
あなたの現在の得点は20です。カードを引きますか?(y/N)
y
あなたの引いたカードはハートの9です。
あなたの現在の得点は19です。カードを引きますか?(y/N)
N
ディーラーの引いた2枚目のカードはダイヤの4でした。
ディーラーの現在の得点は9です。
ディーラーがカードを引きます。
ディーラーの引いたカードはダイヤのKです。
判定に移ります。
----- 判定結果 -----
あなたの得点は19です。
ディーラーの得点は19です。
同点でした。この勝負は引き分けとします。
ブラックジャックを終了します。
ルールの追加(ステップ3)
最大3人までのプレイヤーでプレイできるようにプログラムを修正します。
◯ステップ3
最大3人までのプレイヤーでプレイできるようにしましょう(ディーラーと合わせて合計4人)。
増えたプレイヤーはCPUが自動的に操作します。
プレイヤー選択の実装
CPUプレイヤーが増えたことでコードがかなり複雑になることを実感しました。
コードが複雑になるなか、下記の2つを意識しました。
- オブジェクト指向を意識してリファクタリングを行う。
- UMLを修正しながら実装する。
これらを意識することで、方向性を見失わずコードを書き進められたと感じます。
UMLの修正
ステップ3を終えた時点での クラス図、シーケンス図、ユースケース図 です。
PlantUMLを用いています。
クラス図
シーケンス図
ユースケース図
コンソール画面
ステップ3まで実装した時点での実行画面です。
プレイヤーの人数(1人~3人)を選択してください(1~3の数値を入力)
2
ブラックジャックを開始します。
あなたの引いたカードはダイヤの8です。
あなたの引いたカードはハートのQです。
CPUプレイヤー1の引いたカードはクラブのAです。
CPUプレイヤー1の引いたカードはクラブのJです。
ディーラーの引いたカードはダイヤの6です。
ディーラーの引いた2枚目のカードはわかりません。
あなたの現在の得点は18です。カードを引きますか?(y/N)
N
ディーラーの引いた2枚目のカードはハートの10でした。
ディーラーの現在の得点は16です。
ディーラーがカードを引きます。
ディーラーの引いたカードはクラブの8です。
判定に移ります。
---------- 得点発表 ----------
ディーラーの得点は24です。
あなたの得点は18です。
CPUプレイヤー1の得点は21です。
-----------------------------
---------- 結果発表 ----------
ディーラーはバーストしました。
あなたの勝ちです。
CPUプレイヤー1の勝ちです。
------------------------------
ブラックジャックを終了します。
ルールの追加(ステップ4)
取り組み中です...
反省点
Gitの仕組みをよく理解しておらず、detached HEAD状態で作業してしまいました。
解消しようとしてコミット履歴がぐちゃぐちゃになり焦ります。
基本仕組みを理解することが最優先だなと痛感しました。
次回にプログラムを作成する場合は、Gitの理解を深めてから臨みたいです。
おわりに
プログラムの開発は地道な作業の繰り返しでした。
- UML作成 -> テスト作成 -> コードを書く -> リファクタリング -> UML修正-> テスト修正 -> …
地道な作業ですが、とても楽しく取り組むことができました。
(育児中の息抜きになっていました。)
オブジェクト指向の概念も、小指の先ほどですが掴めたのではないかと思います。
+゚ ゚*。*⌒*。゚*⌒*゚。*⌒*。゚*⌒*゚。゚*⌒*゚。*゚*
ここまで読んでくださりありがとうございました☺