JavaScriptでシェフィを実装する(UI本格化編/操作性向上の巻)


2014年 11月 08日

前回までのあらすじ

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

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

今回のあらすじ

これまで

  1. ゲームエンジンを作って
  2. 仮UIを載せて
  3. 個々のカードを実装し
  4. UIをまともにする

という過程で実装を進めてきました。
UIは徐々にまともになってきたものの、
残る課題と言えば最初に仮UIを作った時から変わらない「選択肢をボタンとして提示」しているところです。

カードゲームなのにカードをクリックできないとは何事でしょう。
と言う訳で選択肢はボタンではなくカードの形で提示することにしましょう。

設計

「選択肢をカードの形で提示する」というのは、
まあ言葉にするのは簡単ですが、
実際にやろうとすると少々面倒臭いです。
なのでしっかりプランを練ってからにしましょう。

現状の「選択肢をボタンの形で提示する」UIですが、
このボタンの作成は nodizeMove で行っていました:

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

「ボタンを作成」して「クリック時に対応する手を進めるよう設定」しているだけです。
つまり、今回の話はでやることは、
このボタンの部分をカード(の形で画面に表示されるもの)に置き換えるだけの話です。

しかし問題は「この選択肢はどのカードに対応するのか」ということです。
今の選択肢オブジェクトにはそういう情報が一切無いので、
選択肢と盤面のカードを結びつける事ができません。

仕方がないので選択肢オブジェクトの定義を以下の形に変えましょう:

{
  description: ...,      // 選択肢の説明(「手札を補充する」等)
  automated: ...,        // 自動処理可否フラグ
  gameTreePromise: ...,  // 進行後のゲーム木(遅延評価)
  cardRegion: ...,       // 対応するカードがある領域名(handやdiscardPile等)
  cardIndex: ...,        // 対応するカードのインデックス
}

これがあれば選択肢と盤面のカードを結びつけることはできるでしょう。

また、対応するカードが無いということもありえます
(例: ゲームオーバー時に新しくゲームを始めるかどうか)。
そういう場合は従来通りボタンの形で選択肢を提示することにしましょう。

選択肢に対応するカード情報の設定

例えば《落石/Falling Rock》の場合、
場からひつじカードを1枚手放すので、
以下のように調整が必要です:

cardHandlerTable['Falling Rock'] = function (world, state) {
  return world.field.map(function (c, i) {
    return {
      description: 'Release ' + c.rank + ' Sheep card',
      cardRegion: 'field',  // ***
      cardIndex: i,         // ***
      gameTreePromise: S.delay(function () {
        var wn = S.clone(world);
        S.releaseX(wn, i);
        return S.makeGameTree(wn);
      })
    };
  });
};

同じ要領で選択肢を列挙している箇所全てに対して情報を設定していきましょう
(地味な作業なので詳細は省略します)。

局面の表示

さて本題は画面上のカードに対するクリックハンドラーの設定です。
局面の表示を行う drawGameTree を以下のように調整しましょう:

function drawGameTree(gameTree) {
  var w = gameTree.world;
  ...
  var v = {
    field: visualizeCards(w.field),
    hand: visualizeCards(w.hand)
  };
  $('#field > .cards').html(v.field);
  $('#hand > .cards').html(v.hand);
  ...

  ...
    gameTree.moves
      .filter(function (m) {return m.cardRegion !== undefined;})
      .forEach(function (m) {
        v[m.cardRegion][m.cardIndex]
          .click(function () {
            processMove(m);
          });
      });
    $('#moves')
      .empty()
      .append(
        gameTree.moves
        .filter(function (m) {return m.cardRegion === undefined;})
        .map(nodizeMove)
      );
  ...
}

微妙にすっきりしない感じがしますが、
提示方法が二種類ある時点で仕方がありませんね。
諦めましょう。

動作確認(1)

では動作確認してみましょう。

カードがクリックできる様子

う、うーん? 何だかよく分からないですね。
考え直してみると以下の問題がある事が分かります:

  • 選べるカードがどれか区別できない。
  • どのカードを選ぶべきか参考になる情報が画面上に一切表示されていない。

一つづつ直していきましょう。

選べるカードがどれか区別できるようにする

これは drawGameTree で適宜 class を付ければそれでOKでしょう。

function drawGameTree(gameTree) {
  ...
    gameTree.moves
      .filter(function (m) {return m.cardRegion !== undefined;})
      .forEach(function (m) {
        v[m.cardRegion][m.cardIndex]
          .addClass('clickable')
          .click(function () {
            processMove(m);
          });
      });
  ...
}

後はこれに対応するCSSを書くだけです:

.clickable.card {
  box-shadow: 0 0 0.5ex 0 #000;
  cursor: pointer;
}

.clickable.card:hover {
  box-shadow: 0 0 1.0ex 0 #cc0;
}

どのカードを選ぶべきか参考情報を表示する

こいつは微妙に難問です。
これは個々の選択肢が持つ情報ではなく、
選択肢の集合が持つ情報だからです。
しかも選択肢の集合は配列の形で表現していました。
今更これを変えるというのも面倒な話です。

と言う訳で「配列ではあるが参考情報も持つオブジェクト」で扱うことにします。
例えば《落石/Falling Rock》の場合は以下の形で表現します:

cardHandlerTable['Falling Rock'] = function (world, state) {
  var moves = world.field.map(function (c, i) {
    return ...;
    };
  });
  moves.description = 'Choose a Sheep card to release';
  return moves;
};

後はこれに従ってメッセージを表示するよう drawGameTree を調整するだけです
(簡単な話なのでコードは省略)。

動作確認(2)

では動作確認してみましょう。

カードがクリックできて状況がよく分かる様子

おー、これでかなりのカードゲーム感が出てきました。やりましたね。

(今回の変更点の全貌)

次回予告

これで一通りゲームは実装できました。
後は

  • 盤面の表示をより格好良くする
  • どこからどこへカードが移動したか見せる
  • 1000ひつじカード取得後もゲームを続行できるようにする

等と完成度を高める為に取り組める事柄はありますが、
あまり本質的な変更ではないので面白くありません。

元々、本格的なカードゲームを実装しようと思いつつも、
いきなりそういうものに取り組むとまず間違いなく挫折しそうなので、
ひとまず小規模なシェフィを例題にして肩慣らしをしていたのでした。

という訳で次回はデッキ成長型カードゲーム実装編です。