PHPでCUIすごろくをつくるの巻

            今年の上半期は「すごろく」のソーシャルゲームを作っていました。

特に意味はないけれど、PHPを忘れないための復習と今年の成果のひとつとしてすごろくを作ってみた。 ただし、あまり時間もないのでCUI && データベースは使わない簡易版で。

すごろくの要素を考える

すごろくにはどういった人物やオブジェクトが出てくるかを考える。 今回考えるのはシンプルに1人プレイ用だ。 ・プレイヤー ・盤面 ・マス ・サイコロ

すごろくの処理を考える

すごろくはすごくシンプルなゲームで、以下の感じの流れで進んでいく。 1. サイコロをふる。 2. 出た目の数だけ盤面の駒を進める。 3. 止まったマスのイベントをこなす。 ゴールするまで1〜3を繰り返す 4. ゴールする。

今回作るすごろく

・1人プレイ用。 ・マスは「チェックマス(スタートとゴール)」「普通のマス(特になにも起きない)」「ラッキーマス(1マス進む)」「バッドマス(はじめに戻る)」の4種類とする。 インタラクティブにして、それっぽいメッセージを出す。

じゃあ実際に作ってみる

今回は以下のように分けてみた。 ・Sugorokuクラス 駒の現在の位置を記録して、「サイコロをふる」インターフェースを提供する。 ・Boardクラス 盤面にマスがどのように配置されているかを管理する。 ・Massクラス マス情報の管理とマスに止まったときのイベントを処理する。

ソーシャルゲームですごろくを作ったときもこんな構成で作っていたけど、駒の現在位置の情報はユーザーに紐づけてデータベースで管理していた。 盤面の情報やマスの情報もデータベースに入れて管理していたが、不変的な性質のデータなのでわざわざデータベースに入れておく必要なかったなを思っている。 [php] <?php play();

function play() { // 今回はNORMAL盤で遊ぶ $boardType = Board::NORMAL; $sugoroku = new Sugoroku($boardType);

while (!$sugoroku->isGoal()) { $turn = $sugoroku->getCurrentTurn(); $pos = $sugoroku->getCurrentPos(); echo $turn . "ターン目(現在は" . $pos . "マス目だよ)\n"; echo "サイコロをふります。\n"; echo "力を込めてエンターキーを押すんだ!\n"; fgets(STDIN); $sugoroku->shootDice(); } }

class Sugoroku { private $board; // 盤面 private $currentTurn; // 現在のターン数 private $currentPos; // 現在位置

/* * コンストラクタ * * @params $boardType 盤面の種類 / public function __construct($boardType) { $b = new Board();

$this->board       = $b->getBoard($boardType);
$this->currentTurn = 1;
$this->currentPos  = 1;

echo "*-----------------------------------------*\n";
echo "|          すごろくスタート!!!         |\n";
echo "*-----------------------------------------*\n";

$mass = new Mass();
foreach ($this->board as $k => $massType) {
  echo $k + 1 . "マス目 : " . $mass->getMassName($massType) . "\n";
}

echo "*-----------------------------------------*\n";

}

/* * サイコロをふる / public function shootDice() { $this->currentTurn++; $num = mt_rand(1, 6); $this->currentPos += $num; if ($this->currentPos > count($this->board)) { $this->currentPos = count($this->board); }

echo "とりゃ!\n";
sleep(2);
echo "何が出るかな?何が出るかな?何が出るかな・・・\n";
sleep(2);
echo "・・・じゃらじゃじゃん!!!\n";
sleep(1);
echo "『" . $num . "』が出た!\n";

$mass = new Mass();

echo "『" . $mass->getMassName($this->board[$this->currentPos - 1]) . "』に止まった!\n";

$mass->stop($this->board[$this->currentPos - 1], $this);

echo "-------------------------------------------\n";

}

/* * ゴールしたかどうか? * * @return true: ゴール済み false: ゴールしていない / public function isGoal() { return $this->currentPos >= count($this->board); }

/* * 現在の位置を変更する / public function setCurrentPos($pos) { $this->currentPos = $pos; }

/* * 現在の位置を取得する / public function getCurrentPos() { return $this->currentPos; }

/* * 現在のターンを取得する / public function getCurrentTurn() { return $this->currentTurn; } }

class Board { const NORMAL = 1, SPECIAL = 2;

private $boards = array( self::NORMAL => array( MASS::CHECK, MASS::PLAIN, MASS::PLAIN, MASS::LUCKY, MASS::BAD, MASS::PLAIN, MASS::PLAIN, MASS::LUCKY, MASS::BAD, MASS::PLAIN, MASS::LUCKY, MASS::PLAIN, MASS::LUCKY, MASS::BAD, MASS::PLAIN, MASS::BAD, MASS::PLAIN, MASS::LUCKY, MASS::BAD, MASS::CHECK, ), self::SPECIAL => array( MASS::CHECK, MASS::LUCKY, MASS::LUCKY, MASS::LUCKY, MASS::BAD, MASS::LUCKY, MASS::LUCKY, MASS::LUCKY, MASS::BAD, MASS::CHECK, ), );

public function getBoard($boardType) { return $this->boards[$boardType]; } }

class Mass { const CHECK = 1, // チェックマス(スタートであり、ゴールであるマス) PLAIN = 2, // 普通のマス(特になにも起きないマス) LUCKY = 3, // ラッキーマス(なにかラッキーなことが起きるマス) BAD = 4; // バッドマス(なにか不幸なことが起きるマス)

private $mass = array( self::CHECK => array( 'name' => 'チェックマス', 'func' => 'stopCheck', ), self::PLAIN => array( 'name' => '普通のマス', 'func' => 'stopPlain', ), self::LUCKY => array( 'name' => 'ラッキーマス', 'func' => 'stopLucky', ), self::BAD => array( 'name' => 'バッドマス', 'func' => 'stopBad', ), );

/* * マスの名前を取得する / public function getMassName($massType) { return $this->mass[$massType]['name']; }

/* * マスに止まったときに実行する * * @params $massType マスの種類を表す文字列 (例. Mass::CHECK) / public function stop($massType, $sugoroku) { $funcName = $this->mass[$massType]['func']; return $this->{$funcName}($sugoroku); }

/* * チェックマスに止まったときの処理 / private function stopCheck($sugoroku) { echo "チェックマスに止まったよ。やったね!\n"; return true; }

/* * 普通のマスに止まったときの処理 / private function stopPlain($sugoroku) { echo "辺りを見わたしたが何も見つからなかった。\n"; return true; }

/* * ラッキーマスに止まったときの処理 / private function stopLucky($sugoroku) { echo "クードヴァン(1マス先に進む)\n"; $sugoroku->setCurrentPos($sugoroku->getCurrentPos() + 1); return true; }

/* * バッドマスに止まったときの処理 / private function stopBad($sugoroku) { echo "お気の毒ですがあなたの冒険はリセットされてしまいました。(はじめに戻る)\n"; $sugoroku->setCurrentPos(1); return true; } } [/php]

おまけ

データベースで現在の位置を管理する場合を考える。

Usersテーブル

カラム名その他
idintprimary key, auto increment


UserPositionsテーブル

カラム名その他
idintprimary key, auto increment
user_idint
positionint


UPDATE戦略

現在位置のデータをUPDATEするやりかた。 UserPositionsテーブルのuser_idにUnique制約をつけておきます。 [sql] // 駒をうごかす INSERT INTO UserPositions (user_id, position) VALUES (?, ?) ON DUPLICATE KEY UPDATE position = ?;

// 現在位置を取得する SELECT position FROM UserPositions WHERE user_id = ? LIMIT 1; [/sql]


INSERT戦略

現在位置のデータをどんどんINSERTするやりかた。 今回はUnique制約は必要ないです。 [sql] // 駒をうごかす INSERT INTO UserPositions (user_id, position) VALUES (?, ?);

// 現在位置を取得する SELECT position FROM UserPositions WHERE user_id = ? ORDER BY id DESC LIMIT 1; [/sql]

実際この2つを思いついてINSERTを採用した。 特に不自由は感じなかったし、「UserPositionsをある条件で集計してランキングにする」みたいなことをしていたので、別途ログテーブルが必要なくて楽ではあった。 こういうのはどっちが良いとか断言できるほど経験していないので、ソーシャルゲームみたいな大規模なサービスに携わる機会をつくりたいとは思っている。