📘

カードゲームの対戦機能をクライアント主体で設計したらこうなった

に公開

はじめに

お久しぶりです。にわとりです。

過去に個人開発でカードゲームをリリースしたことがありまして、その時の設計、特にインゲーム部分の設計を公開したいなと以前から思ってました。

この度 claude code のおかげでコードベースから設計書の書き起こしが大変簡単にできるようになったので、その内容を共有します。

僕個人は他人の設計書を読むのは比較的楽しいと感じるほうなので、そのような方、とくにカードゲームのロジックの複雑さを肴に酒が飲めるような方に楽しんでもらえると幸いです。

ゲーム概要やルール説明は一切省略して始めますがご了承ください。では、以下からは claude と一緒に書いた内容です。

概要

本ドキュメントでは、デジタルカードゲーム(DCG)のバトルシステムにおけるクラス間の関係性を、ゲームフローに沿って解説します。

前提条件

  • 複雑なカード効果が必要
    • カードの召喚、移動、攻撃
    • トリガー型能力と継続効果
    • エリア効果とパラメータ補正
  • ターン制の複雑なフェーズ管理
  • AI対戦とネットワーク対戦への対応
  • リプレイ・観戦機能
  • 途中参加・再接続機能

1. データ管理モデルとアーキテクチャ方針

1.1 クライアントサイド同期型アーキテクチャ

本バトルシステムはクライアントサイド同期型のアーキテクチャを採用しています。これは、サーバー上で盤面データを管理する方式とは対照的なアプローチです。

2つのアーキテクチャモデル:

項目 サーバー管理型 クライアントサイド同期型(本システム)
盤面データの場所 サーバー上で一元管理 各プレイヤーのクライアント上で独立管理
主体 サーバーが絶対的な真実 すべてのクライアントが対等(主従関係なし)
同期方式 クライアント → サーバー → クライアント クライアント ⇄ クライアント(Commandの送受信)
計算負荷 サーバーに集中 クライアントに分散
遅延の影響 サーバー応答待ちで遅延発生 ローカル処理のため即座に反応
チート対策 サーバーで検証(強固) 署名検証 + 再接続時の整合性チェック

1.2 クライアントサイド同期型の仕組み

本システムでは、すべてのプレイヤーのクライアントで全く同じ盤面進行が行われることを前提としています。

動作原理:

  1. 決定論的な処理: 同じCommand履歴を同じ順序で実行すれば、必ず同じBattleDataになる
  2. Commandの共有: 各プレイヤーは自分の操作をCommandに変換してネットワーク経由で送信
  3. 同期実行: 全クライアントが受信したCommandを同じ順序で実行
  4. 整合性の保証: Command自体がイミュータブル(不変)で、署名検証により改ざんを検知

1.3 このアーキテクチャの利点と課題

利点:

  • 低レイテンシ: 自分の操作は即座にローカルで反映(サーバー応答待ちなし)
  • サーバーコスト削減: ゲームロジックの計算をクライアントに分散
  • リプレイ機能: Command履歴を保存するだけで対戦の完全再現が可能
  • 再接続対応: Command履歴を取得して再実行すれば、切断後も復帰可能

課題:

  • ⚠️ チート対策: クライアントで処理するため、メモリ改ざん等のリスク
    • 対策: 署名検証、再接続時の整合性チェック
  • ⚠️ 同期ズレのリスク: 実装バグがあると盤面がズレる
    • 対策: 決定論的な処理の徹底、単体テストでの検証

2. ゲーム進行とクラスの役割

ユーザー操作やカード効果に応じて盤面状況を変化させる、というのが基本的な流れになります。

各コンポーネントの説明:

コンポーネント 説明
BattleData 盤面データの保持(詳細は別表)
Operation ユーザーが実行可能な操作を表現(実行前の検証情報を含む)
Command 盤面を更新する不変オブジェクト(履歴として保存可能)
Trigger 盤面変更イベント(Ability発動条件、UI通知に使用)

補足説明:

  • Operation: カードの移動、攻撃、ターン終了など12種類。UI表示用の選択肢データ(AvailableCells等)や事前バリデーション結果(WillSuccessIfSelect)を含む。Commandとは異なり、実行前の「可能性」を表現する
  • Command: 29種類の不変オブジェクト。攻撃宣言、ダメージ計算、ターン進行など、すべての盤面変更を表現。JSON形式でシリアライズ可能で、ネットワーク同期・リプレイ・再接続に使用
  • Trigger: 40種類のイベント。Abilityの発動条件判定とUIへの変更通知の2つの用途で使用。Actor(主語)、Target(対象)、Damage(ダメージ量)などのコンテキスト情報を含む。UniRxのIObservable<IBattleTrigger>として購読可能

BattleDataの構成要素:

構成要素 説明
State 現在のゲームフェーズを表す33種類の状態。ドロー、召喚、攻撃などターン進行の各段階を管理し、実行可能なOperationやTrigger発火条件を決定。例:MainStateでは召喚・移動・攻撃が可能、DrawStateではドロー/チャージの選択のみ可能。Commandによって遷移する
Cards すべてのカード情報を保持するOrderedDictionary。デッキ/手札/フィールド/墓地の区別はLayerKindプロパティ(Hand/Strategy/Field/Book/Grave)で管理。各カードはHP、攻撃力、位置、状態異常、Abilityリストなどのパラメータを持つ。すべてのカードが単一コレクションに統合され、LayerKindで位置を区別する設計
Ability カードに付与された効果。Trigger(発動条件)+ Targeting(対象選択)+ Effect(効果内容)の3要素で構成。JSONで定義され、実行時に動的に読み込まれる
Cells フィールドのマス情報を保持するDictionary。各マスは(x, y)座標を持ち、カードの配置位置を管理
Host/Guest 2人のプレイヤー情報。HP、MP、攻撃回数、ドロー回数など、プレイヤー固有のパラメータを保持
TurnCount 現在のターン数を示す整数値。ターン開始時にインクリメントされ、一部の効果発動条件に使用される
IsFinished バトル終了フラグ。trueの場合、勝敗が確定しゲーム進行を停止

2.1 Operation - ユーザー操作のデータ表現

Operationは、ユーザーが実行可能な操作を表現するデータ構造です。Commandとは異なり、**実行前の「可能性」**を表現します。

Operationの特徴:

  1. UI表示用データを含む

    • AvailableCells: 選択可能なマスのリスト
    • WillSuccessIfSelect: 選択すれば成功するかの事前判定
  2. 事前バリデーション結果

    • 実行できない操作はIsSuccess = falseとなり、UIでグレーアウト表示される
    • ユーザーに「なぜ実行できないか」を視覚的にフィードバック
  3. BattleDataの読み取り専用参照

    • BattleDataとStateを参照して、実行可能な操作を判定
    • BattleDataを直接変更することはできない

全12種類のOperation:

Operation種別 説明
MoveDeclare カードの移動宣言 フィールド上のカードを別のマスへ移動
Attack 攻撃 敵カードへの攻撃
Direct 直接攻撃 プレイヤーへの直接攻撃
Summon 召喚 手札からフィールドへカードを召喚
Feint フェイント召喚 伏せカードとして召喚
TurnFinish ターン終了 現在のターンを終了
DrawOrCharge ドロー/チャージ選択 カードを引くかMPを回復するか選択
StrategyMove ストラテジー移動 初期配置時のカード移動
ReactSummon リアクション召喚 移動に対する召喚リアクション
ReactFeint リアクションフェイント 移動に対するフェイントリアクション
ReactNone リアクションなし リアクションをスキップ
MoveRetry 移動先再選択 リアクション後の移動先変更

2.2 State - ゲームフェーズ管理

DCGにおけるターンの各フェーズを表現します。

全33種類のState:

カテゴリ State名 説明(ゲームフロー視点) 遷移タイプ
バトル開始・終了 InitialState 初期状態(セットアップ待ち) プレイヤー入力待ち
CointossState コイントス(先攻/後攻決定) プレイヤー入力待ち
StrategyState ストラテジータイム(初期配置) プレイヤー入力待ち
BattleStartState バトル開始 自動遷移
BattleFinishState バトル終了 終了
ターン制御 TurnStartState ターン開始 自動遷移
DrawState ドロー/チャージ選択 プレイヤー入力待ち
MainState メインフェーズ(行動選択) プレイヤー入力待ち
TurnFinishState ターン終了 自動遷移
PoisonDamageState 毒ダメージ処理 自動遷移
移動 MoveDeclareState 移動宣言 プレイヤー入力待ち
MoveRetryState 移動再試行(リアクション) プレイヤー入力待ち
MoveResultState 移動完了 自動遷移
攻撃 AttackDeclareState 攻撃宣言 自動遷移
AttackDamageState 攻撃ダメージ計算 自動遷移
AttackDestroyState 攻撃破壊判定 自動遷移
直接攻撃 DirectDeclareState 直接攻撃宣言 自動遷移
DirectDamageState 直接攻撃ダメージ 自動遷移
召喚・フェイント SummonDeclareState 召喚宣言 自動遷移
FeintDeclareState フェイント宣言 自動遷移
効果発動 EffectDeclareState 効果発動宣言 自動遷移
EffectInterruptState 効果割り込み状態 能力発動チェック

遷移タイプの説明:

  • プレイヤー入力待ち: プレイヤーの操作(Command)を待機する状態
  • 自動遷移: 処理完了後、自動的に次のStateへ遷移
  • 能力発動チェック: 発動可能な能力があれば EffectDeclareState へ、なければ元の State に戻る

状態遷移図:

状態遷移の特徴:

  • 自動遷移: AttackDeclareStateAttackDamageStateAttackDestroyState のような連鎖は自動
  • 能力割り込み: 各タイミングでEffectInterruptStateに遷移し、能力発動後に元のStateに戻る
  • プレイヤー入力待ち: MainState, DrawState, MoveDeclareState等で入力を待機

2.3 Command - 盤面変更の指示

プレイヤーの行動やシステムの自動処理を表現するイミュータブルなデータです。

全29種類のCommand:

カテゴリ Command名 説明(ユーザー視点) 実行タイプ
バトル開始・終了 InitialCommand 準備完了を宣言する プレイヤー入力
CointossCommand コイントスで先攻/後攻を決定する プレイヤー入力
StrategyCommand 初期配置フェーズを開始する プレイヤー入力
BattleFinishCommand バトルを終了する 自動実行
BattleForceFinishCommand バトルを強制終了する(切断等) 自動実行
ターン制御 TurnStartCommand 新しいターンを開始する 自動実行
TurnFinishCommand ターンを終了する プレイヤー入力
DrawCommand カードを1枚引く、またはMPを1回復する プレイヤー入力
召喚 SummonDeclareCommand 手札のカードを場に召喚する プレイヤー入力
SummonFinishCommand 召喚処理を完了する 自動実行
移動 MoveDeclareCommand 場のカードを指定したマスに移動させる プレイヤー入力
MoveReactNoneCommand 移動に対してリアクションしない プレイヤー入力
MoveRetryCommand 移動を別のマスに変更する(リアクション時) プレイヤー入力
MoveFinishCommand 移動処理を完了する 自動実行
攻撃 AttackDeclareCommand 場のカードで攻撃する プレイヤー入力
AttackDamageCommand 攻撃ダメージを計算する 自動実行
AttackDestroyCommand 破壊されたカードを墓地に送る 自動実行
直接攻撃 DirectDeclareCommand 相手プレイヤーに直接攻撃する プレイヤー入力
DirectDamageCommand 直接攻撃ダメージを与える 自動実行
フェイント FeintDeclareCommand フェイントを使用して攻撃を無効化する プレイヤー入力
FeintFinishCommand フェイント処理を完了する 自動実行
能力発動 EffectDeclareCommand カードの能力を発動する 自動実行
EffectFinishCommand 能力発動処理を完了する 自動実行
毒ダメージ PoisonDamageCommand ターン終了時の毒ダメージを処理する 自動実行
PoisonFinishCommand 毒ダメージ処理を完了する 自動実行

実行タイプの説明:

  • プレイヤー入力: プレイヤーが明示的に選択する行動
  • 自動実行: ゲームルールに従って自動的に実行される処理(プレイヤーは選択できないが、内部的に状態を進める)

重要な特性:

  • すべてのCommandは履歴として保存される
  • リプレイ機能では、この履歴を再実行することで対戦を再現
  • ネットワーク対戦では、Commandを送受信して盤面を同期
  • 署名検証により改ざんを検知(チート対策)

OperationとCommandの分割理由:

  • Operationは自分の操作のみを表現(BattleDataを読み取って実行可能かを判定)
  • Commandは自分と相手の操作の両方を表現(ネットワーク経由で受信した相手の操作にも対応)
  • この分割により、自分の操作検証と相手の操作受信を独立して処理できる

2.4 Trigger - イベント通知

ゲーム内で発生したイベントを表現し、カード能力の発動タイミングを判定します。

全40種類のTrigger:

カテゴリ Trigger名 発生タイミング(ユーザー視点)
召喚 SummonedTrigger カードが場に召喚された時
StrategySummonedTrigger 初期配置フェーズでカードが場に出た時
移動 MoveDeclaredTrigger カードが移動を宣言した時
MovedTrigger カードが移動を完了した時
MoveFinishedTrigger 移動処理が完全に終了した時
TeleportedTrigger カードがテレポート(瞬間移動)した時
攻撃 AttackDeclaredTrigger カードが攻撃を宣言した時
AttackDamagedTrigger カードが攻撃ダメージを受けた時
AttackDestroyedTrigger カードが攻撃によって破壊された時
AttackFinishedTrigger 攻撃処理が完全に終了した時
AttackCountChangedTrigger カードの攻撃回数が変化した時
直接攻撃 DirectDeclaredTrigger プレイヤーへの直接攻撃を宣言した時
DirectDamagedTrigger プレイヤーが直接攻撃ダメージを受けた時
DirectFinishedTrigger 直接攻撃処理が完全に終了した時
能力効果 EffectDeclaredTrigger カードの能力が発動した時
EffectDamagedCardTrigger カードが能力によってダメージを受けた時
EffectDamagedPlayerHpTrigger プレイヤーが能力によってダメージを受けた時
EffectRecoveredCardTrigger カードが能力によって回復した時
EffectRecoveredPlayerHpTrigger プレイヤーが能力によって回復した時
EffectDestroyedTrigger カードが能力によって破壊された時
ダメージ・破壊 CostDamagedTrigger コスト支払いによってダメージを受けた時
CardGravedTrigger カードが墓地に送られた時
ドロー DrawTrigger カードを引いた時
状態変化 CardAilmentChangedTrigger カードの状態異常が変化した時
AbilityAttachedTrigger カードに能力が付与された時
AbilityDetachedTrigger カードから能力が除去された時
AbilityOblivionChangedTrigger カードの能力封印状態が変化した時
プレイヤー PlayerMpChangedTrigger プレイヤーのMPが変化した時
PlayerHpInitializedTrigger プレイヤーのHPが初期化された時
ターン TurnStartedTrigger ターンが開始された時
TurnFinishTrigger ターンが終了した時
StrategyInitializedTrigger 初期配置フェーズが開始された時
システム StateChangedTrigger バトルの状態が変化した時
悪夢 NightmareSealedTrigger 悪夢能力が封印された時
NightmareUnsealedTrigger 悪夢能力が解放された時

使用例:

  • 「場に出た時、相手プレイヤーに1ダメージ」
    SummonedTriggerを条件に、PlayerDamageEffectを発動

  • 「攻撃された時、攻撃者に2ダメージを与える」
    AttackDamagedTriggerを条件に、CardDamageEffectを発動

  • 「ターン開始時、HPを1回復」
    TurnStartedTriggerを条件に、PlayerRecoverHpEffectを発動

重要な特性:

  • Triggerは能力発動の条件判定に使用される
  • 1つのアクションで複数のTriggerが発行されることがある(例: 攻撃→ダメージ→破壊)
  • Triggerに反応した能力が実行されると、新たなTriggerが発行される(連鎖)

連鎖の具体例:

例1: 召喚→ダメージ→破壊の連鎖

カードA「場に出た時、敵全体に1ダメージ」を召喚
1. SummonedTrigger発行
2. → カードAの能力発動 (EffectDeclaredTrigger)
3. → CardDamageEffect実行 → 敵カードB,C,Dにダメージ
4. → EffectDamagedCardTrigger × 3発行
5. → カードB「ダメージを受けた時、相手プレイヤーに2ダメージ」発動
6. → PlayerDamageEffect実行
7. → EffectDamagedPlayerHpTrigger発行
8. → カードBのHP <= 0で破壊
9. → CardGravedTrigger発行
10. → カードC「味方が破壊された時、HPを2回復」発動

2.5 Ability - カード能力の仕組み

カードが持つ特殊能力を定義するシステム。Abilityは Targeting, Effect, Calculator, Comparator という4つの構成要素から組み立てられ、これらの組み合わせにより多様な能力を表現します。

Abilityの4つのタイプ

タイプ 構成要素 説明
TriggerAbility Targeting + Effect 特定の条件で発動し、効果を実行
ParameterAbility Calculator + Targeting パラメータを補正し、有効期間を管理
AreaAbility Targeting + Effect 範囲内のカードに効果を付与
NightmareAbility Targeting 条件を満たすまで他の能力を封印

構成要素1: Targeting(対象選択)

役割:

  • 能力の発動条件を判定
  • 効果の対象を選択
  • 能力の有効期間を管理

使用箇所:

  • TriggerAbility の Conditions(発動条件)
  • ParameterAbility の Lifetime(有効期間)
  • AreaAbility の EffectTarget, ReachInCondition(範囲判定)
  • Effect の Target(効果対象)

設計思想:
クエリ構造(Sentence/Paragraph)で宣言的に記述

例: 「敵の前列にいるHP3以下のカード」を選択

MapCardTargeting (すべてのカードを起点)
  → FilterCardOwnerTargeting (敵のカードに絞り込み)
  → FilterLayerTargeting (前列に絞り込み)
  → FilterCardHpTargeting (HP3以下に絞り込み)

Targeting の構造:

Targetingは Head(起点)Pipe(フィルタ) を組み合わせたクエリ構造で構成されます。

Head(起点)- 対象の起点を定義:

Targeting名 説明 使用例
MapCardTargeting すべてのカードを起点 「敵のカード」「HP3以下のカード」など
MapPlayerTargeting プレイヤーを起点 「相手プレイヤー」「自分」など
MapCellTargeting フィールドのマスを起点 「前列のマス」「空いているマス」など
MapActorCardTargeting 能力の発動者カードを起点 「このカード」「発動者」など
MapTargetCardTargeting 攻撃対象カードを起点 「攻撃されたカード」など
MapFollowCellTargeting 特定のマスに隣接するマスを起点 「周囲1マス」「隣接マス」など
MapAbilityTargeting 能力自体を起点 能力の発動回数チェックなど
AnyTargeting 常に真を返す(条件なし) 「常に発動」など

Pipe(フィルタ)- 対象を絞り込む:

カテゴリ Targeting名 説明 使用例
カード属性 FilterCardOwnerTargeting カードの所有者で絞り込み 「敵のカード」「味方のカード」
FilterCardKindTargeting カードの種類で絞り込み 「モンスター」「サポート」「イベント」
FilterCardCostTargeting カードのコストで絞り込み 「コスト3以下」「コスト5」
FilterCardAttackTargeting カードの攻撃力で絞り込み 「攻撃力2以上」
FilterCardDamageTargeting カードのダメージで絞り込み 「ダメージを受けているカード」
FilterCardAilmentTargeting カードの状態異常で絞り込み 「猛毒状態」「状態異常なし」
位置情報 FilterLayerTargeting カードの列で絞り込み 「前列」「後列」
FilterCardCellTargeting カードのマスで絞り込み 「特定のマスにいるカード」
FilterInvaderTargeting 侵入者判定で絞り込み 「敵陣にいるカード」
Trigger関連 FilterTriggerTypeTargeting Triggerの種類で絞り込み 「召喚Trigger」「攻撃Trigger」
FilterTriggerKindTargeting Triggerの詳細種類で絞り込み 特定のTriggerイベント
FilterTriggerDamageTargeting Triggerのダメージ量で絞り込み 「ダメージ3以上のTrigger」
FilterTargetCardTargeting Triggerの対象カードで絞り込み 「攻撃されたカード」「対象カード以外」
能力関連 FilterAbilityConsumerTargeting 能力の適用先で絞り込み 「この能力を持つカード」
FilterAbilityProducerTargeting 能力の発生源で絞り込み 「この能力を付与したカード」
FilterActivateCountTargeting 能力の発動回数で絞り込み 「1ターンに1回まで」
プレイヤー情報 FilterTurnPlayerTargeting ターンプレイヤーで絞り込み 「自分のターン中」
FilterPlayerDirectDamageInThisTurnTargeting 今ターンの直接攻撃で絞り込み 「直接攻撃済み」
数・選択 TakeTargeting 対象の数を制限 「ランダムに2枚」「最大3枚」
CountTargeting 対象の数をカウント 「カードの枚数」

Comparator(比較器):

Comparator 説明 使用例
Equal 等しい 「HP = 3」
GreaterThan より大きい 「攻撃力 > 2」
LessThan より小さい 「コスト < 4」
GreaterThanOrEqual 以上 「HP ≥ 5」
LessThanOrEqual 以下 「ダメージ ≤ 1」

Targetingの組み合わせ例(カードゲームでよくある効果):

  • 敵の前列全体 - 前列に配置された敵カード全てを選択

    MapCard & FilterCardOwner(Enemy) & FilterLayer(Front)
    
  • HP3以下の敵カード1枚 - 残りHPが3以下の敵から1枚を選択

    MapCard & FilterCardOwner(Enemy) & FilterCardDamage & Take(1)
    
  • 毒状態でない敵ランダム2枚 - 毒を持たない敵からランダムに2枚選択

    MapCard & FilterCardOwner(Enemy) & FilterCardAilment(NotPoison) & Take(2, Random)
    
  • コスト3以下の味方全体 - コストが3以下の味方カード全てを選択

    MapCard & FilterCardOwner(Player) & FilterCardCost(LessThanOrEqual, 3)
    
  • 攻撃力2以上の敵 - 攻撃力が2以上の敵カードを選択

    MapCard & FilterCardOwner(Enemy) & FilterCardAttack(GreaterThanOrEqual, 2)
    
  • 周囲1マスの味方 - 自カードの周囲1マス内にいる味方カードを選択

    MapFollowCell(D1) & MapCard & FilterCardOwner(Player)
    
  • 場に出たカード - カードが場に出た時のトリガーで、そのカードを参照

    FilterTriggerType(ActorTrigger) & MapActorCard
    
  • 攻撃されたカード - 攻撃対象になったカードを参照

    FilterTriggerType(CardTargetTrigger) & MapTargetCard
    
  • ダメージ3以上を与えた時 - 与えたダメージが3以上の攻撃成功時に発動

    FilterTriggerKind(AttackDamaged) & FilterTriggerDamage(GreaterThanOrEqual, 3)
    
  • 自分のターン開始時 - 自分のターンが開始した瞬間に発動

    FilterTriggerKind(TurnStarted) & FilterTurnPlayer
    
  • 敵陣にいる味方カード - 敵陣に侵入している味方カードを選択

    MapCard & FilterCardOwner(Player) & FilterInvader
    
  • 破壊されたカード以外 - トリガーで破壊されたカードを除外して選択

    MapCard & FilterTargetCard(Except)
    
  • 1ターンに1回まで - 同一ターン内で既に発動していないことを条件に

    FilterActivateCount(LessThan, 1)
    

構成要素2: Effect(効果実行)

役割:
盤面を実際に操作する処理を実行

主な Effect の種類(12種類):

Effect 説明
PlayerDamageEffect プレイヤーにダメージ
PlayerRecoverHpEffect プレイヤーのHP回復
CardDamageEffect カードにダメージ
CardRecoverEffect カードのHP回復
CardMoveEffect カードを移動
CardSwapEffect カードの位置を入れ替え
CardAilmentEffect カードに状態異常を付与
AttachAbilityEffect カードに能力を付与
PlayerHandEffect カードをドロー
ConsumeFeintEffect フェイントを消費

使用箇所:

  • TriggerAbility の Effect プロパティ

重要な特性:

  • Effect実行 → Trigger発行 → 連鎖発動
  • すべての Effect は Targeting で対象を選択

Effectの使用例(カードゲームでよくある効果):

  • 場に出た時、敵プレイヤーに1ダメージ - 召喚時に相手の体力を削る効果

    Conditions = TriggerIs(Summoned) & ActorIsConsumer()
    Effect = PlayerDamageEffect { Amount = 1, Target = EnemyPlayer() }
    
  • 攻撃時、自分のHPを2回復 - 攻撃するたびに自己回復

    Conditions = TriggerIs(AttackDamaged) & ActorIsConsumer()
    Effect = PlayerRecoverHpEffect { Amount = 2, Target = MapPlayer() }
    
  • 破壊された時、周囲の味方全体に2ダメージ - 自爆効果

    Conditions = TriggerIs(CardGraved) & ActorIsConsumer()
    Effect = CardDamageEffect { Amount = 2, Target = MapFollowCell(D1) & MapCard & FilterCardOwner(Player) }
    
  • ターン開始時、ランダムな敵1体を毒状態に - 持続デバフ付与

    Conditions = TriggerIs(TurnStarted) & FilterTurnPlayer()
    Effect = CardAilmentEffect { Ailment = Poison, Target = MapCard & FilterCardOwner(Enemy) & Take(1, Random) }
    
  • HP3以下の敵を倒した時、1枚ドロー - 撃破報酬

    Conditions = TriggerIs(CardGraved) & FilterCardOwner(Enemy) & FilterCardDamage(LessThanOrEqual, 3)
    Effect = PlayerHandEffect { Amount = 1, Target = MapPlayer() }
    
  • 場に出た時、前方1マスに移動 - 召喚時の自動前進

    Conditions = TriggerIs(Summoned) & ActorIsConsumer()
    Effect = CardMoveEffect { Direction = Forward, Distance = 1, Target = MapActorCard() }
    
  • 攻撃時、対象と位置を入れ替える - 攻撃後に敵と場所交換

    Conditions = TriggerIs(AttackDamaged) & ActorIsConsumer()
    Effect = CardSwapEffect { Target = MapTargetCard() }
    
  • ターン終了時、味方全体のHPを1回復 - 全体回復オーラ

    Conditions = TriggerIs(TurnFinished)
    Effect = CardRecoverEffect { Amount = 1, Target = MapCard & FilterCardOwner(Player) }
    
  • 敵を倒した時、その敵の能力をコピー - 撃破時能力獲得

    Conditions = TriggerIs(CardGraved) & FilterCardOwner(Enemy)
    Effect = AttachAbilityEffect { AbilitySource = MapTargetCard(), Target = MapActorCard() }
    
  • ダメージ5以上受けた時、フェイント1消費して無効化 - ダメージカット

    Conditions = TriggerIs(Damaged) & ActorIsConsumer() & FilterTriggerDamage(GreaterThanOrEqual, 5)
    Effect = ConsumeFeintEffect { Amount = 1, Target = MapActorCard() }
    

構成要素3: Calculator(演算器)

役割:
ParameterAbility でパラメータの補正値を計算

使用箇所:

  • ParameterAbility の Calculator プロパティ専用

演算モード:

  • Add: 加算(元の値 + 補正値)
  • Subtract: 減算(元の値 - 補正値)
  • Multiply: 乗算(元の値 × 補正値)
  • Equalizer: 固定値(補正値で上書き)
  • Max: 最大値(元の値と補正値の大きい方)
  • Min: 最小値(元の値と補正値の小さい方)

実装例(AbilityTemplate: JSONデータ出力・調整用の補助ツールより):

// 「このカードの攻撃力+2」
new ParameterAbility
{
    ExpectedParameter = Attack
  , Calculator = NumericCalculator.Add(2)
  , Lifetime = Forever()  // Targeting使用
}

// 「移動コストを0に固定」
new ParameterAbility
{
    ExpectedParameter = MoveCost
  , Calculator = NumericCalculator.Equalizer(0)
    , Lifetime = UntilMoveDeclared()  // Lifetimeパラメータ (複数のTargeting句の組み合わせ)
}

能力タイプの組み合わせ例

TriggerAbility(条件発動能力)
= Targeting(条件判定) + Effect(効果実行)

「攻撃された時、攻撃者に2ダメージ」
- Targeting: AttackDamagedTrigger & ActorIsConsumer()
- Effect: CardDamageEffect(2, Target=攻撃者)

ParameterAbility(パラメータ補正能力)
= Calculator(補正計算) + Targeting(有効期間)

「エンドタイムまで、このカードの攻撃力+3」
- Calculator: NumericCalculator.Add(3)
- Targeting: UntilTurnFinished()

AreaAbility(範囲効果能力)
= Targeting(範囲判定) + Effect(付与する能力)

「周囲1マスの味方の攻撃力+1」
- Targeting: Reach(D1) & PlayerRubric()
- Effect: AttachAbility(ParameterAbility(Attack, +1))

NightmareAbility(悪夢能力)
= Targeting(解放条件)

「自分のHPが10以下で解放」
- Targeting: PlayerHp <= 10
- 封印中は他の能力が無効化される

Ability設計の重要な特性

  1. 宣言的な記述

    • Targeting による条件の組み合わせ
    • Effect による効果の明示的な定義
    • AbilityTemplate(JSONデータ出力・調整用の補助ツール)による再利用可能なパターン
  2. 連鎖発動の仕組み

    • Effect実行 → Trigger発行 → TriggerAbility発動 → Effect実行...
    • 無限ループ防止のため、発動回数制限を実装
  3. 動的な能力付与

    • AttachAbilityEffect で実行時に能力を追加
    • AreaAbility でカードの移動に応じて自動付与/削除
  4. JSONシリアライズ可能な設計

    • すべての Ability, Effect, Targeting, Calculator, Comparator は JSON 形式でシリアライズ可能
    • カードデータはサーバーから JSON 形式で配信され、実行時に読み込まれる
    • アプリをアップデートせずに新しいカード能力を追加できる
    • 専用の JsonConverter でポリモーフィックなデシリアライズに対応

JSONシリアライズの実装詳細:

コンポーネント JsonConverter 用途
IBattleAbility BattleAbilityConverter TriggerAbility, ParameterAbility, AreaAbility, NightmareAbility の型判別
IBattleEffect BattleEffectConverter PlayerDamageEffect, CardDamageEffect など12種類のEffect型判別
IBattleTargetingPhrase BattleTargetingPhraseConverter MapCardTargeting, FilterXxxTargeting など20種類以上の型判別
ICalculator CalculatorConverter NumericCalculator, BoolCalculator など6種類の型判別
IComparator ComparatorConverter Equal, GreaterThan など5種類の型判別

カードデータの読み込みフロー:

1. サーバーからJSON取得 (CardsRepository)
   ↓
2. JSON → List<DisplayCard> へデシリアライズ
   ↓
3. DisplayCard.AbilitySet (JSON内に能力定義が含まれる)
   ↓
4. バトル開始時: DisplayCard → BattleCard へ変換
   ↓
5. AbilitySet.Clone() で実行時インスタンス化

利点:

  • 新カード追加時、アプリ本体の再ビルド不要
  • サーバー側でカードバランス調整が可能
  • A/Bテストや期間限定カードの実装が容易
  • JSONとコードの整合性チェック機能を実装済み (Editor拡張)

3. 具体例で理解するクラス間の関係

3.1 例1: 状態異常の付与と撃破報酬の連鎖(TriggerAbility)

使用カード: アビルマ・エスターシャ (ID:4, 攻撃2/HP6/コスト3)

能力:

  • 能力1: 「攻撃で相手のモンスターにダメージを与えた時、ランダムな相手のモンスター2枚を【猛毒状態】にする」
  • 能力2: 「攻撃で相手の状態異常のモンスターを墓地に送った時、このカードのライフを3回復する」

実行フロー(ターン1: アビルマが敵カードAを攻撃):

State Operation Command 盤面状態 Trigger 補足事項
MainState AttackOperation - 敵A(HP2), B(HP3), C(HP4) - ユーザーが攻撃操作を実行 → 実行可能判定
AttackDeclareState - AttackDeclareCommand 同上 AttackDeclaredTrigger 攻撃宣言処理 → 自動的にAttackDamageCommand生成
AttackDamageState - AttackDamageCommand 敵A(HP0) AttackDamagedTrigger アビルマの攻撃2ダメージ → 敵AのHP: 2→0
(能力発動) - - 敵B(猛毒), C(猛毒) - 能力1条件判定: TriggerIs(AttackDamaged) & ActorIsConsumer() & TargetIsRubric() → 満たす
CardAilmentEffect実行: MapCard & FilterCardOwner(Enemy) & Take(2, Random) でランダムに敵2枚選択 → 敵B・Cに猛毒状態付与
(能力発動) - - 同上 CardGravedTrigger 敵Aが破壊される(HP≤0) → CardGravedTrigger発行
(能力発動) - - アビルマ(HP9) - 能力2条件判定: TriggerIs(CardGraved) & ActorIsConsumer() & TargetCardIsAilmented() → 満たす
PlayerRecoverHpEffect実行: アビルマのHP +3(6→9)

技術的特徴:

  • TriggerAbilityは2つのコンポーネントで構成:
    • Conditions: 発動条件(Targeting式の組み合わせ)
    • Effect: 実行する効果
  • 攻撃による連鎖: AttackDamagedTrigger → CardAilmentEffect → CardGravedTrigger → PlayerRecoverHpEffect
  • すべての条件式・効果はJSONでシリアライズ可能

3.2 例2: AreaAbility(エリア効果による範囲バフ)

使用カード: ファルハラ (ID:22)

能力 (AreaAbility): 「範囲【D1】にある自分のモンスターのアタックが1増加する」

実行フロー(ファルハラ召喚と味方移動による範囲バフの付与・解除):

State Operation Command 盤面状態 Trigger 補足事項
MainState SummonOperation SummonCommand ファルハラ + 味方A(左) + 味方B(右) + 味方C(下) SummonedTrigger ファルハラ召喚
reach_in_condition評価: MapActorCard & MapFollowCell(D1) & FilterCardKind(Rubric) & FilterCardOwner(Player) → D1範囲内に味方A・Bを検出
味方A・BにParameterAbility(攻撃+1, IsAttached=true)を付与
味方Cは範囲外のため効果なし
MainState MoveOperation MoveCommand ファルハラ + 味方A(遠方) + 味方B(右) + 味方C(下) MovedTrigger 味方Aが範囲外に移動
ファルハラのreach_in_condition再評価 → 味方Aが範囲外
味方Aに付与していたParameterAbility(IsAttached=true)を自動解除
味方Bのみバフ継続
MainState - - 味方A + 味方B + 味方C CardGravedTrigger ファルハラ破壊(攻撃により破壊されたと仮定)
IsAttached=trueかつProducer=ファルハラの全ParameterAbilityを検索 → 味方Bの攻撃+1バフを削除
全ての範囲バフが解除される

JSONデータ(AreaAbilityの抜粋):

{
  "kind": 3,  // AreaAbility
  "reach": 131,  // D1範囲
  "zone": 0,
  "reach_in_condition": {
    "phrases": [
      {"kind": 10, "type": 0},  // MapActorCard (ファルハラ起点)
      {"kind": 15},  // MapFollowCell (周囲のセル)
      {"cardKind": 1, "kind": 3},  // FilterCardKind(Rubric)
      {"kind": 5, "opponent": false}  // FilterCardOwner(Player)
    ]
  },
  "effect": {
    "calc": {"amount": 1, "kind": 0, "mode": 0},  // NumericCalculator.Add(1)
    "kind": 1,  // ParameterAbility
    "parameter": 0,  // Attack
    "lifetime": []
  }
}

技術的特徴:

  • AreaAbilityは範囲式(reach_in_condition)で対象を検索し、各対象にParameterAbilityを付与
  • 付与されたParameterAbilityはIsAttached=trueでマークされ、付与元が破壊されると自動削除
  • 対象カードが範囲外に移動した場合も、次の盤面更新時に自動解除される

4. Facilitator/Facilitatee - バトル進行の制御層

4.1 Facilitatorの役割

Facilitator(進行役) は、バトル全体のセットアップと進行を管理する最上位のコントローラーです。BattleModelがゲームロジックに専念するため、プレイモード固有の処理はFacilitatorに委譲されています。

主な責務:

  1. バトルのセットアップ - プレイヤー情報、カードデータ、メタデータの初期化
  2. プレイモードの制御 - オンライン/ローカル/リプレイ等、モードごとの差分を吸収
  3. Facilitateeの管理 - Host/Guestプレイヤーの生成と管理
  4. ネットワーク同期 - コマンドの送受信、盤面復元
  5. ゲーム状態の監視 - バトル開始/終了の判定、タイムアウト処理

Facilitatorの種類(7種類):

Facilitator プレイモード 主な用途
PhotonFacilitatorModel オンライン対戦 Photon + Beyondサーバーでの通常対戦(再接続機能を含む)
DebugFacilitatorModel デバッグ 開発用ローカル対戦(任意のブックで検証可能)
TutorialFacilitatorModel チュートリアル 初回プレイ時のチュートリアル
TrainingFacilitatorModel トレーニング ローカルAI対戦
SandbagFacilitatorModel サンドバッグ デッキテスト用(相手は何もしない)
ReplayFacilitatorModel リプレイ 過去の対戦を再生
LearningFacilitatorModel 機械学習 AIの学習用環境

4.2 Facilitateeの役割

Facilitatee(参加者) は、各プレイヤー(Host/Guest/Audience)の行動を表現するコンポーネントです。

主な責務:

  1. 操作の実行可能性判定 - GetAvailableOperations() でユーザーが選択可能な操作をリストアップ
  2. 操作の処理 - HandleNewBattleOperation() でユーザー操作をCommandに変換
  3. Commandの送信 - SendBattleCommand() でネットワーク経由またはローカルで送信
  4. ローカルイベント発行 - ストラテジータイム等のUI専用イベントを発行

Facilitateeの種類:

Facilitatee 役割 説明
DefaultPlayerModel セッションプレイヤー ユーザー自身の操作を処理
DefaultOpponentModel 対戦相手 ネットワーク経由で相手のCommandを受信
TutorialOpponentModel チュートリアルAI 決められた行動を実行するAI
SandbagOpponentModel サンドバッグAI 何も行動しないAI(デッキテスト用)
ComputerOpponentModel AI対戦相手 盤面を評価して行動するAI
AudiencePlayerModel 観戦者 操作不可、盤面の監視のみ

4.3 Facilitator/Facilitateeの協調動作

バトルセットアップフロー:

ユーザー操作の処理フロー:

4.4 プレイモードごとの差分

PhotonFacilitatorModel(オンライン対戦 + 再接続):

  • Photon経由でコマンド送受信
  • Beyondサーバーにバトル履歴を保存
  • 対戦相手の切断監視とリトライ処理
  • カード使用回数の記録
  • 途中参加・再接続時のバトル復元機能(詳細は後述)

DebugFacilitatorModel(デバッグ):

  • ローカルで完結(サーバー通信なし)
  • 任意のブックを直接指定可能
  • ローカルプレイモード(履歴を保存しない)も選択可能
  • 盤面復元でデバッグ効率化

TutorialFacilitatorModel(チュートリアル):

  • 固定されたブック構成
  • TutorialOpponentModelで相手の行動を制御
  • サーバー通信なし、ローカル完結

4.4.1 PhotonFacilitatorModelの再接続機能

PhotonFacilitatorModel は、オンライン対戦中の通信切断や途中参加に対応するため、バトル状態の復元機能を内蔵しています。独立した PhotonReconnectFacilitatorModel ではなく、PhotonFacilitatorModel 自身がこの機能を持っています。

再接続のシーケンス:

実装の詳細(PhotonFacilitatorModel.cs:289-332):

  1. 履歴取得(1回目):

    • GameCommandHistoryRepository.Get(RoomId) でBeyondサーバーからコマンド履歴を取得
    • 履歴が空なら新規バトルとして開始
  2. 高速リプレイ:

    • 取得した全コマンドを BattleModel.UpdateBattleState() で順次実行
    • UIは非表示のまま、FSMだけが高速で遷移
  3. 履歴取得(2回目):

    • 復元中に対戦相手がコマンドを送信している可能性があるため、再度履歴を取得
    • histories2.Count > histories.Count なら追加コマンドを実行
  4. バトル開始通知:

    • BattleModel.NotifyReadyToBattle() でUIに復元完了を通知
    • ユーザーは途中の盤面から操作可能になる

設計上の利点:

観点 利点
Commandパターンとの相性 すべてのゲーム進行がCommandで表現されているため、履歴を再実行するだけで盤面を復元可能
JSON直列化 CommandはすべてJSONシリアライズ可能なため、サーバー保存・通信が容易
観戦者への応用 同じ仕組みでAudience(観戦者)も途中参加可能
デバッグ効率化 コマンド履歴をローカルに保存すればリプレイとして再利用可能
署名検証 Commandには署名が含まれており、改ざん検知が可能

エラーハンドリング:

  • ルーム未参加時: !PhotonNetwork.InRoom で例外をスロー
  • 履歴取得失敗時: !(historiesGet is RoomHistoriesGetSuccess) で例外をスロー
  • 対戦相手のTTL切れ: BattleForceFinishCommand でバトルを異常終了
  • コマンド送信失敗時: リトライ処理(最大10回、5秒間隔)

4.4.2 ComputerOpponentModel(AI対戦)

概要:

  • TrainingFacilitatorModelで使用されるAI対戦相手
  • BattleEvaluatorによる盤面評価で行動を自動決定
  • 別スレッドでの並列評価により、UI停止なしで思考処理を実行

評価システムの核心:

要素 説明
シミュレーション BattleModel.Clone()で未来の盤面を仮想実行
スコアリング Score = Σ(報酬 - コスト) で各行動を評価
並列処理 最大12並列で評価、勝利確定手(Score>10000)発見時に早期終了
報酬 プレイヤーダメージ、カード破壊による得点
コスト MP消費、移動距離による減点

他のOpponentModelとの違い:

Model 評価方式 強さ 特徴
ComputerOpponentModel 盤面評価(BattleEvaluator) 強い スコアリング、並列探索、最適化
TutorialOpponentModel 固定パターン 非常に弱い 前方1マス移動のみ、予測可能
SandbagOpponentModel 消極的行動 弱い 移動制限、召喚条件厳しい、練習台

技術的ポイント:

  • 非同期評価エンジン(UniTask + CancellationToken)
  • 評価ルート管理(複数ターン先の計画を保持)
  • 枝刈り最適化(セルごとの最大スコア記憶、フェイント回避90%/70%)

4.5 責務分担表

責務 Facilitator Facilitatee BattleModel
バトルセットアップ - -
プレイヤー管理 - -
ネットワーク同期 - -
盤面復元 - -
操作の検証 - -
Commandの生成 - -
ローカルイベント発行 - -
FSM状態遷移 - -
ゲームロジック - -
Trigger発行 - -

4.6 設計上の利点

1. プレイモードの切り替えが容易

  • 新しいプレイモードの追加は、新しいFacilitatorを実装するだけ
  • BattleModel本体のロジックは変更不要

2. ネットワーク/ローカルの差分を吸収

  • Facilitateeレベルでは送信先(ネットワーク/ローカル)を意識しない
  • CommandRepositoryの差し替えで切り替え

3. AI対戦の実装が容易

  • Facilitateeを差し替えるだけでAI対戦に対応
  • ComputerOpponentModel, TutorialOpponentModel, SandbagOpponentModel等

4. テスタビリティの向上

  • DebugFacilitatorModelで任意の盤面を再現可能
  • 特定の状況をローカルで検証できる

5. 重要な設計パターンとその理由、まとめ

5.1 本バトルシステムで採用した設計パターン

このバトルシステムは、複雑なゲームロジックを整理・管理するために、複数の古典的な設計パターンを組み合わせて実装されています。

課題 設計パターン 解決方法
複雑なフェーズ遷移の管理
DCGのターン進行は複雑なフェーズ遷移を伴う(ターン開始→ドロー→ストラテジー→バトル→ターン終了...)
各フェーズで実行可能なアクションが異なり、フェーズ間の遷移条件が多様(自動遷移、ユーザー入力待ち、条件分岐)
State Pattern
(状態パターン)
33種類のStateクラスで各フェーズを表現し、各Stateは次のStateを返す ResolveState() メソッドを実装。連続実行が必要な場合は GetContinuousCommand() で自動遷移。

利点: フェーズごとのロジックが独立したクラスに分離され可読性・保守性が向上。新しいフェーズの追加が容易(新しいStateクラスを作るだけ)。FSM(有限状態機械)として明確に表現できる。
ネットワーク同期、リプレイ、再接続
ネットワーク対戦で盤面を同期する必要がある
リプレイ機能を実装したい
途中参加・再接続時にバトル状態を復元したい
デバッグ時に特定の盤面を再現したい
Command Pattern
(コマンドパターン)
すべてのゲーム進行を29種類のCommandで表現。すべてのCommandはイミュータブル(不変)でJSONシリアライズ可能。署名検証によるチート対策。

利点: Command履歴を再実行するだけで盤面を完全復元可能。ネットワーク越しにCommandを送るだけで同期完了。サーバーに履歴を保存すれば再接続・リプレイに対応。デバッグ時に問題のあったCommand列を再現できる。
能力の発動タイミング制御
カードの能力は特定のタイミング(Trigger)で発動する
1つのTriggerに複数の能力が反応することがある
UIも盤面変化に反応して更新する必要がある
Observer Pattern
(オブザーバーパターン)
40種類のTriggerでイベントを表現。Abilityが特定のTriggerに反応して効果を発動。ReactiveX (UniRx)でストリーム化し、UIと疎結合。

利点: 能力の発動タイミングを柔軟に制御できる。新しい能力を追加してもTrigger側の変更不要。UIへの通知も同じ仕組みで実現(疎結合)。
多様な対象選択の実装
カード能力の対象選択は多様(「敵の前列」「HP3以下」「召喚されたカード」など)
複数の条件を組み合わせたい(「敵の」「前列の」「HP3以下の」カード)
宣言的に記述したい(コードではなくデータで定義したい)
Strategy Pattern
(ストラテジーパターン)
Head(起点)8種類 + Pipe(フィルタ)18種類 + Comparator 5種類。Pipeを連鎖させることで複雑な条件を表現。すべてJSONで記述可能。

利点: 複雑な対象選択を柔軟に実装できる。新しいフィルタを追加するだけで表現力が向上。データ駆動なので、プログラムを変更せずに新しい能力を追加可能。
プレイモード固有処理の分離
オンライン対戦、ローカル対戦、AI対戦、リプレイ、チュートリアル...と多様なプレイモードがある
BattleModel自体はゲームロジックに専念したい
プレイモード固有の処理(ネットワーク同期、AI思考、履歴再生)を分離したい
Strategy Pattern + Abstract Factory Pattern 7種類のFacilitatorでプレイモードを表現。Facilitatee(参加者)で各プレイヤーの行動を抽象化。BattleModelはプレイモードを意識しない純粋なゲームロジック層。

利点: 新しいプレイモードの追加が容易。ネットワーク/ローカルの差分を吸収。AI対戦の実装が容易(Facilitateeを差し替えるだけ)。テスタビリティの向上(DebugFacilitatorModelで任意の盤面を再現可能)。
能力の多様性への対応
カード能力は非常に多様(200種類以上)
継承による実装は硬直的で、組み合わせ爆発を起こす
データ駆動で能力を定義したい
Composition over Inheritance
(合成による拡張)
Ability(能力)をEffect(効果)、Targeting(対象)、Calculator(計算)、Trigger(発動条件)のコンポーネントに分解。コンポーネントを組み合わせることで能力を表現。すべてJSONシリアライズ可能。

利点: 新しい能力を追加する際、既存コンポーネントの組み合わせで実装可能。アプリをアップデートせずに新しいカードを追加できる。能力の再利用性が高い。

5.2 DCGとしての特徴

このバトルシステムは、一般的なDCG(デジタルカードゲーム)の要素を網羅しています:

  • ✓ カードの召喚、移動、攻撃
  • ✓ トリガー型能力と継続効果
  • ✓ エリア効果とパラメータ補正
  • ✓ ターン制の複雑なフェーズ管理
  • ✓ AI対戦とネットワーク対戦への対応
  • ✓ リプレイ・観戦機能
  • ✓ 途中参加・再接続機能

これらの機能を、古典的な設計パターンを組み合わせたクリーンなアーキテクチャで実装することで、高い拡張性・保守性・テスタビリティを実現しています。


5.4 最後に:なぜこの設計を選んだのか

このバトルシステムの設計思想は、**「変更に強く、拡張しやすいアーキテクチャ」**です。

DCGは運用型ゲームであり、リリース後も継続的に新しいカードや機能が追加されます。そのため、以下の点を重視しました:

  1. 新しいカードを追加する際、既存コードへの影響を最小限にする
    → Composition over Inheritance

  2. 複雑なゲームロジックを整理し、可読性を保つ
    → State Pattern

  3. ネットワーク対戦、AI対戦、リプレイ等、多様なモードに対応する
    → Command Pattern、Strategy Pattern + Abstract Factory Pattern

  4. デバッグとテストを容易にする
    → Command履歴、Clone()、Mock差し替え

結果として、200種類以上のカード能力、7種類のプレイモード、33種類のフェーズを持つ複雑なバトルシステムを、保守可能な形で実装できました。

これらの設計判断は、ゲーム開発以外のドメイン(業務システム、Webサービス等)でも応用可能な普遍的なパターンです。本ドキュメントが、複雑なドメインロジックをクリーンに実装する際の参考になれば幸いです。

Discussion