マガジン

カードゲームの作り方:デッキ・手札・効果の設計

遊びをつくる

執筆にClaudeを使用しています。

会話しながらアイデアを試し、作られた小さなゲームを自分の手で確かめる制作机。
記事のテーマをもとに制作したAI生成イラスト。

ブラウザで動くカードゲームを作りたいとき、最初にぶつかる壁はルールの曖昧さではなく「データ構造の決め方がわからない」という問題です。デッキをどう持ち、手札をどう管理し、カード効果をどう実行するか——この三つを最初に整理しておくと、後から機能を足しても崩れないコードになります。 この記事では、ブラウザゲームとしてのカードゲームに絞り、デッキ・手札・効果のそれぞれをどう設計するかを順番に解説します。フレームワークや言語は問いませんが、例示にはJavaScriptを使います。AIツールでコードを生成する場合でも、設計の考え方を持っていれば指示が格段に具体的になります。

この記事の目次
  1. デッキはオブジェクトの配列として持つ
  2. 手札は「ゾーン」の概念で管理する
  3. 効果は「効果タイプ」と「パラメーター」に分離する
  4. コストと優先度:ゲームバランスを壊しにくい設計
  5. AIを使ったカードゲーム開発の現実的な進め方
  6. 次の行動

デッキはオブジェクトの配列として持つ

カードゲームの中心はデッキです。デッキを「カードIDの配列」だけで持つ実装を見かけますが、後から効果や状態を追加するときに破綻しやすいです。最初から「カードオブジェクトの配列」として設計するのが安全です。

js
const card = {
  id: "fire_bolt",
  name: "ファイアボルト",
  cost: 2,
  type: "spell",
  effect: "damage",
  value: 3,
};

このオブジェクトをマスターデータとして別ファイルに定義しておき、デッキはその参照(またはディープコピー)の配列として扱います。マスターデータを直接デッキに入れてしまうと、同じカードを複数枚積んだときに一方の状態変化がもう一方に影響する「参照バグ」が起きます。デッキを組むときは必ずstructuredClone()や独自のコピー関数でオブジェクトを複製してください。

シャッフルはFisher-Yatesアルゴリズムが定番です。乱数の偏りが少なく、実装も短いため、AIに「Fisher-Yatesでシャッフルして」と指示するだけで正確なコードが出ます。

デッキ枚数の上限をどこで管理するかも決めておきます。マスターデータ側にmaxInDeck: 2のようなプロパティを持たせると、デッキ構築画面で自動的に上限チェックをかけられます。後からルールを変えたいときもマスターデータを直せば済みます。

手札は「ゾーン」の概念で管理する

手札の実装で多いミスは、hand配列とdeck配列を別々に操作し、同じカードが二か所に存在するような状態を作ってしまうことです。カードゲームの状態管理は「ゾーン間の移動」として統一するとシンプルになります。

ゾーンとは、カードが存在できる場所の総称です。デッキ・手札・フィールド(場に出たカード)・墓地・除外ゾーンなど、ゲームのルールに応じて定義します。カードはつねにいずれか一つのゾーンにだけ存在し、移動とは「あるゾーンから取り出して別のゾーンに追加する」操作です。

js
function moveCard(card, fromZone, toZone) {
  const idx = fromZone.indexOf(card);
  if (idx === -1) return; // 存在しなければ何もしない
  fromZone.splice(idx, 1);
  toZone.push(card);
}

この関数一つで、ドロー(デッキ→手札)・プレイ(手札→フィールド)・破壊(フィールド→墓地)・回収(墓地→手札)のすべてを表現できます。「手札上限を超えたらどうするか」「山札が0枚のときのドローをどう扱うか」は、moveCardを呼ぶ前後にチェックを入れる層で処理します。ゾーン移動のロジック自体はシンプルに保つことが重要です。

手札の表示については、枚数が増えると横幅を超える問題が起きます。カードを重ねてファン状に並べる方法と、スクロール可能な横並びにする方法の二つが一般的です。スマートフォン対応が必要なら、カードの最小タップ領域(44px前後)を確保したうえでファン表示を選ぶと自然に見えます。

効果は「効果タイプ」と「パラメーター」に分離する

カード効果をif (card.id === "fire_bolt") { ... }のようにID直書きで実装すると、カード枚数が増えるたびにコードが膨らみます。効果を「タイプ」と「パラメーター」に分けて設計すると、新しいカードを追加してもコードを触らずに済みます。

効果タイプの例を挙げます。

  • damage、意味:対象にダメージ、必要なパラメーター例:target, value
  • heal、意味:HPを回復、必要なパラメーター例:target, value
  • draw、意味:カードを引く、必要なパラメーター例:count
  • buff、意味:ステータスを上昇、必要なパラメーター例:target, stat, value, duration
  • destroy、意味:カードを破壊、必要なパラメーター例:target

マスターデータのカードオブジェクトにeffectTypeとeffectParamsを持たせ、効果実行関数はそれを読んで処理を分岐させます。

js
function resolveEffect(card, context) {
  const { effectType, effectParams } = card;
  switch (effectType) {
    case "damage":
      applyDamage(context[effectParams.target], effectParams.value);
      break;
    case "draw":
      drawCards(context.activePlayer, effectParams.count);
      break;
    // 以降、タイプを追加するだけ
  }
}

この構造の利点は、AIツールへの指示が明確になることです。「effectType: "summon"を追加して、フィールドにトークンを1体出す処理を実装して」と言えば、既存の構造を壊さずに機能を追加できます。

複数効果を持つカード(「2ダメージ与えて1枚引く」など)は、effectsを配列にして複数のオブジェクトを持たせる方法が自然です。resolveEffectを繰り返し呼ぶだけで処理できます。

コストと優先度:ゲームバランスを壊しにくい設計

カードゲームのバランスは、コストと効果の比率で決まります。コスト1で「3ダメージ+1枚ドロー」が出せるカードを作ると、他のカードが使われなくなります。この問題をコード設計で完全に防ぐことはできませんが、数値を一か所で管理することで調整しやすくなります。

効果の強さを数値化する「バリュースコア」をマスターデータに持たせる方法があります。設計者が主観で入力する数値ですが、これがあると「コスト2でバリュースコアが5以上のカードが多すぎる」といった問題を発見しやすくなります。

ターンの優先度(どちらが先に動くか、効果の解決順)は、多くのカードゲームでトラブルになる箇所です。「相手のターン中に発動できる罠カード」や「効果が同時に発動した場合の順序」を最初から考慮するのは難しいため、まずは「アクティブプレイヤーが効果を出して即時解決する」シンプルなモデルから始めるのが現実的です。複雑な割り込みルールは、基本が動いてから追加します。

フィールドに出たカードが持つ「継続効果」(毎ターンHPを回復するなど)は、ターン開始・終了のタイミングでapplyPersistentEffects()を呼ぶ関数を用意し、フィールドのカードを走査して処理します。この関数を最初から設計に組み込んでおくと、後から継続効果カードを追加するときに自然に対応できます。

AIを使ったカードゲーム開発の現実的な進め方

AIツールでカードゲームを作る場合、「カードゲームを作って」と一度に指示しても、設計の詳細を補完してしまい意図しない構造になりがちです。代わりに、次の順番で段階的に指示すると制御しやすくなります。

  1. マスターデータの構造を定義させる(カードオブジェクトのスキーマ)
  2. ゲーム状態(ステート)の初期化関数を作らせる
  3. ゾーン移動関数を作らせる
  4. 効果解決関数を作らせる
  5. 表示・UIを作らせる

各ステップで動作確認してから次に進むと、どこでバグが入ったかを追いやすくなります。一度に全部作らせるより時間がかかるように見えますが、修正コストは大幅に下がります。

カードのマスターデータはJSONファイルとして外出しにしておくと、コードを触らずにカードを追加・調整できます。AIに「このJSONに新しいカードを10枚追加して、バランスを取りながらeffectTypeとeffectParamsを設定して」と指示するだけでデータが増やせます。

次の行動

設計のイメージが固まったら、まずカードオブジェクト3〜5枚分のマスターデータJSONを自分で書いてみてください。実際に書くと「このカードにはtargetが必要か」「継続効果はどのプロパティで表現するか」といった設計上の問いが自然に浮かびます。そのうえでAIに「このJSONの構造を読み込んで動作するカードゲームの骨格を作って」と指示すると、意図に近いコードが生成されます。完成したゲームはHTMLまたはZIPファイルとしてアソバース(https://aso-verse.com)に投稿・公開できます。