シェフィの実装を始めた俺達はゲームを快適にプレイする為の機能を作り込んでいる最中。しかしまだやるべき事は山積み……
(※ソースコードはGitHubで公開されておりすぐに遊ぶこともできます)
これまで
という過程で実装を進めてきました。
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)
);
...
}
微妙にすっきりしない感じがしますが、
提示方法が二種類ある時点で仕方がありませんね。
諦めましょう。
では動作確認してみましょう。
う、うーん? 何だかよく分からないですね。
考え直してみると以下の問題がある事が分かります:
一つづつ直していきましょう。
これは 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
を調整するだけです
(簡単な話なのでコードは省略)。
では動作確認してみましょう。
おー、これでかなりのカードゲーム感が出てきました。やりましたね。
これで一通りゲームは実装できました。
後は
等と完成度を高める為に取り組める事柄はありますが、
あまり本質的な変更ではないので面白くありません。
元々、本格的なカードゲームを実装しようと思いつつも、
いきなりそういうものに取り組むとまず間違いなく挫折しそうなので、
ひとまず小規模なシェフィを例題にして肩慣らしをしていたのでした。
という訳で次回はデッキ成長型カードゲーム実装編です。