概要
独学エンジニアさんの課題である「ブラックジャックゲームを作ろう」に取り組みました。
ルール
- 実行開始時、ディーラーとプレイヤー全員に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
<?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
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
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
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
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
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
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
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
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
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
まとめ
綺麗なコードではないかもしれませんが、現状の力は出し切りました。
何度も動画やメモを見直し、時間は掛かりましたが、一通り完成させられた点良かったです。
+αの課題であった「ダブルダウン、スプリット、サレンダー」のルール追加は、リファクタリング中に上手くいっていないことが分かり、解決できなかったため断念しました。
今後のレッスンでひらめきがあれば、再挑戦したいと思います。