ブラックジャックゲームをオブジェクト指向を使って実装（PHP）

## 概要
独学エンジニアさんの課題である「ブラックジャックゲームを作ろう」に取り組みました。

## ルール
* 実行開始時、ディーラーとプレイヤー全員に2枚ずつカードが配られる
* 最大３人までのプレイヤーでプレイすることが可能（ディーラーと合わせて合計４人）
* 増えたプレイヤーはCPUが自動的に操作
* 自分のカードの合計値が21に近づくよう、カードを追加するか、追加しないかを決める
* カードの合計値が21を超えてしまった時点で、その場で負けが確定する
* プレイヤーはカードの合計値が21を超えない限り、好きなだけカードを追加できる
* ディーラーはカードの合計値が17を超えるまでカードを追加する

各カードの点数は次のように決まっています。
* 2から9までは、書かれている数の通りの点数
* 10,J,Q,Kは10点
* Aは1点あるいは11点として、手の点数が最大となる方で数える

## クラス分け
レッスンで学んだ「ポーカーゲーム」のクラス分けを参考に、以下のクラス分けを行いました。
* Main.php（プログラムの実行）
* BlackJackGame.php
* User.php（抽象クラス）
* Player.php
* PlayerCpu.php
* Dealer.php
* Hand.php（手札）
* Deck.php（カードの集まり）
* Card.php（１枚ずつのカード）
* HandEvaluator.php（勝敗の判定）

悩みに悩みましたが、埒が明かないので上記で行くこととしました。

## プログラム
[GitHub](https://github.com/ryutaromiyagawa/BlackJack)にもソースを公開しております。

### Main.php
```php:Main.php
<?php

namespace BlackJack;

require_once('Player.php');
require_once('PlayerCpu.php');
require_once('BlackJackGame.php');

$player = new Player('あなた');
$cp1 = new PlayerCpu('CP1');
$cp2 = new PlayerCpu('CP2');
$game = new BlackJackGame($player, $cp1, $cp2);
$game->start();
```
### BlackJackGame.php
```php:BlackJackGame.php
<?php

namespace BlackJack;

require_once('Dealer.php');
require_once('Deck.php');
require_once('HandEvaluator.php');

class BlackJackGame
{
    private const FIRST_DRAW_NUM = 2;
    private const CONTINUE_DRAW_NUM = 1;
    private const ACTION_HIT = 'H';
    private const ACTION_STAND = 'S';
    private const DRAW = 'draw';

    /**
     * @var array<int, User>
     */
    private array $players;
    private Dealer $dealer;
    private Deck $deck;

    public function __construct(object ...$players)
    {
        $this->players = $players;
        $this->dealer = new Dealer('ディーラー');
        $this->deck = new Deck();
    }

    public function start(): void
    {
        echo 'ブラックジャックを開始します。' . PHP_EOL;

        foreach ($this->players as $player) {
            $player->drawCards($this->deck, self::FIRST_DRAW_NUM);
        }
        $this->dealer->drawCards($this->deck, self::FIRST_DRAW_NUM);

        foreach ($this->players as $player) {
            $this->actionChoice($player);
        }
        $this->actionChoice($this->dealer);

        $handEvaluator = new HandEvaluator();
        foreach ($this->players as $player) {
            echo '--------------------' . PHP_EOL;
            $winner = $handEvaluator->getWinner($player, $this->dealer);
            $this->showWinner($winner);
        }

        echo 'ブラックジャックを終了します。' . PHP_EOL;
    }

    private function actionChoice(object $user): void
    {
        while (true) {
            $user->calculateHandScore();
            if (HandEvaluator::isBust($user->getHandScore())) {
                break;
            }
            $action = $user->actionJudge();
            if ($action === self::ACTION_HIT) {
                $user->hit($this->deck, self::CONTINUE_DRAW_NUM);
            } elseif ($action === self::ACTION_STAND) {
                break;
            } else {
                echo 'HかSで入力してください。' . PHP_EOL;
            }
        }
    }

    private function showWinner(string $winner): void
    {
        if ($winner === self::DRAW) {
            echo '同点です。' . PHP_EOL;
        } else {
            echo $winner . 'の勝ちです！' . PHP_EOL;
        }
    }
}
```
### User.php
```php:User.php
<?php

namespace BlackJack;

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

abstract class User
{
    abstract public function actionJudge(): string;

    public const ACTION_HIT = 'H';
    public const ACTION_STAND = 'S';
    public const DRAW_STOP_SCORE = 17;

    public Hand $hand;

    public function __construct(protected string $name)
    {
        $this->hand = new Hand();
    }

    public function drawCards(Deck $deck, int $drawNum): void
    {
        $cards = $deck->drawCards($drawNum);
        foreach ($cards as $card) {
            echo $this->name . 'の引いたカードは' . $card->getSuit() . 'の' . $card->getNumber() . 'です。' .  PHP_EOL;
            $this->addhand($card);
        }
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function addHand(Card $card): void
    {
        $this->hand->addHand($card);
    }

    /**
     * @return array<int, Card>
     */
    public function getHand(): array
    {
        return $this->hand->getHand();
    }

    public function calculateHandScore(): void
    {
        $this->hand->calculateHandScore();
    }

    public function getHandScore(): int
    {
        return $this->hand->getHandScore();
    }

    public function hit(Deck $deck, int $drawNum): void
    {
        $this->drawCards($deck, $drawNum);
    }
}
```
### Player.php
```php:Player.php
<?php

namespace BlackJack;

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

class Player extends User
{
    public function actionJudge(): string
    {
        echo $this->name . 'の現在の得点は' . $this->getHandScore() . 'です。カードを引きますか？（H:ヒット / S:スタンド）' . PHP_EOL;
        return trim(fgets(STDIN));
    }
}
```
### Dealer.php
```php:Dealer.php
<?php

namespace BlackJack;

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

class Dealer extends User
{
    private const SECOND_CARDS_INDEX = 1;

    public function drawCards(Deck $deck, int $drawNum): void
    {
        $cards = $deck->drawCards($drawNum);
        foreach ($cards as $index => $card) {
            $this->addHand($card);
            if ($index === self::SECOND_CARDS_INDEX) {
                echo $this->name . 'の引いた2枚目のカードは分かりません。' .  PHP_EOL;
                break;
            }
            echo $this->name . 'の引いたカードは' . $card->getSuit() . 'の' . $card->getNumber() . 'です。' .  PHP_EOL;
        }
    }

    public function actionJudge(): string
    {
        $hand = $this->getHand();
        if (count($hand) === 2) {
            echo $this->name . 'の引いた2枚目のカードは' . $hand[1]->getSuit() . 'の' . $hand[1]->getNumber() . 'でした。' . PHP_EOL;
        }

        $action = self::ACTION_STAND;
        if ($this->getHandScore() < self::DRAW_STOP_SCORE) {
            $action = self::ACTION_HIT;
            echo $this->name . 'の現在の得点は' . $this->getHandScore() . 'です。' . PHP_EOL;
        }
        return $action;
    }
}
```
### PlayerCpu.php
```php:PlayerCpu.php
<?php

namespace BlackJack;

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

class PlayerCpu extends User
{
    public function actionJudge(): string
    {
        $action = self::ACTION_STAND;
        if ($this->getHandScore() < self::DRAW_STOP_SCORE) {
            $action = self::ACTION_HIT;
            echo $this->name . 'の現在の得点は' . $this->getHandScore() . 'です。' . PHP_EOL;
        }
        return $action;
    }
}
```
### Hand.php
```php:Hand.php
<?php

namespace BlackJack;

class Hand
{
    /**
     * @var array<int, Card>
     */
    private array $hand = [];
    private int $handScore = 0;

    public function addHand(Card $card): void
    {
        $this->hand[] = $card;
    }

    /**
     * @return array<int, Card>
     */
    public function getHand(): array
    {
        return $this->hand;
    }

    public function calculateHandScore(): void
    {
        $this->handScore = 0;
        foreach ($this->hand as $card) {
            if ($card->getNumber() === 'A') {
                $this->handScore += (int) $card->getRankA($this->handScore);
            } else {
                $this->handScore += (int) $card->getRank();
            }
        }
    }

    public function getHandScore(): int
    {
        return $this->handScore;
    }
}
```
### Deck.php
```php:Deck.php
<?php

namespace BlackJack;

require_once('Card.php');

class Deck
{
    /**
     * @var array<int, Card> $cards
     */
    private array $cards;

    private const CARD_SUIT = ['クラブ', 'ハート', 'スペード', 'ダイヤ'];
    private const CARD_NUMBER = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];

    public function __construct()
    {
        foreach (self::CARD_SUIT as $suit) {
            foreach (self::CARD_NUMBER as $number) {
                $this->cards[] = new Card($suit, $number);
            }
        }

        $this->shuffleCards();
    }

    /**
     * @return array<int, Card>
     */
    public function drawCards(int $drawNum): array
    {
        return array_splice($this->cards, 0, $drawNum);
    }

    private function shuffleCards(): void
    {
        shuffle($this->cards);
    }
}
```
### Card.php
```php:Card.php
<?php

namespace BlackJack;

class Card
{
    private const CARD_RANK = [
        2 => 2,
        3 => 3,
        4 => 4,
        5 => 5,
        6 => 6,
        7 => 7,
        8 => 8,
        9 => 9,
        10 => 10,
        'J' => 10,
        'Q' => 10,
        'K' => 10,
    ];
    private const CARD_RANK_MAX_A = 11;
    private const CARD_RANK_MIN_A = 1;

    public function __construct(private string $suit, private string $number)
    {
    }

    public function getSuit(): string
    {
        return $this->suit;
    }

    public function getNumber(): string
    {
        return $this->number;
    }

    public function getRank(): int
    {
        return self::CARD_RANK[$this->number];
    }

    public function getRankA(int $handScore): int
    {
        $cardRank = self::CARD_RANK_MIN_A;
        if ($handScore <= 10) {
            $cardRank = self::CARD_RANK_MAX_A;
        }
        return $cardRank;
    }
}
```
### HandEvaluator.php
```php:HandEvaluator.php
<?php

namespace BlackJack;

class HandEvaluator
{
    private const BUST_SCORE = 22;
    private const BLACK_JACK_SCORE = 21;

    public function getWinner(User $player, Dealer $dealer): string
    {
        $playerScore = $player->getHandScore();
        $playerName = $player->getName();
        $dealerScore = $dealer->getHandScore();
        $dealerName = $dealer->getName();
        echo $playerName . 'の得点は' . $playerScore . 'です。' . PHP_EOL;
        echo $dealerName . 'の得点は' . $dealerScore . 'です。' . PHP_EOL;

        $winner = 'draw';

        if ($this->isBust($playerScore)) {
            $winner = $dealerName;
        } elseif ($this->isBust($dealerScore)) {
            $winner = $playerName;
        } elseif ($this->isPlayerWinner($playerScore, $dealerScore)) {
            $winner = $playerName;
        } elseif ($this->isDealerWinner($playerScore, $dealerScore)) {
            $winner = $dealerName;
        } elseif ($this->isPush($playerScore, $dealerScore)) {
            $winner = 'draw';
        }

        return $winner;
    }

    public static function isBust(int $score): bool
    {
        return $score >= self::BUST_SCORE;
    }

    private function isPlayerWinner(int $playerScore, int $dealerScore): bool
    {
        return (self::BLACK_JACK_SCORE - $playerScore) < (self::BLACK_JACK_SCORE - $dealerScore);
    }

    private function isDealerWinner(int $playerScore, int $dealerScore): bool
    {
        return (self::BLACK_JACK_SCORE - $playerScore) > (self::BLACK_JACK_SCORE - $dealerScore);
    }

    private function isPush(int $playerScore, int $dealerScore): bool
    {
        return $playerScore === $dealerScore;
    }
}
```

## その他
レッスンで学んだ下記の静的解析も一通り行い、テスト通過しております。

* PHP_CodeSniffer
* PHPStan
* PHPMD
* PHPUnit

## まとめ

綺麗なコードではないかもしれませんが、現状の力は出し切りました。
何度も動画やメモを見直し、時間は掛かりましたが、一通り完成させられた点良かったです。
＋αの課題であった「ダブルダウン、スプリット、サレンダー」のルール追加は、リファクタリング中に上手くいっていないことが分かり、解決できなかったため断念しました。
今後のレッスンでひらめきがあれば、再挑戦したいと思います。