「第12回 オフラインリアルタイムどう書く」の課題をPHPで書いてみた


## 0.きっかけ
qiitaを見ていてたまたま見かけて興味を持ったので。
なんで第12回かというと、特に意味はありません。たまたまです。
[第12回 オフラインリアルタイムどう書く](http://atnd.org/events/40389)

実行環境にFuelPHPを使用していますが、
アルゴリズムの部分はFuelPHPと切り離して書いたので、namespaceの一文削れば他でも使えるはずです。
FuelPHPはあくまで実行環境として利用した程度です。
まあついで程度で。

## 1. サイコロを表すDiceクラスの実装

課題の部分です。

自分の場合、下記のように書きました。
まだ回答例等見ていないのでさぶいコードでしたらすみません・・・

工夫した点としては、デザインパターンのStateパターンぽい感じに、
「現在の状態」から「次の状態」に遷移させることでサイコロの状態を表現するようにした点です。

苦労した点は、その「状態」をどういった形で表現すべきか？という点です。
で、やってみたのは、サイコロの初期状態に合わせて、「1は上」「2は北」「3は西」というルールを定義する、
という表現方法です。


もっといいやり方もあるかもしれませんが、気づいたら手が止まらなくなっていたのでこんなもんでw

```php:dice.php
<?php

namespace Fuel\App\Object;

/**
 * サイコロを表すクラス
 */
class Dice{

    /** サイコロの方向 x 数値のマッピング */
    const TOP    = 1;
    const NORTH  = 2;
    const WEST   = 3;
    const EAST   = 4;
    const SOUTH  = 5;
    const BOTTOM = 6;

    /** 略称とのマッピング */
    private $DIRECTIONS = array(
        'N' => self::NORTH,
        'W' => self::WEST,
        'E' => self::EAST,
        'S' => self::SOUTH,
    );

    /** サイコロの状態を管理する配列 */
    private $_state = array();

    /**
     * コンストラクタ
     */
    public function __construct($top, $north, $west){

        // サイコロの初期状態を設定する
        $this->setState($top, $north, $west);
    }

    /**
     * サイコロの状態を設定する
     */
    public function setState($top, $north, $west){

        $this->_state = array(
            self::TOP    => $top,
            self::NORTH  => $north,
            self::WEST   => $west,
            self::EAST   => 7 - intval($west),
            self::SOUTH  => 7 - intval($north),
            self::BOTTOM => 7 - intval($top),
        );
    }

    /**
     * サイコロを転がす
     */
    public function shake($directions){

        // 初期配置時の上面を取得
        $result = array($this->getTop());

        for($i = 0, $len = mb_strlen($directions); $i < $len; $i++){

            // 方向を1つ取る
            $direction = mb_substr($directions, $i, 1);

            // サイコロを1マス転がす
            $next = $this->getNext($direction);

            // サイコロの状態を更新
            $this->setState(
                $next['top'],
                $next['north'],
                $next['west']
            );

            // 現在の上面を取得
            $result[] = $this->getTop();
        }

        $result = implode('', $result);

        return $result;
    }

    /**
     * サイコロが転がったときの状態を取得する
     *
     * TODO:縦と横方向に分解すればもっとリファクタできるかも
     */
    public function getNext($direction){

        $dirNum = $this->DIRECTIONS[$direction];

        // 上面を取得(上面にくるのは、転がった方向の裏側の数値)
        $top = $this->_state[7 - $dirNum];

        switch($direction){

            // 東, 西の場合、北面は変わらない
            case 'E':
                $north = $this->_state[self::NORTH];
                $west  = $this->_state[self::BOTTOM];
                break;

            case 'W':
                $north = $this->_state[self::NORTH];
                $west  = $this->_state[self::TOP];
                break;

            // 北, 南面の場合、西面は変わらない
            case 'S':
                $north = $this->_state[self::BOTTOM];
                $west  = $this->_state[self::WEST];
                break;

            case 'N':
                $north = $this->_state[self::TOP];
                $west  = $this->_state[self::WEST];
                break;
        }

        return array(
            'top'   => $top,
            'north' => $north,
            'west'  => $west,
        );
    }

    /**
     * 上面の数値を取得する
     */
    public function getTop(){

        return $this->_state[self::TOP];
    }

}
```

## 2.bootstrapでDiceクラスを定義

上記で作成したDiceクラスをFuelPHP内で実行できるように、bootstrap.phpに登録します。
下記の文を追記しました。

```
Autoloader::add_namespace('Fuel\\App', APPPATH.'classes/');

Autoloader::add_classes(array(
	// Add classes you want to override here
	// Example: 'View' => APPPATH.'classes/view.php',
    'Fuel\\App\\Object\\Dice' => APPPATH.'classes/object/dice.php',
));
```

こんな感じになりました。

```php:bootstrap.php
<?php

// Load in the Autoloader
require COREPATH.'classes'.DIRECTORY_SEPARATOR.'autoloader.php';
class_alias('Fuel\\Core\\Autoloader', 'Autoloader');

// Bootstrap the framework DO NOT edit this
require COREPATH.'bootstrap.php';

Autoloader::add_namespace('Fuel\\App', APPPATH.'classes/');

Autoloader::add_classes(array(
	// Add classes you want to override here
	// Example: 'View' => APPPATH.'classes/view.php',
    'Fuel\\App\\Object\\Dice' => APPPATH.'classes/object/dice.php',
));

// Register the autoloader
Autoloader::register();

/**
 * Your environment.  Can be set to any of the following:
 *
 * Fuel::DEVELOPMENT
 * Fuel::TEST
 * Fuel::STAGING
 * Fuel::PRODUCTION
 */
Fuel::$env = (isset($_SERVER['FUEL_ENV']) ? $_SERVER['FUEL_ENV'] : Fuel::DEVELOPMENT);

// Initialize the framework with the config file.
Fuel::init('config.php');
```


## 3.Taskクラスの作成

TaskクラスはFuelPHPのコマンドライン用のクラスです。(バッチとかで使うやつ)
デフォルトは「run」に書かれた処理が実行されるので、runからDiceクラスのメソッドを実行するだけです。

あとは問題の下部に記載されていたテストケースを肉付け。
(本当はテストクラスを別に作成すべきですがここでは省略)

```php:shakedice.php
<?php

namespace Fuel\Tasks;

use Fuel\App\Object\Dice;;

/**
 * サイコロを転がす実行クラス
 */
class ShakeDice {

    /** テストケースの番号 */
    private static $_caseNum = 0;

    /**
     * サイコロを振る
     */
    public static function run($directions){

        // サイコロの初期状態を設定し、インスタンスを生成
        $dice = new Dice('1', '2', '3');

        return $dice->shake($directions);
    }

    /**
     * テストケースを実行
     */
    public static function doTest(){

        /*0*/ static::test( "NNESWWS", "15635624" );
        /*1*/ static::test( "EEEE", "13641" );
        /*2*/ static::test( "WWWW", "14631" );
        /*3*/ static::test( "SSSS", "12651" );
        /*4*/ static::test( "NNNN", "15621" );
        /*5*/ static::test( "EENN", "13651" );
        /*6*/ static::test( "WWNN", "14651" );
        /*7*/ static::test( "SSNN", "12621" );
        /*8*/ static::test( "NENNN", "153641" );
        /*9*/ static::test( "NWNNN", "154631" );
        /*10*/ static::test( "SWWWSNEEEN", "12453635421" );
        /*11*/ static::test( "SENWSWSNSWE", "123123656545" );
        /*12*/ static::test( "SSSWNNNE", "126546315" );
        /*13*/ static::test( "SWNWSSSWWE", "12415423646" );
        /*14*/ static::test( "ENNWWS", "1354135" );
        /*15*/ static::test( "ESWNNW", "1321365" );
        /*16*/ static::test( "NWSSE", "154135" );
        /*17*/ static::test( "SWNWEWSEEN", "12415154135" );
        /*18*/ static::test( "EWNWEEEEWN", "13154532426" );
        /*19*/ static::test( "WNEWEWWWSNW", "145151562421" );
        /*20*/ static::test( "NNEE", "15631" );
        /*21*/ static::test( "EEEEWNWSW", "1364145642" );
        /*22*/ static::test( "SENNWWES", "123142321" );
        /*23*/ static::test( "SWWWSNSNESWW", "1245363635631" );
        /*24*/ static::test( "WESSENSE", "141263231" );
        /*25*/ static::test( "SWNSSESESSS", "124146231562" );
        /*26*/ static::test( "ENS", "1353" );
        /*27*/ static::test( "WNN", "1453" );
        /*28*/ static::test( "SSEENEEEN", "1263124536" );
        /*29*/ static::test( "NWSNNNW", "15414632" );
        /*30*/ static::test( "ESSSSSWW", "132453215" );
        /*31*/ static::test( "ESE", "1326" );
        /*32*/ static::test( "SNWNWWNSSSS", "121456232453" );
        /*33*/ static::test( "SWEESEN", "12423653" );
        /*34*/ static::test( "NEEWNSSWWW", "15323631562" );
        /*35*/ static::test( "WSEW", "14212" );
        /*36*/ static::test( "SWSNNNSNWE", "12464131353" );
        /*37*/ static::test( "ENWEWSEEW", "1351513545" );
        /*38*/ static::test( "WSEWN", "142124" );
        /*39*/ static::test( "EWNEESEWE", "1315321414" );
        /*40*/ static::test( "NESEEN", "1531263" );
        /*41*/ static::test( "WSW", "1426" );
        /*42*/ static::test( "ENEWE", "135656" );
    }

    /**
     * テストを実行し、結果を出力する
     */
    public static function test($case, $answer){

        $result = static::run($case);

        if($result !== $answer){

            print_r('[' . static::$_caseNum . '][NG] result:' . $result . "\n");
            exit();
        }

        print_r('[' . static::$_caseNum . '][OK] result:' . $result . "\n");

        static::$_caseNum++;
    }

}
```

## 4.実行

taskクラスは「oil」を使って実行します。
なので、oilがあるディレクトリまで移動。

```
s ls -l
合計 20
drwxrwxr-x 6 mtaji mtaji 4096  3月 24 12:39 2013 fuel
drwxr-xr-x 4 root  root  4096  3月 24 10:20 2013 myproject
-rw-rw-r-- 1 mtaji mtaji 1451  3月 24 12:37 2013 oil
-rw-r--r-- 1 root  root    24  3月 24 07:37 2013 phpinfo.php
lrwxrwxrwx 1 root  root    20  3月 24 06:50 2013 phpmyadmin -> /var/lib/phpmyadmin/
drwxrwxr-x 3 mtaji mtaji 4096  3月 24 12:37 2013 public
```

まずはrunクラスから1つ実行。
下記のように実行します。

```
s sudo php oil refine shakedice NNESWWS
15635624
```

大丈夫そう。

続いて、全てのテストケースを実行。
こちらはdoTestメソッドを実行します。

```
s sudo php sudo php oil refine shakedice:doTest
[0][OK] result:15635624
[1][OK] result:13641
[2][OK] result:14631
[3][OK] result:12651
[4][OK] result:15621
[5][OK] result:13651
[6][OK] result:14651
[7][OK] result:12621
[8][OK] result:153641
[9][OK] result:154631
[10][OK] result:12453635421
[11][OK] result:123123656545
[12][OK] result:126546315
[13][OK] result:12415423646
[14][OK] result:1354135
[15][OK] result:1321365
[16][OK] result:154135
[17][OK] result:12415154135
[18][OK] result:13154532426
[19][OK] result:145151562421
[20][OK] result:15631
[21][OK] result:1364145642
[22][OK] result:123142321
[23][OK] result:1245363635631
[24][OK] result:141263231
[25][OK] result:124146231562
[26][OK] result:1353
[27][OK] result:1453
[28][OK] result:1263124536
[29][OK] result:15414632
[30][OK] result:132453215
[31][OK] result:1326
[32][OK] result:121456232453
[33][OK] result:12423653
[34][OK] result:15323631562
[35][OK] result:14212
[36][OK] result:12464131353
[37][OK] result:1351513545
[38][OK] result:142124
[39][OK] result:1315321414
[40][OK] result:1531263
[41][OK] result:1426
[42][OK] result:135656
```

全てOK
大丈夫ですかね？？

今回実装したソースコードはgithubに上げました。
走り書きなのでなんか気づいたら変えるかも。
https://github.com/otajisan/realtime

ご参考まで。

## 5.やってみた感想

予想よりだいぶ時間かかりました。
1hじゃ終わらなかったですwww
リファクタとか合わせると2hくらいかかったんじゃないかな？
もっと速く書きたい。

課題自体は「手入力した文字列をサイコロの目に変換する」というシンプルな出題ですが、
実際こういうのってゲーム作ったりとかに活かせそうだな〜とか考えながらやってました。


別の機会に、他の課題にも挑戦してみたいです。

ではまた。