カードゲームの作り方:デッキ・手札・効果の設計
遊びをつくる執筆にClaudeを使用しています。

ブラウザで動くカードゲームを作りたいとき、最初にぶつかる壁はルールの曖昧さではなく「データ構造の決め方がわからない」という問題です。デッキをどう持ち、手札をどう管理し、カード効果をどう実行するか——この三つを最初に整理しておくと、後から機能を足しても崩れないコードになります。 この記事では、ブラウザゲームとしてのカードゲームに絞り、デッキ・手札・効果のそれぞれをどう設計するかを順番に解説します。フレームワークや言語は問いませんが、例示にはJavaScriptを使います。AIツールでコードを生成する場合でも、設計の考え方を持っていれば指示が格段に具体的になります。
この記事の目次
デッキはオブジェクトの配列として持つ
カードゲームの中心はデッキです。デッキを「カードIDの配列」だけで持つ実装を見かけますが、後から効果や状態を追加するときに破綻しやすいです。最初から「カードオブジェクトの配列」として設計するのが安全です。
const card = {
id: "fire_bolt",
name: "ファイアボルト",
cost: 2,
type: "spell",
effect: "damage",
value: 3,
};このオブジェクトをマスターデータとして別ファイルに定義しておき、デッキはその参照(またはディープコピー)の配列として扱います。マスターデータを直接デッキに入れてしまうと、同じカードを複数枚積んだときに一方の状態変化がもう一方に影響する「参照バグ」が起きます。デッキを組むときは必ずstructuredClone()や独自のコピー関数でオブジェクトを複製してください。
シャッフルはFisher-Yatesアルゴリズムが定番です。乱数の偏りが少なく、実装も短いため、AIに「Fisher-Yatesでシャッフルして」と指示するだけで正確なコードが出ます。
デッキ枚数の上限をどこで管理するかも決めておきます。マスターデータ側にmaxInDeck: 2のようなプロパティを持たせると、デッキ構築画面で自動的に上限チェックをかけられます。後からルールを変えたいときもマスターデータを直せば済みます。
手札は「ゾーン」の概念で管理する
手札の実装で多いミスは、hand配列とdeck配列を別々に操作し、同じカードが二か所に存在するような状態を作ってしまうことです。カードゲームの状態管理は「ゾーン間の移動」として統一するとシンプルになります。
ゾーンとは、カードが存在できる場所の総称です。デッキ・手札・フィールド(場に出たカード)・墓地・除外ゾーンなど、ゲームのルールに応じて定義します。カードはつねにいずれか一つのゾーンにだけ存在し、移動とは「あるゾーンから取り出して別のゾーンに追加する」操作です。
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を持たせ、効果実行関数はそれを読んで処理を分岐させます。
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ツールでカードゲームを作る場合、「カードゲームを作って」と一度に指示しても、設計の詳細を補完してしまい意図しない構造になりがちです。代わりに、次の順番で段階的に指示すると制御しやすくなります。
- マスターデータの構造を定義させる(カードオブジェクトのスキーマ)
- ゲーム状態(ステート)の初期化関数を作らせる
- ゾーン移動関数を作らせる
- 効果解決関数を作らせる
- 表示・UIを作らせる
各ステップで動作確認してから次に進むと、どこでバグが入ったかを追いやすくなります。一度に全部作らせるより時間がかかるように見えますが、修正コストは大幅に下がります。
カードのマスターデータはJSONファイルとして外出しにしておくと、コードを触らずにカードを追加・調整できます。AIに「このJSONに新しいカードを10枚追加して、バランスを取りながらeffectTypeとeffectParamsを設定して」と指示するだけでデータが増やせます。
次の行動
設計のイメージが固まったら、まずカードオブジェクト3〜5枚分のマスターデータJSONを自分で書いてみてください。実際に書くと「このカードにはtargetが必要か」「継続効果はどのプロパティで表現するか」といった設計上の問いが自然に浮かびます。そのうえでAIに「このJSONの構造を読み込んで動作するカードゲームの骨格を作って」と指示すると、意図に近いコードが生成されます。完成したゲームはHTMLまたはZIPファイルとしてアソバース(https://aso-verse.com)に投稿・公開できます。

