概要
普段はフレームワークを使用してるので、オブジェクト指向の復習をかねて素のPHPでブラックジャックを作る
最終的なゴールのイメージは↓こんな感じでcliの操作で簡単な対戦ができるようにする
※わかりづらいコードがあるかもしれません。その際は指摘していただけると嬉しいです。
今回作るブラックジャック
ブラックジャックにはいくつかのルールがあるので、今回は下記のルールを基に実装する
・ディーラーとプレイヤー(自分)の1対1の勝負とする
・Aの得点は1点もしくは11点のどちらかとする(合計が21に近くなるようにする)
・J,Q,Kは全て10点扱い
・ディーラーは17以上になるまでカードを引き続ける
・21点を超えたらバーストとする
・お互いバーストしていなかったら21に近い方が勝ち
何のオブジェクトを用意するか考える
必要になりそうなオブジェクトをざっくり考えると・・・
・カード(一枚ずつ)
・デッキ(カードの集まり)
・プレイヤー
・ディーラー
・手札
・手札の判定(バーストしたか、勝者を決める)
・対戦(各オブジェクトを使用してゲームの進行を行う)
もっといい分け方があるかもしれませんが、ひとまず上記のクラスで書いていく
オブジェクト指向設計の考え方は以下の記事がわかりやすくまとめられてると思いました
https://zenn.dev/shimotaroo/articles/77223e7652083e
プログラム書いてく
カードクラス
<?php
namespace BlackJack;
class BlackJackCard
{
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,
];
public const MAX_A_RANK = 11;
public const MIN_A_RANK = 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];
}
}
- カードクラスの構成
- 数字と得点の配列、Aの最大得点、最小得点を定数定義
- スートとナンバーを受け取るコンストラクタ
- スート、ナンバー、得点を取得できるメソッド
デッキクラス
<?php
namespace BlackJack;
require_once('BlackJackCard.php');
class BlackJackDeck
{
private const SUIT = ['ハート', 'ダイヤ', 'クローバー', 'スペード'];
private const NUMBER = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
private array $deck;
public function __construct()
{
foreach (self::SUIT as $suit) {
foreach (self::NUMBER as $number) {
$this->deck[] = new BlackJackCard($suit, $number);
}
}
$this->shuffleDeck();
}
public function drawCard(): BlackJackCard
{
$drawCard = array_splice($this->deck, 0, 1)[0];
return $drawCard;
}
private function shuffleDeck(): void
{
shuffle($this->deck);
}
}
- デッキクラスの構成
- スートとナンバーの定数定義
- スートとナンバーの全通りでデッキを作成、シャッフル
- カードを一枚引くメソッド
手札クラス
<?php
namespace BlackJack;
class BlackJackHand
{
private const BLACKJACK_HALF = 11;
private array $hand = [];
public function addHand(BlackJackCard $card): void
{
$this->hand[] = $card;
}
public function getHand(): array
{
return $this->hand;
}
public function getCountHandNumber(): int
{
return count($this->hand);
}
public function getHandScore(): int
{
$handScore = 0;
$countA = 0;
foreach ($this->hand as $card) {
if ($card->getNumber() === 'A') {
$countA ++;
} else {
$handScore += $card->getRank();
}
}
for ($i=0; $i<$countA; $i++) {
if ($handScore <= self::BLACKJACK_HALF) {
$handScore += BlackJackCard::MAX_A_RANK;
} else {
$handScore += BlackJackCard::MIN_A_RANK;
}
}
return $handScore;
}
}
- 手札クラスの構成
- 手札にカードを加えるメソッド
- 手札のカードや枚数、合計得点を取得するメソッド
プレイヤーとディーラークラス
どちらもほとんど同じメソッドを実装していたので抽象クラスを作ることにした
<?php
namespace BlackJack;
require_once('BlackJackHand.php');
abstract class Player
{
private BlackJackHand $hand;
abstract public function drawCard(BlackJackDeck $deck): string;
public function __construct(private string $name)
{
$this->hand = new BlackJackHand();
}
public function getName(): string
{
return $this->name;
}
public function addHand(BlackJackCard $card): void
{
$this->hand->addHand($card);
}
public function getHand(): array
{
return $this->hand->getHand();
}
public function getHandScore(): int
{
return $this->hand->getHandScore();
}
public function getCountHandNumber(): int
{
return $this->hand->getCountHandNumber();
}
}
<?php
namespace BlackJack;
require_once('Player.php');
class BlackJackPlayer extends Player
{
public function __construct(private string $name)
{
parent::__construct($name);
}
public function drawCard(BlackJackDeck $deck): string
{
$card = $deck->drawCard();
$this->addHand($card);
return $this->name . 'の引いたカードは' . $card->getSuit() . 'の' . $card->getNumber() . 'です。' . PHP_EOL;
}
}
<?php
namespace BlackJack;
require_once('Player.php');
class BlackJackDealer extends Player
{
public function __construct(private string $name)
{
parent::__construct($name);
}
public function drawCard(BlackJackDeck $deck): string
{
$card = $deck->drawCard();
$this->addHand($card);
if ($this->getCountHandNumber() === 2) {
return $this->name . 'の引いた2枚目のカードはわかりません。' . PHP_EOL;
}
return $this->name . 'の引いたカードは' . $card->getSuit() . 'の' . $card->getNumber() . 'です' . PHP_EOL;
}
}
- プレイヤーとディーラークラスの構成
- 抽象メソッド(プレイヤーとディーラーで共通のメソッド)を継承
- drawCardメソッドはプレイヤーとディーラーで異なるため(プレイヤーは選択式、ディーラーは自動で出力する文言が異なる)別々に定義
手札の判定クラス
このクラスはgetWinnerメソッドは分岐が多いので、リファクタリングする必要がありそう
<?php
namespace BlackJack;
class BlackJackHandEvaluator
{
private const BLACK_JACK = 21;
private const HAND_BURST = 22;
private const DRAW = 'Draw';
public function getWinner(Player $dealer, Player $player): string
{
$dealerHandScore = $dealer->getHandScore();
$playerHandScore = $player->getHandScore();
$dealerHandNumber = $dealer->getCountHandNumber();
$playerHandNumber = $player->getCountHandNumber();
$dealerName = $dealer->getName();
$playerName = $player->getName();
if ($this->isBust($dealerHandScore) && $this->isBust($playerHandScore)) {
return $dealerName;
} elseif ($this->isBust($playerHandScore)) {
return $dealerName;
} elseif ($this->isBust($dealerHandScore)) {
return $playerName;
} elseif ($dealerHandScore > $playerHandScore) {
return $dealerName;
} elseif ($playerHandScore > $dealerHandScore) {
return $playerName;
} else {
if ($this->isBlackJack($dealerHandScore, $dealerHandNumber) && $this->isBlackJack($playerHandScore, $playerHandNumber)) {
return self::DRAW;
} elseif ($this->isBlackJack($dealerHandScore, $dealerHandNumber)) {
return $dealerName;
} elseif ($this->isBlackJack($playerHandScore, $playerHandNumber)) {
return $playerName;
} else {
return self::DRAW;
}
}
}
public function isBust(int $handScore): bool
{
return $handScore >= self::HAND_BURST;
}
private function isBlackJack(int $handScore, int $handNumber): bool
{
return $handScore === self::BLACK_JACK && $handNumber === 2;
}
}
- 手札の判定クラスの構成
- プレイヤーとディーラーの手札を引数に勝者を返すメソッド
ゲームクラス
ゲームクラスは単純で今までのクラスのメソッドを呼び出しているだけ
<?php
namespace BlackJack;
require_once('BlackJackDeck.php');
require_once('BlackJackHandEvaluator.php');
class BlackJackGame
{
private const DEALER_DRAW_STOP = 17;
private const DRAW = 'Draw';
private BlackJackHandEvaluator $handEvaluator;
private BlackJackDeck $deck;
public function __construct(private Player $dealer, private Player $player)
{
$this->handEvaluator = new BlackJackHandEvaluator();
$this->deck = new BlackJackDeck();
}
public function start(): void
{
echo 'ブラックジャックを開始します。' . PHP_EOL;
echo '' . PHP_EOL;
// 初期手札の取得処理
$this->init($this->player);
$this->init($this->dealer);
// ヒット or スタンド選択
$this->playerTurn();
$this->dealerTurn();
// 勝者を出力
$this->matchPlayerVsDealer($this->player, $this->dealer);
echo '' . PHP_EOL;
echo 'ブラックジャックを終了します。' . PHP_EOL;
}
private function init(Player $player): void
{
for ($i=0; $i<2; $i++) {
$initDrawCard = $player->drawCard($this->deck);
echo $initDrawCard;
}
}
private function playerTurn(): void
{
$playerName = $this->player->getName();
while (true) {
echo $playerName . 'の現在の得点は' . $this->player->getHandScore() . 'です。カードを引きますか?(Y/N)' . PHP_EOL;
$answer = trim(fgets(STDIN));
if ($answer === 'Y') {
$playerDrawCard = $this->player->drawCard($this->deck);
echo $playerDrawCard;
if ($this->handEvaluator->isBust($this->player->getHandScore())) {
break;
}
} elseif ($answer === 'N') {
break;
} else {
echo ' Y か N を選択して入力してください。' . PHP_EOL;
}
}
}
private function dealerTurn(): void
{
$dealerName = $this->dealer->getName();
$dealerBackCard = $this->dealer->getHand()[1];
echo $dealerName . 'の引いた2枚目のカードは' . $dealerBackCard->getSuit() . 'の' . $dealerBackCard->getNumber() . 'でした。' . PHP_EOL;
echo $dealerName . 'の現在の得点は' . $this->dealer->getHandScore() . 'です。' . PHP_EOL;
while (true) {
if ($this->dealer->getHandScore() < self::DEALER_DRAW_STOP) {
$dealerDrawCard = $this->dealer->drawCard($this->deck);
echo $dealerDrawCard;
echo $dealerName . 'の現在の得点は' . $this->dealer->getHandScore() . 'です。' . PHP_EOL;
} else {
break;
}
}
}
private function matchPlayerVsDealer(Player $player, Player $dealer): void
{
$playerName = $player->getName();
$dealerName = $dealer->getName();
echo $playerName . 'の得点は' . $player->getHandScore() . 'です。' . PHP_EOL;
echo $dealerName . 'の得点は' . $dealer->getHandScore() . 'です。' . PHP_EOL;
$winner = $this->handEvaluator->getWinner($dealer, $player);
if ($winner === self::DRAW) {
echo '引き分けです。' . PHP_EOL;
} else {
echo $winner . 'の勝ちです。' . PHP_EOL;
}
}
}
- ゲームクラスの構成
- 各オブジェクトのメソッドを呼び出し、cliに出力する
実行プログラム
<?php
namespace BlackJack;
require_once('BlackJackGame.php');
require_once('BlackJackDealer.php');
require_once('BlackJackPlayer.php');
$dealer = new BlackJackDealer('ディーラー');
$player = new BlackJackPlayer('私');
$game = new BlackJackGame($dealer, $player);
$game->start();
まとめ
だいぶオブジェクト指向の復習にはなりました。
けど課題はまだまだありそうです。
例えば、スートやナンバーを定数ではなく、クラスにするとか。
今後はブラックジャックにはまだまだルールがあるのでそれを実装したり、1対1だけではなくて任意の人数で対戦できるようにしたり、そもそもチップを賭ける機能を追加したり。
そこまでやるとなるとweb上で動くアプリにした方が良さそうかなとも思いました。