スリーカードポーカー（簡易版）の勝敗判定

# 概要
&emsp;「独学エンジニア」というサービスでPHPの勉強をしており、そこの課題でまずはツーカードポーカーの勝敗判定をするコード、次にスリーカードポーカーの勝敗を判定するコードを自力で書いてみよう、というものがあったので、自己流でスリーカードポーカーを書いてみました。
※講座の趣旨から、「独学エンジニア」をやられている方は、こちらの記事は見ずに、まずは御自身でやってみてください。

# ポーカーのルール
&emsp;ちゃんとしたスリーカードポーカーのルールではなく、コードを練習するためのやや簡略化したバージョンです。  

- カードは「2,3,4,5,6,7,8,9,10,J,Q,K,A」の13種類。強さは弱い順に左から。 
- 役は強い順に、スリーカード、ストレート、ペア、ハイカードの4種類。
- スリーカード：3つのカードの数が全て同じだった場合
- ストレート：「3,4,5」,「10,J,Q」のように、強さが隣り合っている3つのカードが並んでいる場合。「2,3,A」の場合はAを最弱としてストレートとして扱う。「2,K,A」や2枚のカードの強さのみ隣り合っている場合はストレートとみなさない。
- ペア：3つのうち2つのみ数が同じだった場合
- ハイカード：上記どの役にも当てはまらなかった場合  

&emsp;2人のプレイヤーの役が同じだった場合は、手札の中で最も大きい数同士を比較し、それも同じだった場合は2番目に大きい数同士、最小の数同士と大小を比較します。全て同じだった場合は引き分けです。

# 要件定義
- ユーザー定義関数showDownを作成し、showDown(C3, D5, SQ, H4, S2, CA)といったように、引数としてプレイヤー1，2の各カードを指定することで、[プレイヤー1の役,プレイヤー2の役,勝敗]といった形で結果が返ってくるようにします。(C,D,S,Hは、それぞれクローバー、ダイヤ、スペード、ハートの略です。)
- 勝敗の結果は、1:1の勝ち、2:2の勝ち、0:引き分けといったように、数値で表します。
- Aが最強になったり最弱になったりする仕様は、リングバッファー(最初の値と最後の値が連結して、環状になるように格納するアルゴリズムのこと)を使おうと思います。
- 保守性やコードの読みやすさを考えて、極力同じような処理を何度も書くことは避け、各処理についてなるべくスマートな式で実現できるように努めます。

# 作成したコード
&emsp;上記のことを意識して、拙いながらどうにかこちらのコードが完成しました。
https://harigami.net/cd?hsh=6643068d-4691-4635-a48a-702b57866158

```php:threeCardPorker.php
<?php

const CARDS = [
    '2' => 2
    , '3' => 3
    , '4' => 4
    , '5' => 5
    , '6' => 6
    , '7' => 7
    , '8' => 8
    , '9' => 9
    , '10' => 10
    , 'J' => 11
    , 'Q' => 12
    , 'K' => 13
    , 'A' => 14
];

const HIGH_CARD = 'high card';
const PAIR = 'pair';
const STRAIGHT = 'straight';
const THREE_CARD = 'three card';

const HAND_RANKS = [
    'high card' => 0
    ,'pair' => 1
    ,'straight' => 2
    ,'three card' => 3
];


function showDown (string $card11, string $card12, string $card13, string $card21, string $card22, string $card23): array
{
    $cards = [$card11, $card12, $card13, $card21, $card22, $card23];
    $cardRanks = [];
    // 6枚のカードの文字列から数字部分のみを抽出し、新しい配列$cardNumbersに格納
    // その後、CARDS定数を使って数字ごとのランクに変換
    foreach ($cards as $card) {
        array_push($cardRanks, CARDS[substr($card, 1)]);
    }
    // 3つずつ各プレイヤーの手札として振り分ける
    $playerCardRanks = array_chunk($cardRanks, 3);
    // プレイヤーごとの役をユーザー定義関数checkHandsで決定。
    // $playerCardRanksを2プレイヤー分に分けてからでないと、checkHands関数で2次元配列を扱うことになってしまう上に、配列の引数２つというのも関数を定義しづらい
    $player1Hand = checkHands($playerCardRanks[0]);
    $player2Hand = checkHands($playerCardRanks[1]);
    // 勝敗を決定
    $winner = decideWinner($player1Hand, $player2Hand);
    return [$player1Hand['hand'], $player2Hand['hand'], $winner];
}

function checkHands(array $playerCardRanks): array
{
    $hand = HIGH_CARD;
    if (isThreeCard($playerCardRanks)) {
        $hand = THREE_CARD;
    } elseif (isPair($playerCardRanks)) {
        $hand = PAIR;
    } elseif (isStraight($playerCardRanks)) {
        $hand = STRAIGHT;
    }
    rsort($playerCardRanks);

    // 手札がa,2,3の場合はaを最弱にする
    // aのランク14をカードの種類の数13で割った余りは1なので、それをaのランクとする
    if ($playerCardRanks[1] === $playerCardRanks[2] + 1 && $playerCardRanks[2] === $playerCardRanks[0] % count(CARDS) + 1) {
        $playerCardRanks[0] = $playerCardRanks[0] % count(CARDS);
    }

    return [
        'hand' => $hand
        ,'handRank' => HAND_RANKS[$hand]
        ,'biggestNum' => $playerCardRanks[0]
        ,'middleNum' => $playerCardRanks[1]
        ,'smallestNum' => $playerCardRanks[2]
    ];
}

function isThreeCard(array $playerCardRanks): bool
{
    // 一つ目の値の数がカード全体の枚数に等しいとき（すなわち、全てのカードの値が同じとき）、trueを返す（ボツ案）
    // if (array_count_values($playerCardRanks)[0] === count($playerCardRanks)) {
    //     return true;

    // それぞれのプレイヤーの手札のうち、重複する種類が1種類の（すべて重複する）ときにtrueを返す
    if (count(array_unique($playerCardRanks)) === 1) {
        return true;
    } else {
        return false;
    }

    // ここは無難に、それぞれのカードの値が等しいときでも可
    // if ($playerCardRanks[0] === $playerCardRanks[1] && $playerCardRanks[1] === $playerCardRanks[2]) {
    //     return true;
    // }
}

function isPair(array $playerCardRanks): bool
{
    // if (count(array_count_values($playerCardRanks)) === 2) {
    //     return true; (ボツ案)

    // 重複する値の種類が2の時
    if (count(array_unique($playerCardRanks)) === 2) {
        return true;
    } else {
        return false;
    }
}

function isStraight(array $playerCardRanks): bool
{
    rsort($playerCardRanks);
    $biggestCard = $playerCardRanks[0];
    $middleCard = $playerCardRanks[1];
    $smallestCard = $playerCardRanks[2];
    // echo('biggestCard:' . $biggestCard);

    // 1番大きい数が2番目の数より1大きく、2番目の数が3番目の数より大きい場合
    if ($biggestCard === $middleCard + 1 && $middleCard === $smallestCard + 1) {
        return true;
    // 1番大きい数がA、2番目の数が3、1番小さい数が2の場合を想定している。
    // AのCardRank14をカードの種類の数13で割った余りは1なので、AのCardRankをそれにすり替える。（リングバッファーのアルゴリズムから着想。）
    } elseif ($middleCard === $smallestCard + 1 && $smallestCard === $biggestCard % count(CARDS) + 1) {
        return true;
    } else {
        return false;
    }
}

function decideWinner (array $player1Hand, array $player2Hand): int
{

    // 勝敗を決める要素をまとめる。それぞれの役、1番大きい数、2番目の数、1番小さい数
    $judgementElements = ['handRank', 'biggestNum', 'middleNum', 'smallestNum'];

    // それぞれの要素ごとに大小を比較していって、大小が決まった時点で結果を返す
    // いずれの要素でも決着がつかなかった場合に、0（引き分け）を返す
    foreach ($judgementElements as $judgementElement) {
        if($player1Hand[$judgementElement] > $player2Hand[$judgementElement]) {
            return 1;
        } elseif ($player2Hand[$judgementElement] > $player1Hand[$judgementElement]) {
            return 2;
        }
    }
    return 0;
}

var_dump(showDown('SK', 'D9', 'CJ', 'S10', 'H10', 'D6'));
var_dump(showDown('DK', 'S2', 'HA', 'C4', 'H5', 'S6'));
var_dump(showDown('S9', 'CJ', 'DK', 'C3', 'C3', 'H3'));
var_dump(showDown('H3', 'D4', 'C5', 'HK', 'S10', 'DK'));
var_dump(showDown('S3', 'H3', 'C3', 'D10', 'SK', 'D10'));
var_dump(showDown('C3', 'S3', 'H3', 'CK', 'SJ', 'SQ'));
var_dump(showDown('DJ', 'HK', 'S9', 'CQ', 'D9', 'S10'));
var_dump(showDown('C7', 'S5', 'H3', 'S5', 'S7', 'D3'));
var_dump(showDown('CA', 'DA', 'CK', 'D2', 'D2', 'C3'));
var_dump(showDown('HK', 'DK', 'SA', 'CA', 'SA', 'SK'));
var_dump(showDown('D4', 'D7', 'K7', 'D6', 'K4', 'C6'));
var_dump(showDown('SK', 'D9', 'CJ', 'S10', 'H10', 'D6'));
var_dump(showDown('S4', 'C4', 'S7', 'S4', 'S4', 'C7'));
var_dump(showDown('SA', 'DQ', 'DK', 'CA', 'C2', 'D3'));
var_dump(showDown('SA', 'DK', 'DQ', 'CK', 'CQ', 'DJ'));
var_dump(showDown('H2', 'D3', 'D4', 'HA', 'C2', 'S3'));
var_dump(showDown('S2', 'S3', 'S4', 'C2', 'C3', 'D4'));
var_dump(showDown('S2', 'S2', 'S2', 'DA', 'HA', 'CA'));
var_dump(showDown('CK', 'CK', 'SK', 'SA', 'HA', 'DA'));
var_dump(showDown('D2', 'C2', 'S2', 'C3', 'H3', 'S3'));

```

例えばこちらのような結果が現れます。

```text:result.txt
array(3) {
  [0]=>
  string(9) "high card"
  [1]=>
  string(4) "pair"
  [2]=>
  int(2)
}
```

#解説
&emsp;まずは、それぞれのカードの数字をランクに変換する連想配列を定数として定義します。これは、KやAといった文字列を、大小が比較できる数字に変換するために必要です。
&emsp;また、あとで使う、それぞれの役の定義や役のランクについても定数として定義します。
&emsp;次からは関数の定義です。一番最初に、判定する際に使うshowDown関数を作成します。この関数は、他の関数を呼び出しながら、引数として与えられたカードの種類を使って、数字のランクや各プレイヤーの役をまとめます。最後に、それらの要素を使って勝敗の判定を行う関数を呼び出し、配列の形式で戻します。
&emsp;元々与えられた引数からスペードやクローバーなどの文字を切り離したり、プレイヤーごとの配列にまとめたりするのに、substr()やarray_chunk()といった様々な関数を使っています。
&emsp;ここで注意した点としては、例えばcheckHands関数で役などの判定をする際に、プレイヤー1の配列とプレイヤー2の配列をまとめて引数として渡したいところですが、そうするとcheckHands関数の中で2次元配列を扱ったりといった、ややこしい事態になりかねないので、ここはそれぞれのプレイヤーごとで引数を渡すようにしています。
&emsp;以降はshowDown関数で呼び出している各関数を定義します。checkHands関数では、さらに役の判定をする関数を呼び出し、役の判定をした後、後の勝敗の判定に使うための要素を連想配列としてまとめ、戻り値として返しています。ここで連想配列として定義しておくことで、勝敗を判定する関数に引数を渡す際の処理が簡潔になります。また、最も大きい数、最も小さい数などを連想配列としてまとめる前に、A,2,3だった場合にAを最弱にする処理も行ってしまいます。
&emsp;showDownの中で呼び出されているisThreeCard,isPairなどの役を判定する関数は、array_unique(),リングバッファーのアルゴリズムを使うなどして、なるべく簡潔に処理をまとめられるようにしています。ここは、「カードが...で...の場合」といったように、ゴリ押しの書き方でも成立する部分ですが、保守性やコードの読みやすさ、といった側面にもつながってくると思うので、こだわって極力自己流で、コードが短くて済むように工夫しました。
&emsp;最後にdecideWinner関数です。ここが今回のポーカーのコードのキモであり、調整に一番苦労した部分でもあります。先ほど連想配列としてまとめたプレイヤーごとの「役のランク、一番大きい数、二番目に大きい数、一番小さい数」の要素をそれぞれforeachで比較していき、大小が決まった時点で数が大きいプレイヤーの数を戻り値として返します。foreachのループの中で、どの比較対象でも大小が決まらなかった場合（両者のカードが全て同じ場合）、引き分けとして0を返します。
&emsp;後は判定が上手くいっているかの検証のためにshowDown関数を引数を指定して実行します。戻り値が配列の形式なので、実行結果を表示するには、echoではなくvar_dumpを使う必要がありました。

# 振り返り
- 連想配列の使い方、関数の定義の仕方や順序についてよく考えるきっかけとなり、よい練習になりました。
- 取り入れてみようと思ったアルゴリズムや方法を実装する練習にもなりました。
- bool型の場合「a === hoge」など条件の式をそのまま書くだけで、条件に合致する場合trueが返されますが、今回はエラー処理の都合上、少しくどい書き方になってしまいました。
- 本物のポーカーのルールではなく、あくまでコードを作りやすいように簡略化したルールではあるので、次は「'55A'の場合5の役がペアとなりAよりも強くなる」などといった、本格的なルールでの判定も行えるように実装しようと思います。
- 一番大きいカードを変数として定義する処理など、別の関数で複数回同じようなことを書いてしまっているコードがあるので、これを解消して一つにまとめる方法がないか設計を見直そうと思います。

# 参考
独学エンジニア　※有料会員のみ見れる部分です。
https://dokugaku-engineer.com/

※その他、コードの改善にあたって複数のエンジニアさんに意見を求めました。答えてくださった方ありがとうございました。