JavaScriptでシェフィを実装する(基本ルール実装編)


2014年 08月 06日

前回までのあらすじ

シェフィを実装しようと思い立った俺達はゲームの基礎部分の作成を終えたばかり。しかし地味な作業ばかり続いて若干飽きてきた。果たして無事にゲームを実装し終えることができるのか。

基本ルールの実装

必要なユーティリティは前回で実装が一通り終わりました。
ようやく本題の基本ルール = listPossibleMovesForBasicRules の実装に入れます。
これは与えられた盤面から取り得る選択肢を列挙する関数です。
この関数全体はやや長く、一度に全体を見せると把握し辛いので、本来なら

S.listPossibleMovesForBasicRules = function (world) {

  // この辺り

};

に書くコードをルール毎に抜粋して列挙していきます。

選択肢の表現

そういえば肝心の選択肢 = 次にあり得る局面をどう表現するかについては触れていませんでした。

この「次にあり得る局面」はゲーム木そのものなのですが、
これは遅延評価しないと全局面を算出することになって悲惨な事になります。
また、後々UIを作る時に「この選択肢はどういう行動に対応するのか」を分かり易く見せる必要もあります。
今後、あれこれ拡張したくなることも考えると、
選択肢の表現は単にゲーム木を使うのではなく、
「次の局面」を表すオブジェクトを用いる形にした方が良いでしょう。
具体的には以下のような構成のオブジェクトを使うことにします:

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

勝利条件

フィールドに1000ひつじカードがあればプレイヤーの勝利です。
この場合、次の局面は存在しないので戻り値は空シーケンスです。

if (world.field.some(function (c) {return c.rank == 1000;}))
  return [];

敗北条件

フィールドに自分のひつじが1匹もいない = 全滅した場合はプレイヤーの敗北です。
なお、大自然は大変厳しいので割と簡単に全滅します

if (world.field.length == 0)
  return [];

また、敵ひつじが1000匹になった場合もプレイヤーの敗北です。

if (1000 <= world.enemySheepCount)
  return [];

敗北した場合も次の局面は存在しないので戻り値は空シーケンスです。

山札(と手札)の補充

手札も山札も無くなった場合、
山札を補充し、新たな手札を5枚引き、敵ひつじの数を10倍にします。

if (world.hand.length == 0 && world.deck.length == 0) {
  return [
    {
      description: 'Remake Deck then fill Hand',
      gameTreePromise: S.delay(function () {
        var wn = S.clone(world);
        S.remakeDeckX(wn);
        while (S.shouldDraw(wn))
          S.drawX(wn);
        wn.enemySheepCount *= 10;
        return S.makeGameTree(wn);
      })
    }
  ];
}

手札の補充

手札の枚数が5枚未満の場合、5枚になるよう手札を補充します。
山札が無くカードを1枚も引けない場合は補充をスキップします。

if (S.shouldDraw(world)) {
  return [
    {
      description:
        5 - world.hand.length == 1
        ? 'Draw a card'
        : 'Draw cards',
      gameTreePromise: S.delay(function () {
        var wn = S.clone(world);
        while (S.shouldDraw(wn))
          S.drawX(wn);
        return S.makeGameTree(wn);
      })
    }
  ];
}

手札のカードのプレイ

上記の処理のどれにも引っかからなかった場合は手札のカードをプレイします。
手札のカード毎に「それをプレイした場合」の局面を列挙します。

「プレイしたカードを捨て場に置く」は共通事項で、
それより先はカード毎に異なります。
前者はここで取り扱いますが、
後者は listPossibleMovesForPlayingCard で取り扱うので、
何のカードがプレイされたか分かるよう「状態」を指定してゲーム木を作ります。

return world.hand.map(function (c, i) {
  return {
    description: 'Play ' + c.name,
    gameTreePromise: S.delay(function () {
      var wn = S.clone(world);
      S.discardX(wn, i);
      return S.makeGameTree(wn, {step: c.name});
    })
  };
});

個々のカードの効果

基本ルールは実装できたので、
残るはイベントカードをプレイした時の処理 =
listPossibleMovesForPlayingCard です。

イベントカードは全部で19種類存在します。
現時点で一つ一つ実装していると全体が見えなくなってしまうので、
今の段階では何の効果もないものとして扱います。

よって特にここでやることは無く、
与えられた盤面から次の局面を列挙すれば十分です。
つまりは makeGameTree を呼ぶだけで完了です。
が、それだけだと何が起きたか分かり辛いので、
未実装であることを示すようにします:

S.listPossibleMovesForPlayingCard = function (world, state) {
  // TODO: 個々のカードの効果の実装
  return [
    {
      description: 'Nothing happened (not implemented yet)',
      gameTreePromise: S.delay(function () {
        return S.makeGameTree(world);
      })
    }
  ];
};

次回予告

これで基本ルールは一通り実装できました。
個々のカードの効果を除けばゲーム本体は完成したと言って良いでしょう。
しかしUIが付いてないので肝心のゲームを遊ぶことができません。
遊べないゲームに意味はあるのでしょうか。
と言う訳で次回は仮UI作成編です。