JavaScriptでシェフィを実装する(UI本格化編/雑務自動化の巻)


2014年 10月 18日

前回までのあらすじ

シェフィの実装を始めた俺達はゲームを快適にプレイする為の機能を作り始めたばかり。しかしまだまだやるべき事は多い……

(※ソースコードはGitHubで公開されておりすぐに遊ぶこともできます)

今回のあらすじ

最初の設計では、
実装の簡略化のため、プレイヤーの意思とは関係なく自動的に処理される事柄
(「手札を補充する」や「山札を補充して次のラウンドを始める」等)
も「選択肢」として扱いました。
しかし、この手の「選択肢」までプレイヤーに選ばせるのは不便です。
と言う訳で今回はこの手の「選択肢」を自動処理することにします。

選択肢の表現のおさらい

まずは選択肢の表現についておさらいしましょう。

個々の選択肢の表現については
基本ルール実装編
で以下の形のオブジェクトで表現することに決めました:

{
  description: '「手札を補充する」等の説明',
  gameTreePromise: S.delay(function () {
    // ... 新しい盤面の作成してゲーム木を生成 ...
    return S.makeGameTree(...);
  })
}

これはあくまで個々の選択肢に過ぎません。
実際にはある局面から次に打てる手(=選択肢)を列挙する必要があります。
これは関数 listPossibleMoves として実装していました。
概念上、この関数の戻り値は選択肢の集合ですが、
実装上は選択肢の配列として表現することに決めました。

自動処理すべき選択肢の判別

ここからが本題です。
ある選択肢が自動処理されるべきか否かはどうやって判別すれば良いのでしょうか?

次に取り得る選択肢が複数あるなら明らかに自動処理はできません。
取り得る選択肢が一つしかないなら自動処理しても良さそうですが、
実際にはそうもいきません。

例えば山札が0枚かつ手札が1枚の状況だと、
以下の処理をする他ありません:

  1. その手札のカードをプレイする
  2. プレイした時の効果を処理する
  3. 山札を補充する
  4. 手札を補充する

カードによっては2の処理で選べる選択肢が無い場合もあります。
仮に「取り得る選択肢が一つしかないなら自動処理する」ことにすると、
上記の状況になった途端にかなり先の方まで自動処理されてしまうことになります。
手札のカードのプレイはプレイヤーが決定する事柄なので、
これを勝手に処理されるのはなかなかに不愉快でしょう。

そうするともう選択肢自体に
「これは自動処理してもよい」
という情報を埋め込むしかありませんね。
つまり、選択肢の表現は以下の形に改められます:

{
  description: '「手札を補充する」等の説明',
  automated: ...,  // 自動処理してもよいなら真。そうでなければ偽
  gameTreePromise: S.delay(function () {
    // ... 新しい盤面の作成してゲーム木を生成 ...
    return S.makeGameTree(...);
  })
}

後は各選択肢の作成部分で automated を適宜明示するだけです。
地道な作業ですがやるしかないですね……

どう自動処理するか(1)

残るは自動処理をどう実現するかです。今の
仮UI
では選択肢をボタンとして提示し、
そのボタンが押下されるとボタンに対応する選択肢が表す局面に進み、
画面表示を更新する形にしていました。
個々の選択肢に対するボタンのDOMの作成処理は以下のようになっていました:

function nodizeMove(m) {
  var $m = $('<input>');
  $m.attr({
    type: 'button',
    value: m.description
  });
  $m.click(function () {
    drawGameTree(S.force(m.gameTreePromise));  // ***
  });
  return $m;
}

*** で示した箇所が問題です。
ここでは単に選んだ局面に進めているだけですが、
自動処理できる限り先へ先へと進めておいて、
最終結果を表示すれば良いでしょう。

と言う訳で以下の形に書き換えると良いのではないでしょうか?

function mayBeAutomated(gameTree) {                                  // ***
  return gameTree.moves.length == 1 && gameTree.moves[0].automated;  // ***
}                                                                    // ***

function nodizeMove(m) {
  var $m = $('<input>');
  $m.attr({
    type: 'button',
    value: m.description
  });
  $m.click(function () {
    var gt = S.force(m.gameTreePromise);          // ***
    while (mayBeAutomated(gt))                    // ***
      gt = S.force(gt.moves[0].gameTreePromise);  // ***
    drawGameTree(gt);                             // ***
  });
  return $m;
}

動作確認(1)

と言う訳で実際に実行してみましょう:

些末な事柄が自動処理される様子の一部始終(1)

う、うーん……?
これだとあっという間に3手4手先へ進んだ結果しか表示されないので、
一体途中で何が起こったのか分かりませんね……

どう自動処理するか(2)

と言う訳で途中経過も表示するよう改善しましょう。
ゲームエンジン部分は問題ないのですから、
UI部分をちょいと調整すればどうにかなるはずです。

まず、局面(=盤面+選択肢)を表示する drawGameTree を調整して、
自動処理される局面の場合は適宜メッセージが表示されるようにしましょう:

function drawGameTree(gameTree) {
  // ...
  if (mayBeAutomated(gameTree)) {                       // ***
    $('#message').text(gameTree.moves[0].description);  // ***
    $('#moves').empty();                                // ***
  } else {                                              // ***
    $('#message').text(
      gameTree.moves.length == 0
      ? S.judgeGame(gameTree.world).description
      : 'Choose a move:'
    );
    $('#moves').empty().append(gameTree.moves.map(nodizeMove));
  }
}

その上で選択肢ボタンの押下時処理を
「途中経過の盤面を表示しつつ可能な限り先へ自動処理」
にします:

var AUTOMATED_MOVE_DELAY = 500;                 // ***
                                                // ***
function processMove(m) {                       // ***
  var gt = S.force(m.gameTreePromise);          // ***
  drawGameTree(gt);                             // ***
  if (mayBeAutomated(gt)) {                     // ***
    setTimeout(                                 // ***
      function () {processMove(gt.moves[0]);},  // ***
      AUTOMATED_MOVE_DELAY                      // ***
    );                                          // ***
  }                                             // ***
}                                               // ***

function nodizeMove(m) {
  var $m = $('<input>');
  $m.attr({
    type: 'button',
    value: m.description
  });
  $m.click(function () {
    processMove(m);  // ***
  });
  return $m;
}

動作確認(2)

と言う訳で実際に実行してみましょう:

些末な事柄が自動処理される様子の一部始終(2)

おお……これでクリック数が減って体力が温存できるようになったので、
より長くゲームが遊べるようになりました。やりましたね。

(今回の変更点の全貌)

次回予告

という訳で次回はUI本格化編/操作性向上の巻です。