イノベーション エンジニアブログ


株式会社イノベーションのエンジニアたちの技術系ブログです。ITトレンド・List Finderの開発をベースに、業務外での技術研究などもブログとして発信していってます!


このエントリーをはてなブックマークに追加

リクルートコミュニケーションズのコーディング試験の過去問を題材にOOPの練習してみた

はじめに

どうも、bigenです。
10月に入って、僕もとうとう入社後2年が経過しました。やっと3年目です。

最近めっきりコーディングをする機会が減ってしまったので、練習もかねてリクルートコミュニケーションズさんのコーディング試験の過去問を題材にプログラミングしてみました。
問題はこちら
https://www.rco.recruit.co.jp/career/engineer/entry/

問題文を引用すると、

A,B,Cの3人が1~5の5枚のカードを使ってインディアンポーカーをします。
3人は、ランダムに1枚のカードを引いて額にかざします。相手のカードは見えますが、自分のカードは見えません。

この状態で、A->B->Cの順番に、自分が1番大きい(MAX)、自分は2番目に大きい(MID)自分が1番小さい(MIN)、わからない (?)、を答えます。

1人でも答えがわかった場合、そこで終了となります。「わからない」と答えた場合、回答権が次の人に移ります。 Cもわからない場合、再度Aに回答権が移ります。3人ともウソを言ったり、適当に答えてはいけません。

例1)「A=1 B=4 C=5」だった場合、「A => MIN」で終了します。
例2)「A=1 B=2 C=4」だった場合、「A => ?, B => MID」で終了します。
Bは「Aがわからないなら、自分は5ではない」と考えるからです。

以上を踏まえて、

引数で「A=1 B=4 C=5」で実行すると「A =>MIN」を出力
引数で「A=1 B=2 C=4」で実行すると「A =>?, B =>MID」

を出力するようなコマンドラインのプログラムを作成してください。

なお、人数やカードの枚数がパラメーター化されていて、さまざまなケースがシミュレーションできるコードが望ましいです。

最近社内の勉強会でオブジェクト指向プログラミング(OOP)を取り扱ってるので、オブジェクトをいろいろ活用して書いてみました。

今回は試験合格が目的ではないので、一番手慣れているPHPで書いています。

考え方

結果のソースコードは全てこちらに置いておきました。

https://github.com/bigen1925/exercise/tree/master/poker_exersice

いきなりでアレなんですが、この記事を書いてる途中にプレイヤーの回答ロジックに間違いがあることに気づきました。
多くのケースについては十分対応できるんですが、一部のケースについて最適解にならないことがあります。
今回はオブジェクト指向っぽく全体の構成を作る勉強が目的なので、いったんよしとしておきます。

以下では、僕がコーディングをする際にたどった考え方を書いておきます。
もちろんこれが100%正しい道筋などではありませんし、むしろ改善点を指摘していただける方は m_kasai[@]innovation.co.jpまでご連絡ください。

1. 全体構造の把握

まずゲームの基本的なルールを確認しながら、必要なクラスを考えていきます。

  • ゲームは、カードの枚数と、プレイヤーの人数を決めれば、開始できる。
    → PokerGameのinputはcard_numとplayer_numのみ。

  • ゲームは1ターンにつき1人のプレイヤーがアクションをし、各ターンごとにゲームの終了判定を行う。
    → イテレーターパターン風で実装するのが良さそう。PokerGameオブジェクトがあり、isGameFinished()のようなメソッドと、playTurn()のようなメソッドで進行する。
    → 各ターンごとに、手番となるプレイヤーを判定するかの部分もイテレーター風に書くと便利そうなので、GamePlayersクラスを作り、getThisTurnPlayer()などで取得できるようにしておくと便利そう。

  • 各ターンのプレイヤーのアクションはかなり複雑だが、大きく分けると2つ。
    → ①他プレイヤーのカードによる判断(inputは他のプレイヤー全員のカード)
    →→ 「他のプレイヤーのカードが1,2だから自分はMAXしかありえない」など
    →→ Playersクラスをインプットとして与え、そこに保持されているプレイヤー全員を取得し、順番にカードを見せてもらえば判断できそう
    → ②他プレイヤーの回答による判断(inputは他のプレイヤー全員のカードと、他のプレイヤー全員の回答)
    →→ 「もし自分がカードXだったとしたら、プレイヤーYはカード状況からこう判断するはずだ・・・」という判断をする。
    →→ 「もし自分がXだったら・・・」という仮定は、自分のカードをXと設定した仮のPlayersクラスを作り、それをYに与えて①の判断(カードだけによる判断)をさせるといけそう。

  • クラスの役割としてGameクラスはターンの進行とゲームの終了判定という、少し抽象的な事象を扱うクラス(≠抽象クラス)なので、どんなカードがあと何枚残っているかなど、具体的な情報は扱わないようにしたい。責任の分離。
    → Deckクラスを作り、具体的な残りカードや順番はそっちに切り分ける

  • プレイヤーができるのは、①他のプレイヤーのカードを見る②他のプレイヤーの回答を聞く③自分のアクションをする、あたりができれば大丈夫そう
    → Playerクラスにはこのあたりを実装しておく

ざっとこんな感じでしょうか。後追いで書いてるので、いろいろ抜けてるかもしれませんが。

ソースコード

実際に書いたソースコードを見ていきます。

まずはメイン処理。全体としてはゲームが終わるまでターン進めるだけなので、かなりシンプルに。

main.php
<?php
require_once('PokerGame.php');

// カードの枚数を設定
$card_num = 5;
// プレイヤーの人数を設定
$player_num = 3;

// ゲームを生成
$game = new PokerGame($card_num, $player_num);

// 終了条件を満たすまで、繰り替えし次のターンをプレイする
while (!$game->isGameFinished()) {
    $game->playNextTurn();
}

次に、ゲームの進行状況を管理するPokerGameクラス。

  • コンストラクタでDeckとGamePlayersのクラスを生成して、プロパティに保持しています。

  • このクラスはあまり具体的な情報に関与したくないので、プレイヤーにカードを引いてもらうときもデッキを渡すだけで、どんなカードを引いたかは知りません。

  • また、「次のプレイヤーをどうやって選ぶか(=プレイヤーの順序)」についても関与せず、GamePlayersクラスにまかせてしまっています。イテレーターっぽい作りにしました。

  • また当然ですが、GamePlayersクラスはあくまで「どんなプレイヤーがどんな順序で参加しているか」を扱うクラスなので、実際のアクション(答えを出す)をさせません。
    GamePlayersから次のプレイヤーを取得し、Playerクラスにアクションをさせます。

PokerGame.php
<?php
require_once('Deck.php');
require_once('GamePlayers.php');

/**
 * ゲーム全体の進行状況を状態として持つクラス
 * カードの枚数、プレイヤーの合計人数、ゲームのターン数、終了状況などを状態として持つが
 * 「どのプレイヤーがどのカードを持っているか」「どんなカードが山に残っているか」など具体的な情報は関与しない
 */
class PokerGame
{
    private $card_num = null;
    private $player_num = null;
    private $deck = null;
    private $game_players = null;
    private $turn_num = 0;
    private $is_solved = false;

    /**
     * @param int $card_num カードの枚数
     * @param int $player_num プレイヤーの人数
     */
    public function __construct(int $card_num, int $player_num)
    {
        $this->card_num = $card_num;
        $this->player_num = $player_num;

        // カードの山を生成
        $this->deck = new Deck($card_num);

        // プレイヤーを人数分ゲームに追加
        $this->game_players = new GamePlayers();
        for ($i = 0; $i < $player_num; $i++) {
            $this->game_players->addPlayer($this->deck);
        }
        echo "PokerGame: プレイヤーの追加がすべて完了しました\n";
        var_dump($this->game_players);

    }

    /**
     * ゲームが終了しているかどうかを判定する
     * @return bool
     */
    public function isGameFinished()
    {
        // 既に答えが出たか、3周が終了した時点(無限ループ防止)でゲームは終わり
        if ($this->is_solved || $this->turn_num >= $this->game_players->getNumberOfPlayers() * 3) {
            echo "PokerGame: ゲームが終了したと判定しました。(turn: $this->turn_num)\n";
            return true;
        }
        echo "PokerGame: ゲームは終了していないと判定しました。(turn: $this->turn_num)\n";
        return false;
    }

    /**
     * 次のターンを進行させる
     */
    public function playNextTurn()
    {
        // ターン数を進める
        $this->turn_num++;
        $next_turn = $this->turn_num;
        echo "次のターンを開始します。next_turn: $next_turn\n";

        // 次のターンのプレイヤーを取得
        $player = $this->game_players->getThisTurnPlayer($this->turn_num);

        // プレイヤーが考えて答えを出す
        $answer = $player->playMyTurn($this->game_players, $this->card_num);

        // 答えが出た場合は、解決フラグにその情報を保持しておく
        if ($answer !== 'no idea') {
            $this->is_solved = true;
        }
    }
}

次に、「どんなプレイヤーが参加しているか」「プレイヤーの順序」を扱うGamePlayersクラスです。

  • プレイヤーが参加するとき(addPlayer())にデッキからカードを引いていますが、どんなカードを引いたかはこのクラスでは関知していません。

  • ある特定のプレイヤーからの「他のプレイヤーはどんな感じか教えて」というリクエストに対しても、このクラスではプレイヤークラスを返すだけにしています。どんなカードを持っているのか、どんな回答状況か、などはあえて扱わないようにしています。責任の分離。

<?php
require_once('Player.php');
/**
 * プレイヤーが何人参加しており、どのような順番でプレイをするかの順番を状態に持つクラス
 *
 */
class GamePlayers
{
    private $players = [];

    public function addPlayer(Deck $deck) {
        $player_id = ($this->getNumberOfPlayers() + 1);
        $player = new Player($player_id);
        $player->drawCard($deck);

        $this->players[$player_id] = $player;
    }

    public function setPlayer(int $player_id, Player $player) {
        $this->players[$player_id] = $player;
    }

    /**
     * ターン数を指定すると、誰がプレイする番かを返す
     * 例えばプレイヤーが3人の場合、
     * ターン1 -> プレイヤー1
     * ターン1 -> プレイヤー2
     * ターン1 -> プレイヤー3
     * ターン4 -> プレイヤー1
     * ターン5 -> プレイヤー2
     * ・・・
     * となるように決まる
     */
    public function getThisTurnPlayer(int $turn_num)
    {
        $id = (($turn_num - 1) % $this->getNumberOfPlayers()) + 1;
        $next_player = $this->players[$id];
        return $next_player;
    }

    public function getNumberOfPlayers()
    {
        return count($this->players);
    }

    /**
     * 指定されたidのプレイヤー以外のプレイヤーを配列で返す
     */
    public function getOtherPlayers(int $player_id)
    {
        $other_players = [];
        foreach ($this->players as $id => $player) {
            if ($id !== $player_id) {
                $other_players[$id] = $player;
            }
        }

        return $other_players;
    }
}

次に、Playerクラス。どんなカードを持っているか、どんな回答状況か、などを状態に持っており、アクション(回答)を行うこともできます。

  • 名前は少しアレンジしてますが、基本的にいわゆるセッターとゲッター系のメソッドばかりです。

  • 案の定、アクション(playMyTurn())が重くなりました。一応まじめに実装してますが、オブジェクト指向の練習という意味ではあまり重要ではないです。

 <?php
require_once('Deck.php');
require_once('GamePlayers.php');

/**
 * プレイヤーを表すクラス
 * 自分が所持しているカードについての情報の保持や操作などは行うが、
 * 「他にどんなプレイヤがいるか」や「他のプレイやがどのカードを持っているか」などは直接保持しない
 * 他のプレイヤークラスや、ゲームプレイヤークラスから情報を取得する。
 */
class Player
{
    private $id = null;
    private $hand = null;
    private $others_hands = null;
    private $possible_hands = null;
    private $answer = 'yet';

    public function __construct(int $player_id)
    {
        $this->id = $player_id;
    }

    /**
     * デッキからカードを引く
     */
    public function drawCard(Deck $deck)
    {
        $this->hand = $deck->drawCard();
    }

    /**
     * プレイヤーのIDを取得する
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * 自分の持ってるカードを見せる。
     * 自分で自分のカードを見れないような仕組みは今のところ実装していない。
     * @return int
     */
    public function showHand()
    {
        return $this->hand;
    }

    /**
     * 前回の自分のターンで考えた答えを返す
     * 今回のゲームの設定では、返しうるのは実質'yet(まだターンが回ってきていない)'と'no idea(分からない)'だけ
     * @return string
     */
    public function tellAnswer()
    {
        return $this->answer;
    }

    /**
     * 自分のターンをプレイする
     * @param GamePlayers $game_players ゲームに参加しているプレイヤーの情報
     * @param int $card_num カードの合計枚数
     */
    public function playMyTurn(GamePlayers $game_players, int $card_num)
    {
        // ログ出力用にIDを取得しておく
        $my_id = $this->getId();

        // 自分が手札として可能性のあるカードを取得する
        if (!$this->possible_hands) {
            $this->possible_hands = $this->getPossibleHands($game_players, $card_num);
        }

        // 他のプレイヤーの回答状況を考慮して、自分の手札として考えられる選択肢を削る
        $other_players = $game_players->getOtherPlayers($this->id);
        foreach ($other_players as $other_player) {
            // 既にno ideaと答えているプレイヤーについてのみ考慮する。yetのプレイヤーはスルーする。
            if ($other_player->tellAnswer() == 'no idea') {
                echo "Player${my_id}:: 少し考えます\n";

                // 自分の取りうる手札それぞれについて、「もし自分のカードがXだったら相手からどう見えるか」を考える
                foreach ($this->possible_hands as $guess_hand => $guess_answer) {

                    // カードXを持つ仮のプレイヤーを生成
                    $guess_myself = clone $this;
                    $guess_myself->hand = $guess_hand;

                    // 自分がカードXをもっていると想定した仮のゲームプレイヤークラスを生成
                    $guess_game_players = clone $game_players;
                    $guess_game_players->setPlayer($this->id, $guess_myself);

                    // 仮のゲームプレイヤークラスを与えた時に、相手は自身がどのような選択肢があるように見えるか検証
                    $possible_hands = $other_player->getPossibleHands($guess_game_players, $card_num);

                    $others_id = $other_player->getId();
                    echo "Player${my_id}:: 私が${guess_hand}だとすると、プレイヤー${id}さんは自分が持っているカードの選択肢はこう見えていたはず";
                    var_dump($possible_hands);

                    // もし仮のゲームプレイヤークラスを想定したときに、相手に答えが出ているようであれば、
                    // no_ideaと回答していることに矛盾するので、カードXは選択肢から除外する
                    // 答えが出ているかどうかは、回答の選択肢が1種類(どのカードを持っていたとしても全てMAX、など)であることで判定する
                    if (count(array_unique($possible_hands)) === 1) {
                        echo "Player${my_id}:: カード${guess_hand}は選択肢から除外する。\n";
                        unset($this->possible_hands[$guess_hand]);
                    }
                }
            }
        }

        echo "Player${my_id}:: 私のターン。possible_handsは以下\n";
        var_dump($this->possible_hands);

        // 自分の手札として考えられる選択肢全てで回答が同じ(どのカードだったとしてもMAX、など)となった時点で回答が決まる
        if (count(array_unique($this->possible_hands)) === 1) {
            $this->answer = current($this->possible_hands);
            echo "Player${my_id}:: 答えが出たよ!答えは $this->answer だ!\n";
        } else {
            $this->answer = 'no idea';
            echo "Player${my_id}:: 答えが出なかったよ・・・答えは $this->answer だ\n";
        }
        echo "Player${my_id}:: ターン終了\n";
        return $this->answer;
    }

    /**
     * 他のプレイヤーの持っているカードから、自分の手札として可能性のあるカード一覧を取得する
     * また、手札になりえるカードそれぞれに対して、手札がそのカードだった場合回答は何になるかも取得する
     * @param Gameplayers $game_players
     * @param int $card_num
     * @return Array 例)手札になりえるカードが1,3,5で、他のプレイヤーのカードを考慮してMIN,MID,MAXになるとき、
     *               $possible_hands = [1 => 'MIN', 3 => 'MID', 5 => 'MAX']
     */
    public function getPossibleHands(GamePlayers $game_players, int $card_num) {
        $possible_hands = [];

        // 他プレイヤーが所持しているカード一覧を取得する
        $others_hands = [];
        $other_players = $game_players->getOtherPlayers($this->id);
        foreach ($other_players as $other_player) {
            $others_hands[] = $other_player->showHand();
        }
        $this->others_hands = $others_hands;

        // 場に存在しうるカード全体との差分をとり、自分の手札として可能性のあるカード一覧を取得する
        $all_cards = range(1, $card_num);
        $diff_cards = array_diff($all_cards, $others_hands);

        // 自分の手札として可能性のあるカードそれぞれに対して、MINかMIDかMAXかを判定する
        foreach ($diff_cards as $card) {
            $all_hands = $others_hands;
            array_push($all_hands, $card);

            if ($card === min($all_hands)) {
                $guess_answer = 'MIN';
            } elseif ($card === max($all_hands)) {
                $guess_answer = 'MAX';
            } else {
                $guess_answer = 'MID';
            }
            $possible_hands[$card] = $guess_answer;
        }

        return $possible_hands;
    }
}

最後に、Deckクラス。

  • コンストラクタで枚数分のカードを生成し、シャッフルしておきます。

  • ドローすると、先頭の1枚を取得して返します。山は取得したカードが削除され、1枚減ります。

<?php
/**
 * カードの山のクラス
 */
class Deck
{
    public $cards = [];

    /**
     * @param int card_num 山にセットするカードの枚数
     */
    public function __construct(int $card_num)
    {
        echo "Deck: cunstruct input (card_num: $card_num)\n";
        // カードを生成
        $this->cards = range(1, $card_num);
        // シャッフルしておく
        shuffle($this->cards);

        echo "Deck: デッキを新たに生成しました。\n";
        var_dump($this->cards);
    }

    /**
     * 山の先頭の1枚を取り出して返す
     * 山にカードがない場合はfalseを返す
     */
    public function drawCard()
    {
        if ($this->cards) {
            return array_shift($this->cards);
        }
        return false;
    }
}

おわりに

本当はCardクラスとかAnswerクラスとかPossibleHandsクラスとかまで作ろうかと思ったんですが、今回はあまり重要じゃなさそうなので省略しちゃいました。
このコードを書くのに、5時間ぐらいかかったので、リクルートコミュニケーションには受かれなさそうですね・・・

でも問題の構造を概念的に分割していって、そのままオブジェクトで実装していくというフローの練習にはなったので、結構良かったです。

補足

プレイヤーのアクションのロジックですが、「相手の気持ちになって考える」パートで「自分以外のプレイヤーは、他の人のカード状況しか考慮せずに回答する。回答状況は考慮しない。」という前提で書かれてしまっているのがミスです。
正しい実装方法はいくつか候補が浮かびますが、またおいおいということで。。。