2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

概要

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

ルール

  • 実行開始時、ディーラーとプレイヤー全員に2枚ずつカードが配られる
  • 最大3人までのプレイヤーでプレイすることが可能(ディーラーと合わせて合計4人)
  • 増えたプレイヤーは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(1枚ずつのカード)
  • HandEvaluator.php(勝敗の判定)

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

プログラム

GitHubにもソースを公開しております。

Main.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

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

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

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

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

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

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

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

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

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

まとめ

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?