JavaScriptでシェフィを実装する(基礎作成編)


2014年 07月 30日

前回までのあらすじ

オセロシリーズをしただけで力尽きてしまった。実際のコードはまだ一行も書かれていない。果たして無事にゲームを実装し終えることができるのか。

前提

以降で例示しているコードは

var shephy = {};

(function (S, $) {

  // この辺

})(shephy, jQuery);

に書かれているものだと思って読んでください。

カードの作成

シェフィはカードゲームです。
と言う訳でまずはカードをどう表現するかについて決めましょう。
理屈の上ではカード名さえ区別できれば十分なのですが、
実装面の都合や今後の拡張も考慮して、
基本的にはカード名のみを持つオブジェクトとして表現することにします。

シェフィのカードは2種類あります。
ひつじカードとイベントカードです。

ひつじカードは自分のひつじを表します。
全ひつじの合計を取ったりひつじカードをグレードアップする等々の処理が多いため、
カードには名前だけでなくひつじの数も保持しておくことにします。
この数はひつじの匹数でもありカードのグレードでもあるので、
プロパティ名は rank にしておきます。
例えば《3ひつじ》カードは {name: '3', rank: 3} と表現します。
これを作る関数はこんな感じになるでしょう:

function makeSheepCard(n) {
  return {
    name: n + '',
    rank: n
  };
}

イベントカードは自分のひつじに襲いかかる数々の天変地異や恩恵を表します。
これはひつじカードと異なり名前だけあれば十分です。
例えば《増やせよ》は {name: 'Multiply'} と表現します。
これを作る関数はこんな感じになるでしょう:

function makeEventCard(name) {
  return {
    name: name
  };
}

(※厳密に言えば敵ひつじカードも存在しますが、これは物理的にはカードの形で提供されているものの、実装する上ではカードである必然性が無いので除外しています。)

盤面の作成

次は盤面をどう表現するか決めましょう。
これは手札や山札等のカードが配置される各領域と敵ひつじの数を持つオブジェクトとして表現することにします。

まずは初期盤面を作る makeInitalWorld を定義して、
足りないユーティリティは後から定義する事にしましょう:

S.makeInitalWorld = function () {
  var sheepStock = {};
  S.RANKS.forEach(function (rank) {
    sheepStock[rank] = makeSheepStockPile(rank);
  });

  var initialSheepCard = sheepStock[1].pop();

  return {
    sheepStock: sheepStock,
    field: [initialSheepCard],
    enemySheepCount: 1,
    deck: makeInitalDeck(),
    hand: [],
    discardPile: [],
    exile: []
  };
};

S.RANKS = [1, 3, 10, 30, 100, 300, 1000];

1種類分のひつじ山を作る makeSheepStockPile は以下の通り:

function makeSheepStockPile(n) {
  var cards = [];
  for (var i = 0; i < 7; i++)
    cards.push(makeSheepCard(n));
  return cards;
}

山札を作る makeInitalDeck は以下の通り:

function makeInitalDeck() {
  var names = [
    'All-purpose Sheep',
    'Be Fruitful',
    'Be Fruitful',
    'Be Fruitful',
    'Crowding',
    'Dominion',
    'Dominion',
    'Falling Rock',
    'Fill the Earth',
    'Flourish',
    'Golden Hooves',
    'Inspiration',
    'Lightning',
    'Meteor',
    'Multiply',
    'Plague',
    'Planning Sheep',
    'Sheep Dog',
    'Shephion',
    'Slump',
    'Storm',
    'Wolves'
  ];
  var cards = names.map(makeEventCard);
  shuffle(cards);
  return cards;
}

山札のシャッフルはこんな感じにしておきましょう:

function shuffle(xs) {
  for (var i = 0; i < xs.length; i++) {
    var j = random(xs.length - i);
    var tmp = xs[i];
    xs[i] = xs[j];
    xs[j] = tmp;
  }
}

function random(n) {
  return Math.floor(Math.random() * n);
}

盤面の取り扱い

個々のイベントカードをプレイすると
「ひつじカードを得る」
「ひつじカードを手放す」
「ひつじカードを追放する」
等々、盤面に様々な変化が起きます。
このような盤面に対する操作について関数を定義しておきましょう。

また、この手の関数は盤面を変更する(=副作用バリバリな)ものがほとんどなので、
区別の為に関数名の最後は X を付けることにします。

「nひつじカードを1枚得る」

ひつじ山から対応するカードを取ってフィールドに移動します。
なお、
「ひつじ山に在庫が無い」
「フィールドに空きがない」
場合は何も得られません。

S.gainX = function (world, rank) {
  if (world.sheepStock[rank].length == 0)
    return;
  if (7 - world.field.length <= 0)
    return;

  world.field.push(world.sheepStock[rank].pop());
};

「あるひつじカードを手放す」

フィールドから指定のひつじカードを取ってひつじ山へ戻します。

S.releaseX = function (world, fieldIndex) {
  var c = world.field.splice(fieldIndex, 1)[0];
  world.sheepStock[c.rank].push(c);
};

「あるカードを捨てる」

手札から指定のイベントカードを捨て場に移動します。

S.discardX = function (world, handIndex) {
  var c = world.hand.splice(handIndex, 1)[0];
  world.discardPile.push(c);
};

「あるカードを追放する」

指定された領域からカードを追放領域に移動します
(説明書ではカードの行き先が不明瞭なので、
ここでは手札や山札のような領域の一つとして「追放領域」があるものだとしています)。
追放は手札からとは限らないので元の領域を指定する形にしています。

S.exileX = function (world, region, index) {
  var c = region.splice(index, 1)[0];
  world.exile.push(c);
};

「カードを引く」

山札の一番上のカードを手札に移動します。
「山札にカードが無い」
「手札が既に5枚ある」
場合は何もしません。

S.drawX = function (world) {
  if (world.deck.length == 0)
    return;
  if (5 - world.hand.length <= 0)
    return;

  world.hand.push(world.deck.pop());
};

「山札を補充する」

捨て場にあるカードを山札に移動してシャッフルします。

S.remakeDeckX = function (world) {
  world.deck.push.apply(world.deck, world.discardPile);
  world.discardPile = [];
  shuffle(world.deck);
};

次の局面の列挙

さて、残るは次の局面の列挙 makeGameTree を実装しましょう。
引数としては盤面に加えて「いま何をしている最中か」を表す「状態」が必要です。
この「状態」は常に必要と言う訳ではないので省略可能にしておきます。

S.makeGameTree = function (world, opt_state) {
  return {
    world: world,
    moves: S.listPossibleMoves(world, opt_state)
  };
};

listPossibleMoves
は基本ルールによる部分とカードのプレイによる部分で大別できます。
「カードをプレイしている最中か」どうかは「状態」の有無で区別することにします。

S.listPossibleMoves = function (world, opt_state) {
  if (opt_state === undefined)
    return S.listPossibleMovesForBasicRules(world);
  else
    return S.listPossibleMovesForPlayingCard(world, opt_state);
}

その他のユーティリティ

基本ルールの実装に当たって必要になるユーティリティがいくつかあるので、
予め定義しておきましょう。

盤面のコピー

S.clone = function (x) {
  return JSON.parse(JSON.stringify(x));
};

遅延評価

S.delay = function(expressionAsFunction) {
  var result;
  var isEvaluated = false;

  return function () {
    if (!isEvaluated) {
      result = expressionAsFunction();
      isEvaluated = true;
    }
    return result;
  };
};

S.force = function (promise) {
  return promise();
};

手札を補充すべきかどうか

S.shouldDraw = function (world) {
  return world.hand.length < 5 && 0 < world.deck.length;
};

次回予告

これで基礎部分は一通り作成できました。
後は各種ルールを実装していけばゲーム本体は完成になります。
と言う訳で次回は基本ルール実装編です。