簡易ブラックジャックを作ってPHP8.1の列挙型ENUMを試す

# はじめに
2021/11/25に [PHP8.1](https://www.php.net/releases/8.1/en.php) がリリースされました。
今回は8.1で追加された新機能の __列挙型ENUM__ を試してみたいと思います。


# 今回作ったもの
ドキュメントにトランプの例が載っていたのでブラックジャックでも作ってみることにしました。
今回は __52枚のデッキを作る__ ことと __ブラックジャックのカードの数え方__ しか重要じゃないので肝心のブラックジャック部分はめちゃめちゃ雑に作りましたのでご容赦ください。
コード全体は長いのでページ下部に記載します。

# ENUMについて
ENUMについてはドキュメントなどの方がかなり詳細に書かれているので僕の方では作ったものの中で重要な部分のみ取り扱います。

# 52枚のデッキを作る
ENUMはスカラー値をセットできます。全てのcaseでスカラー値を持つものをBacked Enumと呼び、読み取り専用のプロパティvalueを持ちます。関連するデータのないcaseでできたものをPure Enumと呼びます。
Pure Enum と Backed Enumは ともにインターフェース UnitEnum を実装しています。UnitEnumは static メソッド cases() を実装していて、定義された　case　を順に配列を返します。
トランプの13*4のデッキを用意しました。ループして配列にぶち込んだだけです。ジョーカーはそこまで重要じゃないので無視しました。


```php
enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

enum Rank: int {
    case Deuce = 2;
    case Three = 3;
    case Four = 4;
    case Five = 5;
    case Six = 6;
    case Seven = 7;
    case Eight = 8;
    case Nine = 9;
    case Ten = 10;
    case Jack = 11;
    case Queen = 12;
    case King = 13;
    case Ace = 1;
}

function newDeck()
{
    foreach (Suit::cases() as $suit_row) {
        foreach (Rank::cases() as $rank_row) {
            $deck[] = [
                'suit' => $suit_row,
                'rank' => $rank_row,
            ];
        }
    }
}

print Suit::Clubs->value;
// "C"
```


# ブラックジャックのカードの数え方
ブラックジャックの数値は
2~10 → その数値
J, Q, K → 10
A → 1 or 11
でカウントします。
ENUM はメソッドを含めることができます。またインターフェースを実装することもできます。
ブラックジャックのカウント方法を雑に実装してみます。

```php
interface Counter
{
    public function count($bust): int;
}

enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

enum Rank: int implements Counter {
    case Deuce = 2;
    case Three = 3;
    case Four = 4;
    case Five = 5;
    case Six = 6;
    case Seven = 7;
    case Eight = 8;
    case Nine = 9;
    case Ten = 10;
    case Jack = 11;
    case Queen = 12;
    case King = 13;
    case Ace = 1;
    
    public function count($bust): int
    {
        return match($this) {
            Rank::Deuce, Rank::Three, Rank::Four, Rank::Five, Rank::Six, Rank::Seven, Rank::Eight, Rank::Nine, Rank::Ten => $this->value,
            Rank::Jack, Rank::Queen, Rank::King => 10,
            Rank::Ace => $bust ? 1 : 11,
        };
    }
}

```


# コード全体
ENUMを使用することに関しては上記でほぼ終えているので冒頭でも触れましたが、ブラックジャック部分はめちゃめちゃ雑に作りました。

```php
<?php

interface Counter
{
    public function count($bust): int;
}

enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

enum Rank: int implements Counter {
    case Deuce = 2;
    case Three = 3;
    case Four = 4;
    case Five = 5;
    case Six = 6;
    case Seven = 7;
    case Eight = 8;
    case Nine = 9;
    case Ten = 10;
    case Jack = 11;
    case Queen = 12;
    case King = 13;
    case Ace = 1;
    
    public function count($bust): int
    {
        return match($this) {
            Rank::Deuce, Rank::Three, Rank::Four, Rank::Five, Rank::Six, Rank::Seven, Rank::Eight, Rank::Nine, Rank::Ten => $this->value,
            Rank::Jack, Rank::Queen, Rank::King => 10,
            Rank::Ace => $bust ? 1 : 11,
        };
    }
}


class BlackJack {
    private array $deck = [];
    private array $player = [];
    private array $dealer = [];

    public function play()
    {
        $this->newDeck();
        shuffle($this->deck);
        $this->deal();
        $this->showHands(false);
        $this->checkPlayer();
        $this->hit();
        $this->showHands(true);
        $this->checkPlayer();
        $this->checkDealer();
    }

    private function newDeck()
    {
        foreach (Suit::cases() as $suit_row) {
            foreach (Rank::cases() as $rank_row) {
                $this->deck[] = [
                    'suit' => $suit_row,
                    'rank' => $rank_row,
                ];
            }
        }
    }

    private function deal()
    {
        $this->player = array_splice($this->deck, 0, 2);
        $this->dealer = array_splice($this->deck, 0, 2);
    }

    private function hit()
    {
        echo 'hit' . PHP_EOL;
        $this->player[] = array_splice($this->deck, 0, 1)[0];
    }

    private function blackJackCount($hands)
    {
        $sum = 0;
        foreach ($hands as $row) {
            $sum += $row['rank']->count(false);
        }
        if ($sum > 21) {
            foreach ($hands as $row) {
                $sum += $row['rank']->count(true);
            }
        }
        return $sum;
    }

    private function isBust($sum): bool
    {
        return $sum > 21;
    }

    private function isBlackJack($sum): bool
    {
        return $sum === 21;
    }

    private function checkPlayer()
    {
        if ($this->isBust($this->blackJackCount($this->player))) {
            echo 'Bust! You Lose' . PHP_EOL;
            exit(0);
        }

        if ($this->isBlackJack($this->blackJackCount($this->player))) {
            echo 'Check Dealer Hands' . PHP_EOL;
            $this->showHands(true);
            $this->checkDealer();
        }
    }

    private function checkDealer()
    {
        if ($this->isBlackJack($this->blackJackCount($this->dealer))) {
            echo 'Draw' . PHP_EOL;
            exit(0);
        }

        if ($this->isBust($this->blackJackCount($this->dealer))) {
            echo 'Dealer Bust! You Win' . PHP_EOL;
            exit(0);
        }

        echo match ($this->blackJackCount($this->dealer)) {
            $this->blackJackCount($this->dealer) > $this->blackJackCount($this->player) => 'You Lose' . PHP_EOL,
            $this->blackJackCount($this->dealer) === $this->blackJackCount($this->player) => 'Draw' . PHP_EOL,
            default => 'You Win' . PHP_EOL,
        };
    }

    private function showHands($stand)
    {
        echo 'Dealer Hands' . PHP_EOL;
        if ($stand) {
            foreach ($this->dealer as $row) {
                echo $this->padding($row['suit']->value . $row['rank']->value) . PHP_EOL;
            }
        } else {
            echo $this->padding($this->dealer[0]['suit']->value . $this->dealer[0]['rank']->value) . PHP_EOL;
            echo $this->padding('?') . PHP_EOL;
        }

        echo 'Player Hands' . PHP_EOL;
        foreach ($this->player as $row) {
            echo $this->padding($row['suit']->value . $row['rank']->value) . PHP_EOL;
        }
        echo PHP_EOL;
    }

    private function padding($str): string
    {
        return str_pad($str, 5, ' ', STR_PAD_LEFT);
    }
}

$tramp = new BlackJack();
$tramp->play();

```

#最後に
PHPでENUMを利用できるようになりましたので徐々に使用していきたいです。

明日は、 @tfactory さんによる「vertex ai使ってみた」です。
引き続き、[GMOアドマーケティング Advent Calendar 2021](https://qiita.com/advent-calendar/2021/gmo-am) をお楽しみください！