JavaScriptでシェフィを実装する(イベントカード実装編(ひつじブースト系))


2014年 08月 20日

前回までのあらすじ

シェフィを実装しようと思い立った俺達はゲームの大枠を遊べる状態にまで持っていけたところ。しかしカードゲームなのにカードが全く実装されていないのでカードゲームになっていない状態。果たして無事にゲームを実装し終えることができるのか。

カードの効果の実装方法

まずは手札のカードをプレイした時の扱いについて復習しましょう。

「手札にあるカードのプレイ」という選択肢は以下の形で列挙していました:

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});
    })
  };
});

重要なのは makeGameTree に渡している {step: c.name} で、
これにより
「どの種類のカードがプレイされたのか」
「カードの効果によりどういう選択をしてきたのか」
を判断して新たな局面を列挙することができます。

基本ルール実装編では一先ずゲームが一通り動くことの確認を優先して、どのカードも効果は何もないという扱いでした。
具体的な定義は以下の通りです:

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

この関数の中身を state に応じて適切に処理するよう置き換えればゲームの完成ということになります。
一先ず、今後の作業のことを考えて、以下のように書き換えましょう:

S.listPossibleMovesForPlayingCard = function (world, state) {
  var h = cardHandlerTable[state.step] || unimplementedCardHandler;
  return h(world, state);
};

// TODO: 個々のカードの効果の実装
var cardHandlerTable = {};

function unimplementedCardHandler(world, state) {
  // TODO: 全カードを実装し終えたらエラーを出すようにする。
  return [{
    description: 'Nothing happened (not implemented yet)',
    gameTreePromise: S.delay(function () {
      return S.makeGameTree(world);
    })
  }];
};

後は cardHandlerTable に個別のカードの処理を追加していけばOKです。
という訳でどんどんカードを実装していきましょう。
カードは単純なものからややこしいものまで色々とあるので、
まずは単純なものから実装を進めていくことにします。

《増やせよ/Multiply》

[3] を得る。

Place one 3 Sheep card in the Field.

一番簡単なカードです。事実上、選択肢は存在しません。
とはいえ、いきなり盤面に3ひつじを増やすと何が起きたか分かり辛いので、
盤面の変化を表す中間の「選択肢」を列挙することにします。

また、ひつじは7枚までしか得られないことと、
3ひつじの在庫が無い場合もあり得るので、
盤面に応じて「3ひつじを得た」の代わりに「何も得られなかった」を提示する必要があります。

cardHandlerTable['Multiply'] = function (world, state) {
  if (world.field.length < 7 && 0 < world.sheepStock[3].length) {
    return [{
      description: 'Gain a 3 Sheep card',
      gameTreePromise: S.delay(function () {
        var wn = S.clone(world);
        S.gainX(wn, 3);
        return S.makeGameTree(wn);
      })
    }];
  } else {
    return [{
      description: 'Nothing happened',
      gameTreePromise: S.delay(function () {
        return S.makeGameTree(world);
      })
    }];
  }
};

《地に満ちよ/Fill the Earth》

[1] を好きなだけ得る。

Place as many 1 Sheep cards as you like in the Field.

《増やせよ/Multiply》の変化形です。
得られるひつじのランクが3から1になっていることは簡単な話なのですが、
得られる枚数が「好きなだけ」と言うのが厄介な点です。
例えば場に1ひつじが1枚だけある状況では、このカードによる選択肢は
「1枚得る」「2枚得る」……「6枚得る」「何も得ない」
の7種類になります。
このような形の選択肢を提示しても良いのですが、
やや分かり辛いことは否めません。

ここはあり得る選択肢を全て列挙するよりは
「1ひつじを得る」「何も得ない」
の2択を繰り返す形にした方が分かり易いでしょう。
具体的な定義は以下の通りです:

cardHandlerTable['Fill the Earth'] = function (world, state) {
  var moves = [];
  if (world.field.length < 7) {
    moves.push({
      description: 'Gain a 1 Sheep card',
      gameTreePromise: S.delay(function () {
        var wn = S.clone(world);
        S.gainX(wn, 1);
        return S.makeGameTree(wn, state);
      })
    });
  }
  moves.push({
    description: 'Cancel',
    gameTreePromise: S.delay(function () {
      return S.makeGameTree(world);
    })
  });
  return moves;
};

《産めよ/Be Fruitful》

ひつじカード1枚をコピーする。

Duplicate one of your Sheep cards.

実物のカードだと「コピーする」という言い回しになっているのですが、
これは「場のひつじカードを1枚選ぶ。そのカードと同じランクのひつじカードを1枚得る」と同じです。
よって「カードを選ぶ」「カードを得る」の2段階に分解できます。
前者は state = {step: 'Be Fruitful'} で、
後者は state = {step: 'Be Fruitful', rank: n} で区別することにしましょう。

cardHandlerTable['Be Fruitful'] = function (world, state) {
  if (state.rank === undefined) {
    if (world.field.length < 7) {
      return world.field.map(function (c) {
        return {
          description: 'Copy ' + c.rank + ' Sheep card',
          gameTreePromise: S.delay(function () {
            return S.makeGameTree(world, {step: state.step, rank: c.rank});
          })
        };
      });
    } else {
      return [{
        description: 'Nothing happened',
        gameTreePromise: S.delay(function () {
          return S.makeGameTree(world);
        })
      }];
    }
  } else {
    return [{
      description: 'Gain a ' + state.rank + ' Sheep card',
      gameTreePromise: S.delay(function () {
        var wn = S.clone(world);
        S.gainX(wn, state.rank);
        return S.makeGameTree(wn);
      })
    }];
  }
};

《繁栄/Flourish》

ひつじカード1枚を選ぶ。その1ランク下のカードを3枚得る。

Choose one of your Sheep cards and receive three Sheep cards of one rank lower.

《産めよ/Be Fruitful》と同様ですが、
「得られるカードのランクが1つ下」「カードを3枚得る」点が異なります。

また、
「1ひつじカードを選んでもそれ下のランクのひつじは存在しないので何も得られない」ことと、
「場の空きが2枚以下の状況では得られる枚数が減る」ことにも注意する必要があります。

まず、1ランク下を求める関数を定義しておきましょう:

S.dropRank = function (rank) {
  if (rank == 1)
    return undefined;
  var r = rank % 3;
  if (r == 0)
    return rank / 3;
  else
    return rank * 3 / 10;
};

カードの効果は以下のように実装できます:

cardHandlerTable['Flourish'] = function (world, state) {
  if (state.rank === undefined) {
    if (world.field.length < 7) {
      return world.field.map(function (c) {
        return {
          description: 'Choose ' + c.rank + ' Sheep card',
          gameTreePromise: S.delay(function () {
            return S.makeGameTree(world, {step: state.step, rank: c.rank});
          })
        };
      });
    } else {
      return [{
        description: 'Nothing happened',
        gameTreePromise: S.delay(function () {
          return S.makeGameTree(world);
        })
      }];
    }
  } else {
    var lowerRank = S.dropRank(state.rank);
    if (lowerRank === undefined) {
      return [{
        description: 'Gain nothing',
        gameTreePromise: S.delay(function () {
          return S.makeGameTree(world);
        })
      }];
    } else {
      var n = Math.min(3, 7 - world.field.length);
      return [{
        description:
          n == 1
          ? 'Gain a ' + lowerRank + ' Sheep card'
          : 'Gain ' + n + ' cards of ' + lowerRank + ' Sheep',
        gameTreePromise: S.delay(function () {
          var wn = S.clone(world);
          for (var i = 1; i <= n; i++)
            S.gainX(wn, lowerRank);
          return S.makeGameTree(wn);
        })
      }];
    }
  }
};

《黄金の蹄/Golden Hooves》

最大でないひつじカードを好きなだけ選び、それぞれ1ランクアップする。

Raise the rank of as many Sheep cards as you like, except for your highest-ranking Sheep card.

これは面倒ですね。
《産めよ/Be Fruitful》と《繁栄/Flourish》は1枚選ぶだけだったのですが、
この《黄金の蹄/Golden Hooves》は複数枚を選ぶことが可能だからです。

これに関しては選んだひつじカードの場におけるインデックスを state に保持しておくことにしましょう。
例えば場のひつじカードが 1 / 3 / 10 / 30 の順で並んでいて、1と10を選んだ場合は
state = {step: 'Golden Hooves', chosenIndice: [0, 2]}
としましょう。

また、1ランク上を求める関数も定義しておきましょう:

S.raiseRank = function (rank) {
  if (rank == 1000)
    return undefined;
  var r = rank % 3;
  if (r == 0)
    return rank * 10 / 3;
  else
    return rank * 3;
};

最終的な実装は以下の通りです:

cardHandlerTable['Golden Hooves'] = function (world, state) {
  var highestRank = max(world.field.map(function (c) {return c.rank;}));
  var chosenIndice = state.chosenIndice || [];
  var moves = [];
  world.field.forEach(function (c, i) {
    if (c.rank < highestRank && chosenIndice.indexOf(i) == -1) {
      moves.push({
        description: 'Choose ' + c.rank + ' Sheep card',
        gameTreePromise: S.delay(function () {
          return S.makeGameTree(world, {
            step: state.step,
            chosenIndice: (chosenIndice || []).concat([i]).sort()
          });
        })
      });
    }
  });
  moves.push({
    description:
      chosenIndice.length == 0
      ? 'Cancel'
      : 'Raise ranks of chosen Sheep cards',
    gameTreePromise: S.delay(function () {
      var wn = S.clone(world);
      for (var i = chosenIndice.length - 1; 0 <= i; i--) {
        var c = world.field[chosenIndice[i]];
        S.releaseX(wn, chosenIndice[i]);
        S.gainX(wn, S.raiseRank(c.rank));
      }
      return S.makeGameTree(wn);
    })
  });
  return moves;
};

function max(xs) {
  return Math.max.apply(Math, xs);
}

《統率/Dominion》

ひつじカードをいくつか選び、数を足して1枚にする(お釣りは来ない)。

Choose any number of Sheep cards in the Field.
Add their values and then replace them with
one Sheep card of equal of lesser value.

例えば

  • 3ひつじカードを2枚選ぶ→選んだひつじを手放す→3ひつじカードを1枚得る
  • 3ひつじカードを4枚選ぶ→選んだひつじを手放す→10ひつじカードを1枚得る

ということです。
これには「選んだひつじカード群のランクの合計値を越えない最大ランク」を求める必要があります。
まずはこれを実装しましょう:

S.compositeRanks = function (ranks) {
  var rankSum = ranks.reduce(function (ra, r) {return ra + r;});
  var candidateRanks = S.RANKS.filter(function (r) {return r <= rankSum;});
  return max(candidateRanks);
};

《黄金の蹄/Golden Hooves》との違いはひつじがどう交換されるかだけです。
なのでほとんど同じ要領で実装できますね。
ただ、何も選ばなかった場合は交換しようが無いので、
そこは特別扱いしてあげないと不味そうです。

cardHandlerTable['Dominion'] = function (world, state) {
  var chosenIndice = state.chosenIndice || [];
  var moves = [];
  world.field.forEach(function (c, i) {
    if (chosenIndice.indexOf(i) == -1) {
      moves.push({
        description: 'Choose ' + c.rank + ' Sheep card',
        gameTreePromise: S.delay(function () {
          return S.makeGameTree(world, {
            step: state.step,
            chosenIndice: (chosenIndice || []).concat([i]).sort()
          });
        })
      });
    }
  });
  if (chosenIndice.length == 0) {
    moves.push({
      description: 'Cancel',
      gameTreePromise: S.delay(function () {
        return S.makeGameTree(world);
      })
    });
  } else {
    moves.push({
      description: 'Combine chosen Sheep cards',
      gameTreePromise: S.delay(function () {
        var wn = S.clone(world);
        for (var i = chosenIndice.length - 1; 0 <= i; i--)
          S.releaseX(wn, chosenIndice[i]);
        S.gainX(wn, S.compositeRanks(
          chosenIndice.map(function (i) {return world.field[i].rank;})
        ));
        return S.makeGameTree(wn);
      })
    });
  }
  return moves;
};

次回予告

イベントカードは全部で19種類。
今回はひつじを増やす系統のカード全6種類を実装しました。
まだ13種類も残っていますが、一気に実装するには量が多いので少しづつ進めていくことにします。
という訳で次回はイベントカード実装編(天変地異系)です。