見出し画像

[BGA Studio] Hello BGA! その2

Hello BGA! その1に引き続き、BGA Studioでボードゲームを実装する流れを説明していきます。

ちなみに、今回の連載で制作したプロジェクトはこちらにあります。ご自由にどうぞ。

クライアント:状態に応じたUIやアクションを定義する

BGAのクライアントは、状態遷移が発生するごとに特定の関数が呼ばれるようになっています。

ブラウザページ初期化後、最初の状態へ入る → onEnteringState関数
・onEnteringState後 → onUpdateActionButtons関数
・特定の状態から去る → onLeavingState関数
・特定の状態へ入る → onEnteringState関数
(以下、「・」の項目が繰り返される)

せっかくStockを使って手札を定義したので、自分のターンの時のみ手札を選択でき、カードを場に出すことができるようにします。

まずは、自分のターンの時のみ手札を選択できるように、onEnteringState,onLeavingState関数を書き替えます。

onEnteringState: function( stateName, args )
{
  console.log( 'Entering state: '+stateName );

  switch( stateName )
  {
    case "playerTurn":
      if(this.isCurrentPlayerActive()){
        this.playerHand.setSelectionMode(1);
      }
      break;
  }
},

onLeavingState: function( stateName )
{
  console.log( 'Leaving state: '+stateName );

  switch( stateName )
  {
    case "playerTurn":
      this.playerHand.setSelectionMode(0);
      break;
  }
},

isCurrentPlayerActive関数はBGAが提供するAPIで、このWebブラウザでアクセスしているプレイヤーの手番になっているかどうかを返します。
手番プレイヤーになった時だけ、setSelectionMode関数へ1(1枚選択可能)を設定します。また、ターンを抜ける時は0に戻し、選択不可能にします。

そして、手番プレイヤーが選択したカードを出せるように、onUpdateActionButtons関数内でアクションボタンを設置します。

onUpdateActionButtons: function( stateName, args )
{
  console.log( 'onUpdateActionButtons: '+stateName );

  if( this.isCurrentPlayerActive() )
  {
    switch( stateName )
    {
      case "playerTurn":
        if (this.isCurrentPlayerActive()) {
          this.addActionButton('putCard_button', _('Put selected card.'),'onPutCard');
        }
        break;
    }
  }
},

addActionButton関数を使って、ステータスバーにボタンを設置します。

・第一引数:ボタンのDOM ID
・第二引数:ボタンのテキスト
・第三引数:ボタンが押された時に呼ばれるコールバック関数名

テキストは_()でくくったテキストを渡していますが、これは翻訳対象のテキストであることを示しています。

さて、onPutCard関数も定義します。

onPutCard: function(evt)
{
  console.log( 'onPutCard' );
  dojo.stopEvent(evt);
   
  var selected = this.playerHand.getSelectedItems();
  if(selected.length <= 0){
    this.showMessage(_('No cards are selected.'), 'error');
    return;
  }
   
  // console.log(selected);
  this.ajaxcall('/[ゲーム名]/[ゲーム名]/callPutCard.html', {
    lock: true,
    cards: selected.map(card => card.id).join(',')
  }, this, function(result){
  }, function(is_error){
  });
},

色々やっていますが、要するに、以下のような処理をしています。

・「playerHandで選択している全てカード(のID)を出す」というアクションをサーバへ送る(callPutCard.html)
・何も選択していない時は、「カードが選ばれていない」というメッセージを出すだけで何もしない。

選択したカードを複数渡すようにしていますが、現在は設定上1枚しか選択できないので、1枚選択か未選択しかありません。

サーバ:クライアントからの要求を処理する

クライアント側から実行を要求されたアクション(今回の場合callPutCard.html)に対する振る舞いを、サーバ側に実装します。
実装先は[ゲーム名].action.phpです。クライアントからのアクセスによってaction_[ゲーム名]クラスのメンバ関数callPutCardが呼び出されるため、これを実装します。

public function callPutCard()
{
  self::setAjaxMode();

  $numList = explode(',', self::getArg('cards', AT_numberlist, true));
  $cardIDList = [];

  foreach($numList as $pos => $numStr){
    $cardIDList[] = intval($numStr);
  }

  $this->game->putCards($cardIDList);
  self::ajaxResponse();
}

アクションを受信するクラスはゲーム状態を保持していないので、実質的なアクション処理を行うのは[ゲーム名]クラス($this->game)です。このコールバック内では、以下の順序に処理を行っています。

・Ajax受信開始(self::setAjaxMode関数)
・受信したパラメータを解釈(self::getArg関数)
・カードIDを実質的なアクション処理へ渡す(putCards関数)
・Ajax受信終了(self::ajaxResponse関数)

self::getArg関数で受信できる引数の型には制限があります。
イラストリーの実装を開始した時は文字列を渡す方法がなかったため、文字コードを全て数字の配列に変換して渡し、復号していました。今はBASE64形式で渡すことが許容されているようなので、こちらの方が便利です。

さて、次は[ゲーム名]クラス側に実装するputCards関数です。

function putCards($cardList)
{
  self::checkAction('playCard');

  $cardID = $cardList[0];

  $cardInfo = $this->cards->getCard($cardID);
  $actorID = self::getActivePlayerId();

  if (!$cardInfo) {
    self::notifyPlayer($actorID, 'logError', '', [
      'message' => clienttranslate('Invalid card selection! You cannot choose it.')
    ]);
    return;
  }

  if ($cardInfo['location'] != 'hand' || $cardInfo['location_arg'] != $actorID) {
    self::notifyPlayer($actorID, 'logError', '', [
      'message' => clienttranslate('Invalid card selection! You cannot choose it.')
    ]);
    return;
  }

  $this->cards->moveCard($cardID, 'ontable', $this->cards->countCardInLocation('ontable') + 1);

  self::notifyAllPlayers('puttingCard', clienttranslate('${player_name} put card.'), [
    'player_id' => $actorID,
    'player_name' => self::getActivePlayerName(),
    'card' => $cardInfo
  ]);

  $this->gamestate->nextState('playCard');
}

この関数で行っていることは以下の通りです。

playCardステート時に呼ばれたかどうかをチェックする。(self::checkAction関数)
・カード情報を取得し、カードが存在するか&正しい持ち主がカードを出そうとしているかどうかを確認する。間違っていた場合は、出そうとしたプレイヤーに警告を返して何もしない。(self::notifyPlayer関数)
・正しい持ち主がカードを出そうとしていた場合、DB上のカードの状態を更新する。(moveCard関数)
・各プレイヤーにカードが出されたことを通知する。(self::notifyAllPlayers関数)
・状態遷移する。(nextState関数)

putCards関数では、ゲームの色々な処理が行われていますが、これによってクライアント側で新たに発生するイベントは以下の3つです。

・(カードを出すのに失敗した時、)操作プレイヤーにエラー内容と一緒にlogErrorメッセージを通知する。
 ※ここで、clienttranslate関数経由でテキストを渡していますが、これも「翻訳対象のテキストである」という指示です。また、わざわざクライアント側コールバックの引数として渡していますが、これについてはその3で説明します。
・全プレイヤーに、puttingCardメッセージと共に出されたカードの情報を送り、手札の内容を更新する。
nextPlayer状態へ遷移して手番を移動する=各プレイヤーに状態遷移イベントが発生する。

なおnotify系関数に渡したメッセージは、プレー履歴にログとして残るようになっています。

画像1

以上です。ここから先、クライアント側の各種処理と、サーバ側のnextPlayerの処理の定義が必要になりますが、それは次回の記事に持ち越します。

この記事が気に入ったらサポートをしてみませんか?