JavaScriptでシェフィを実装する(UI本格化編/カード可視化の巻)


2014年 09月 10日

前回までのあらすじ

シェフィを実装しようと思い立った俺達はとうとう全カードの実装。具体的にはこんな感じ:

残念なシェフィのUI

これでは作った当人も遊ぶ気が起きない。もっとまっとうなUIにしなければ……

今回のあらすじ

シェフィはカードゲームです。
カードゲームなのにカードをカードとして表示していない今の実装はカードゲームとは言えません。
という訳でカードを良い感じにレンダリングして盤面をカードゲームっぽくしましょう。

とはいえ、いきなり完璧なものを作るのは難しいので、
簡単なものから始めて徐々にカードゲーム感を醸し出していくことにしましょう:

  1. 現状、個々のカードはその名前だけが表示されています。
    まずはカードを表す枠を表示し、その中に名前が記載されている形にして、
    何となくカードであることが分かる形にしましょう。
  2. カードのタイプが区別できるよう、枠の表示をタイプ毎に変えましょう。
  3. ひつじカードのランクが区別できるよう、枠の表示をランク毎に変えましょう。

トランプですら創意工夫が凝らされている今の時代
イラストが付いてないカードゲームなんて誰も見向きもしません。
なので本当はイラストを付けたいところですが、
実物のカードをスキャンして使う訳にもいきませんので、
何となくそれっぽい雰囲気の表示にするだけで抑えておくことにします。

カードを枠で表示する

まずは盤面(正確には局面)の表示を行う関数 drawGameTree を見直しましょう:

function drawGameTree(gameTree) {
  var w = gameTree.world;
  S.RANKS.forEach(function (rank) {
    $('#sheepStock' + rank + ' > .count').text(w.sheepStock[rank].length);
  });
  $('#enemySheepCount > .count').text(w.enemySheepCount);
  $('#field > .cards').text(
    w.field
    .map(function (c) {return c.rank;})
    .join(', ')
  );
  $('#hand > .cards').text(textizeCards(w.hand));
  $('#deck > .count').text(w.deck.length);
  $('#discardPile > .cards').text(textizeCards(w.discardPile));
  $('#exile > .cards').text(textizeCards(w.exile));

  $('#message').text(
    gameTree.moves.length == 0
    ? S.judgeGame(gameTree.world).description
    : 'Choose a move:'
  );
  $('#moves').empty().append(gameTree.moves.map(nodizeMove));
}

function textizeCards(cs) {
  if (cs.length == 0)
    return '-';
  else
    return cs.map(function (c) {return c.name;}).join(', ');
}

テキストで表示してるのですから簡単ですね。
この中で textizeCards を利用している部分を以下の形に変更しましょう:

$(‘#hand > .cards’).html(visualizeCards(w.hand));

それと、フィールドの表示ですが、何でこんな小難しいこと書いてたんでしょうね……
個々のカードを表示すれば良いのですから、これも visualizeCards を使った形に変更しましょう。

結果として drawGameTree は以下のようになります:

function drawGameTree(gameTree) {
  var w = gameTree.world;
  S.RANKS.forEach(function (rank) {
    $('#sheepStock' + rank + ' > .count').text(w.sheepStock[rank].length);
  });
  $('#enemySheepCount > .count').text(w.enemySheepCount);
  $('#field > .cards').html(visualizeCards(w.field));
  $('#hand > .cards').html(visualizeCards(w.hand));
  $('#deck > .count').text(w.deck.length);
  $('#discardPile > .cards').html(visualizeCards(w.discardPile));
  $('#exile > .cards').html(visualizeCards(w.exile));

  $('#message').text(
    gameTree.moves.length == 0
    ? S.judgeGame(gameTree.world).description
    : 'Choose a move:'
  );
  $('#moves').empty().append(gameTree.moves.map(nodizeMove));
}

visualizeCards ではカードを表すDOMツリーを返すことにしましょう:

function visualizeCards(cards) {
  return cards.map(visualizeCard);
}

function visualizeCard(card) {
  var $card = $('<span>');
  $card.addClass('card');
  $card.text(card.name);
  return $card;
}

これだけだとテキスト時代の表示と何ら変わらないので、
ここらでようやくCSSを書くことにしましょう:

.card {
  display: inline-block;
  width: 5.6ex;
  height: 7.8ex;
  border: thin solid #000;
  background: #eee;
  overflow: hidden;
  white-space: nowrap;
  margin: 0.5ex;
  padding: 0.5ex;
}

これでゲームの局面を表示させると以下のようになります:

場、手札、捨て場、追放領域のカードがビジュアル系になった様子

おお……まだまだ素朴な外観とはいえ、多少はやる気が湧きそうな形になってきましたね。

カードのタイプ毎に表示を変える

実物のカードだとひつじカードとイベントカードで枠や背景が異なります。
流石に完全に同じものを再現するのは困難ですし権利的にアウトな感じなので、
何となくそれっぽい雰囲気にはなるよう表示を変えましょう。

まず、カードのタイプを判定する関数がないのでそれを定義しましょう:

function cardType(card) {
  return card.rank === undefined ? 'event' : 'sheep';
}

次に visualizeCard の結果にカードのタイプを仕込みます。
また、さらなる装飾用に追加のレイヤーも仕込んでおきましょう:

function visualizeCard(card) {
  var $body = $('<span>');
  $body.addClass('body');
  $body.text(card.name);

  var $card = $('<span>');
  $card.addClass('card');
  $card.addClass(cardType(card));
  $card.append($body);
  return $card;
}

最後にCSSを調整すれば完成です:

.card {
  display: inline-block;
  border: thin solid #000;
  overflow: hidden;
  white-space: nowrap;
  margin: 0.5ex;
}

.card.sheep > .body {
  display: inline-block;
  width: 5.6ex;
  height: 3.8ex;
  padding: 0.5ex;
  border-top: 2ex solid #cff;
  border-bottom: 2ex solid #3c3;
  background: #fff;
  text-align: center;
}

.card.event > .body {
  display: inline-block;
  width: 5.1ex;
  padding: 0.5ex 0.5ex 0.5ex 0;
  background: #ec8;
  text-align: center;
  line-height: 7.8ex;
  border-left: 0.5ex solid #db7;
}
さらにビジュアル系になった図

うーん。これだとひつじカードが何だかウズベキスタンシエラレオネの国旗のようですね……とはいえ実物のカードにおける雲感や草原感を再現するのはちょっと面倒なのでこれで良しとしましょう。最初のグレーの枠だけに比べればこれでも十分です。

ひつじカードのランク毎に表示を変える

実物のカードをよく見てみると、
実はひつじカードのランクがすぐに分かるよう、ボーダーの色が微妙に違っています。
せっかくなのでこれも再現しましょう。

まずは visualizeCard でひつじカードのランクも仕込みましょう:

function visualizeCard(card) {
  var $body = $('<span>');
  $body.addClass('body');
  $body.text(card.name);

  var $card = $('<span>');
  $card.addClass('card');
  $card.addClass(cardType(card));
  $card.addClass('rank' + card.rank);
  $card.append($body);
  return $card;
}

後はひつじカード用のCSSをちょろっと修正するだけです:

.card.sheep.rank1    > .body {border-bottom-color: #ce0;}
.card.sheep.rank3    > .body {border-bottom-color: #9e0;}
.card.sheep.rank10   > .body {border-bottom-color: #6e0;}
.card.sheep.rank30   > .body {border-bottom-color: #0d0;}
.card.sheep.rank100  > .body {border-bottom-color: #0a0;}
.card.sheep.rank300  > .body {border-bottom-color: #080;}
.card.sheep.rank1000 > .body {border-bottom-color: #060;}
よりビジュアル系になった図

なかなか良い感じになってきたのではないでしょうか。
実物準拠にするならひつじのランクは緑のボーダーの部分に表記すべきですが、
これをやるならカード中央部分にイラストが無いと映えませんし、
そこまでやり始めるとキリがないのでここらで切り上げておくことにしましょう。

次回予告

やはり外観は重要です。
これで結構なカードゲーム感が出てきました。
しかし山札やひつじ山の表示は無機質な数字のままです。
ひつじを増やすほんわかしたゲームなのに、こんな冷たい表示のままでは心が荒んでしまいます。

という訳で次回はUI本格化編/カードスタック可視化の巻です。