転職したらスマレジだった件

スマレジのエンジニアやまてのテックブログです。マジレス大歓迎です。

CLIで操作するブラックジャックゲームをPHPで作ってみる - ④PHPでの実装

こんにちは!

スマレジ・テックファームのWebエンジニアやまてと申します。

はじめに

CLIで操作するブラックジャックゲームをPHPで作ってみる」の 4 記事目として、「PHPでの実装」について投稿します。

実務で未だ実装はしたことがないので、手探りで実装してきた記録という感じです。


この課題は、『独学エンジニア』というWeb開発(主にサーバーサイド)の動画学習教材の中の課題の一つです。

dokugaku-engineer.com


前回までの投稿

ryamate.hatenablog.com

ryamate.hatenablog.com

ryamate.hatenablog.com


環境


INDEX


実装の進め方

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

github.com


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



実装のステップ

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

ステップ1. 基本ルールの実装

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

◯ステップ1

ディーラーとプレイヤーの2人で対戦するコンソールゲームを作成しましょう。

以下のルールの元、コンソール(ターミナル)上で動作するようにします。

  • プレイヤーは実行者、ディーラーはCPUが自動実行する
  • 実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
  • その後、先にプレイヤーがカードを引く。プレイヤーのカードの合計値が21を超えたらプレイヤーの負け
  • プレイヤーはカードを引くたびに次のカードを引くか選択できる
  • プレイヤーがカードを引き終えたら、ディーラーは自分のカードの合計値が17以上になるまで引き続ける
  • プレイヤーとディーラーが引き終えたら勝負。カードの合計値が21により近い方が勝ち
  • Aは1点として取り扱う

コンソール画面のイメージです。

ブラックジャックを開始します。
あなたの引いたカードはハートの7です。
あなたの引いたカードはクラブの8です。
ディーラーの引いたカードはダイヤのQです。
ディーラーの引いた2枚目のカードはわかりません。
あなたの現在の得点は15です。カードを引きますか?(Y/N)
Y
あなたの引いたカードはスペードの5です。
あなたの現在の得点は20です。カードを引きますか?(Y/N)
N
ディーラーの引いた2枚目のカードはダイヤの2でした。
ディーラーの現在の得点は12です。
ディーラーの引いたカードはハートのKです。
あなたの得点は20です。
ディーラーの得点は22です。
あなたの勝ちです!
ブラックジャックを終了します。

1-1. UML の作成

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

github.com

  • UML の作成については、前回投稿しました。

ryamate.hatenablog.com


1-2. ゲームスタート部分の実装

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

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

github.com

  • クラス図をもとにファイルを作成することから始めました。
  • シーケンス図をもとにクラス図でゲームをスタートする処理を書き始めました。
<?php

namespace Blackjack;

require_once('Player.php');
require_once('Deck.php');

use Blackjack\Player;
use Blackjack\Deck;

class Game
{
    private const NUM_OF_CARDS_IN_HAND = 2;

    /**
     * コンストラクタ
     *
     * @param Deck $deck
     * @param Player $player
     * @param Player $dealer
     */
    public function __construct(
        private Deck $deck = new Deck(),
        private Player $player = new Player(),
        private Player $dealer = new Player()
    ) {
    }

    // 省略

    /**
     * ブラックジャックを開始する
     *
     * @return void
     */
    public function start()
    {
        // デッキを初期化する
        $this->deck->initDeck();

        // プレイヤーを初期化する
        // プレイヤーは手札を2枚引く
        $this->player->drawHand($this->deck);
        // デッキはカードを2枚取られる
        $this->deck->takeCard(self::NUM_OF_CARDS_IN_HAND);

        // ディーラーを初期化する
        // ディーラーは手札を2枚引く
        $this->dealer->drawHand($this->deck);
        // デッキはカードを2枚取られる
        $this->deck->takeCard(self::NUM_OF_CARDS_IN_HAND);

        // ブラックジャックの開始時メッセージを表示する
        $this->showStartMessage();

        $inputYesOrNo = trim(fgets(STDIN));
    }

    // 省略
}

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


1-3. 基本ルールの実装完了

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

github.com

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


1-4. 静的解析ツールでのリファクタリング

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

github.com


1-5. テストコードの追記

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

github.com

テストを先に書きながら進める(実装したい機能のテストを先に書いてからその機能を実装する)ことは考えていたのですが、テストの肝をわかっていないが故、どうしても手が止まるため、後回しになりがちで、書きながら学ぶという段階にすらないと感じました。体系的なインプットが必要だと思っています。



ステップ2. カードAのルール追加

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

◯ステップ2

Aを1点あるいは11点のどちらかで扱うようにプログラムを修正しましょう。

Aはカードの合計値が21以内で最大となる方で数えるようにします。

2-1. カードAのルール追加完了

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

github.com

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



ステップ3. プレイヤー人数設定の追加

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

◯ステップ3

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


3-1. プレイヤー人数設定の機能追加完了

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

github.com

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


3-2. 静的解析ツールでのリファクタリング

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

github.com

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

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

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


3-3. UML の修正

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

github.com

クラス図をパッと見ただけでも、なんかしっくり来ない感じがします。どのように修正すれば良いかは思い浮かんではいませんでしたが、何かしらの手直しが必要であることだけは感じました。


3-4. SOLID 原則に則っての修正

解決の糸口として、SOLID 原則の「単一責任の原則」に則って、 複数の責務を負っているクラスを切り離すにはどうすれば良いかを考えて修正をしました。

github.com

github.com

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



ステップ4. チップ機能、特殊ルールの追加

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

◯ステップ4(任意)

ダブルダウン、スプリット、サレンダーのルールを追加しましょう。ルールは各自調べてみてください。

4-1. チップ機能、特殊ルールの追加完了

ブラックジャックゲームのことを元々知らなかったため、ルールをそれぞれ調査することから始めました。

github.com

調べた結果、チップ(架空の賭け金)をベットして、ゲームに勝ってチップを増やして遊べる要素を追加するところから必要ということがわかりました。ざっくり下記の追加が必要と整理し、実装しました。

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

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

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

4-2. 静的解析ツールでのリファクタリングUMLの修正、テストコードの追記

最後に、静的解析ツールでのリファクタリングUMLの修正、テストコードの追記をしました。

github.com

github.com



5. 自己レビュー(反省点)

テストコード

反省点としては、テストを先に書きながら進める(実装したい機能のテストを先に書いてからその機能を実装する)ことができなかったため、習慣づけることが今後の課題です。現状は習慣づけ以前の問題で、どんなテストを書けば良いかもまだよくわかっていないため、何らかのかたちでテストコードの書き方をインプットして、一つ一つ試していく機会が必要だと感じています。

工数の見積もり

自習なので与えられた期限はないとはいえ、完成までの期限については実務を意識して、なるべく工数を見積もって進めることを習慣づけたいです。よくわからんからとりあえず2週間で、ではなく、工数の見積もりを分からないなりにもしてみて、仮でも期日を決めて、それを目処に進めるようにします。



おわりに

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

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

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

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

CLIで操作するブラックジャックゲームをPHPで作ってみる」の連載は今回で終了します。もし何か、コードに手を加えることがあったり、振り返って気づきがある機会があれば、追加で投稿していこうと思います。

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

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



と、言いつつ気を抜くとすぐ夜活したくなっちゃう。