2006-11-24 (Fri)

#id06110008[PHP]Piece FrameworkでRPG?(もどき)

現在、仕事ではPiece Frameworkを触っていないのですが、とりあえず何か動くものを作らないと理解が進まないし要望も出てこないだろうなと考えて、ちょっとしたRPGもどきを作ってみました。
といっても、ただ出口を探して迷路をうろつくだけなので、もどき以前にゲームにもなっていないのですが…。

・piece-unity-config.yaml
内容は省略しますが、1人で遊ぶものなので、Dispatcher_Continuationで、排他モードにしました。

・フロー定義(webapp/config/flows/Maze.yaml)


firstState: DisplayMaze
lastState:
name: DisplayFinish
view: Finish
viewState:
- name: DisplayMaze
view: Maze
activity:
class: MazeAction
method: setupMaze
transition:
- event: moveNorth
nextState: processMaze
action:
class: MazeAction
method: move
- event: moveSouth
nextState: processMaze
action:
class: MazeAction
method: move
- event: moveEast
nextState: processMaze
action:
class: MazeAction
method: move
- event: moveWest
nextState: processMaze
action:
class: MazeAction
method: move
actionState:
- name: processMaze
transition:
- event: goDisplayMaze
nextState: DisplayMaze
- event: goDisplayFinish
nextState: DisplayFinish
initial:
class: MazeAction
method: initialize
final:
class: MazeAction
method: finalize

アクションクラスは MazeAction としました。
ビューは迷路の画面(Maze)と終了画面(Finish)のみです。
ビューステートは DisplayMaze のみで、アクションステートも processMaze のみ。
フォームボタンで移動方向を指定するため、DisplayMaze での遷移イベントとして、moveNorth moveSouth moveEast moveWest を定義し、それぞれ MazeAction::move() を呼ぶよう指定しています。
移動できるかどうか、といった共通のチェック処理が必要となるので、同じメソッドにまとめました。

・バリデーション定義
迷路を移動するだけ(ビューからはイベントの発生のみ)なので、入力値の検証は行いません。

・アクションクラス(webapp/actions/MazeAction.php)

<?php
require_once 'Piece/Flow/Action.php';
require_once 'Status.php';
require_once 'Maze.php';

class MazeAction extends Piece_Flow_Action
{

var $_flow;
var $_payload;
var $_event;

// フロー開始処理
function initialize()
{
$configs = array(
'Maze' => array(
'fields' => array(
'floor' => array(
'name' => 'floor',
'label' => '通路',
'character' => ' ',
'enterable' => true,
'visible' => true,
),
'wall' => array(
'name' => 'wall',
'label' => '壁',
'character' => '#',
'enterable' => false,
'visible' => true,
),
),
'map' => '
#################
# # # # #
# # # # # # ### #
# # # # # # #
# ### # # # #
# # # ### ##### #
# # # # #
# # # # # # # # #
# # # # # # #
##### # ### # # #
# # #
# # ##### ### # #
# # # #
# # # ### # ### #
# # # # # #
#################
',
'events' => array(
'exit' => array(
'position' => array(13,3),
'message' => '脱出成功!',
'nextEvent' => 'goDisplayFinish',
),
),
),
'Status' => array(
'position' => array(1,1),
'life' => 50,
'message' => null,
),
);

$maze = &new Maze();
$maze->initialize($configs['Maze']);
$this->_flow->setAttribute('maze', $maze);
$status = &new Status();
$status->initialize($configs['Status']);
$this->_flow->setAttribute('status', $status);

}

// フロー終了処理
function finalize()
{
$this->_flow->clearAttributes();
}

// 迷路フォーム設定
function setupMaze()
{
$this->_setupFormAttributes();
$status = &$this->_flow->getAttribute('status');
$maze = &$this->_flow->getAttribute('maze');
$elements = $this->_getFormElement('_elements', array());
$messages = $this->_getFormElement('messages' , array());
$directions = array('north' => '北', 'south' => '南', 'west' => '西', 'east' => '東');
foreach ($directions as $direction => $directionName) {
$position = $this->_movedPosition($status->position, $direction);
$field = &$this->_getField($position);
if ($field === false) {
continue;
}
$messages[] = $directionName . 'は ' . $field->label . ' です。';
if (!$field->enterable) {
$formKey = 'move' . ucfirst($direction);
$elements[$formKey]['_attributes']['disabled'] = 'disabled';
}
}

$viewElement = &$this->_payload->getViewElement();
$viewElement->setElement('_elements', $elements);
$viewElement->setElement('messages', $messages);
$map = $maze->getMap($status->position, 2);
$viewElement->setElement('map', $map);
$viewElement->setElementByRef('status', $status);
}

// 移動
function move()
{
$status = &$this->_flow->getAttribute('status');
$maze = &$this->_flow->getAttribute('maze');
$exit = false;
if (preg_match('/^move(North|South|East|West)$/', $this->_event, $matches)) {
$direction = strtolower($matches[1]);
$position = $this->_movedPosition($status->position, $direction);
$field = &$this->_getField($position);
if ($field->enterable) {
$status->position = $position;
}
$status->life--;
}
$nextEvent = 'goDisplayMaze';
if ($maze->hasEvent($position)) {
$event = &$maze->getEvent($position);
$result = $event->execute($status);
if (is_string($result)) {
$nextEvent = $result;
}
}
if ($status->life <= 0) {
$nextEvent = 'goDisplayFinish';
$status->message = 'あなたは死にました…。';
}
$viewElement = &$this->_payload->getViewElement();
$viewElement->setElementByRef('status', $status);
return $nextEvent;
}

// 移動先の位置を返す
function _movedPosition($currentPosition, $direction) {
$position = $currentPosition;
switch ($direction) {
case 'north':
$position[1]--;
break;
case 'south':
$position[1]++;
break;
case 'west':
$position[0]--;
break;
case 'east':
$position[0]++;
break;
}
return $position;
}

// 位置からフィールドを返す
function &_getField($position) {
$maze = &$this->_flow->getAttribute('maze');
$field = &$maze->getFieldByPosition($position);
return $field;
}

// フォーム要素の属性値を設定する
function _setupFormAttributes()
{
$view = $this->_flow->getView();
$elements = $this->_getFormElement('_elements', array());
$elements[$view]['_attributes']['action'] = $this->_payload->getScriptName();
$elements[$view]['_attributes']['method'] = 'post';
$viewElement = &$this->_payload->getViewElement();
$viewElement->setElement('_elements', $elements);
}

// ビューエレメントから任意のキーの要素を返す
function _getFormElement($key, $default=null)
{
$viewElement = &$this->_payload->getViewElement();
$element = ($viewElement->hasElement($key)) ? $viewElement->getElement($key) : $default;
return $element;
}

}
?>


メソッドは以下の構成にしました。

・initialize()
フロー開始時に一度だけ呼ばれます。
迷路の情報を扱うMazeクラス、プレイヤー自身の情報を扱うStatusクラスを初期化しています。
Mazeクラスは、地形の情報を扱うFieldクラス、地図を扱うMapクラス、イベント(アプリケーションではなく、ゲームのイベント)情報を扱うEventクラスを持っています。

・フロー終了時
finalize()
単にフロー変数をクリアしています。

・迷路画面表示時のフォーム設定
setupMaze()
現在のプレイヤーの位置を元に、各方向の地形が何かを表示するとともに、その地形が進入可能かどうかをチェックしています。
進入不可能であれば、flexy:dynamic属性を利用して、ボタンのINPUT要素にdisabled属性を追加することで、押せないようにしています。
また、現在の位置から2つ先までの地図を取得し、表示要素として設定しています。

・移動時のイベントハンドラ
move()
イベント名をもとにpreg_match()で移動方向を検出し、移動可能であれば現在の位置を変更しています。
迷路を脱出できたかどうかのチェックをイベント(アプリケーションではなく、ゲームのイベント)として実装しているので、移動した位置にイベントがあれば発生させています。


・迷路画面(webapp/templates/Maze/Maze.html)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Maze</title>
</head>
<body>

<dl>
<dt>位置</dt>
<dd>{status.position[0]} - {status.position[1]}</dd>
<dt>体力</dt>
<dd>{status.life}</dd>
</dl>

<table>
<tr>
<td>
<pre>
{map}
</pre>
</td>
<td>
<ul flexy:if="messages">
<li flexy:foreach="messages,message">{message}</li>
</ul>
</td>
</tr>
</table>

<form name="Maze">
<input type="hidden" name="{__flowExecutionTicketKey}" value="{__flowExecutionTicket}" />

<table>
<tr>
<td>&nbsp;</td>
<td><input type="submit" id="moveNorth" flexy:dynamic="yes" name="{__eventNameKey}_moveNorth" value="北" /></td>
<td>&nbsp;</td>
</tr>
<tr>
<td><input type="submit" id="moveWest" flexy:dynamic="yes" name="{__eventNameKey}_moveWest" value="西" /></td>
<td>&nbsp;</td>
<td><input type="submit" id="moveEast" flexy:dynamic="yes" name="{__eventNameKey}_moveEast" value="東" /></td>
</tr>
<tr>
<td>&nbsp;</td>
<td><input type="submit" id="moveSouth" flexy:dynamic="yes" name="{__eventNameKey}_moveSouth" value="南" /></td>
<td>&nbsp;</td>
</tr>
</table>

</form>
</body>
</html>


・終了画面(webapp/templates/Maze/Maze.html)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Maze</title>
</head>
<body>
<h1>終了</h1>
<p flexy:if="status.hasMessage()">{status.getMessage()}</p>
<p><a href="{__scriptName}">もう一度</a></p>
</body>
</html>


動作サンプルはこちらです。

[2006-11-25追記]
西と東が逆だったので修正しました。(うーん、アホだ…)
あとMazeAction::move()で、参照で取得したフロー変数のStatusオブジェクトをフロー変数にセットし直すという無駄な処理が入っていたので、削除しました。

[2008-03-09]
PHP5への入れ替えのため、動作サンプルを削除しました。

posted by K-Holy | |