某デッキ構築型カードゲームライクなゲームのサーバーです。
- Docker + Docker Compose
Vite dev server によるホットリロード(HMR)付き。コードを編集すると自動で反映されます。
docker compose up --buildブラウザで http://localhost:3000 にアクセス。
ビルド済み静的ファイルを nginx で配信します。
docker compose -f docker-compose.prod.yml up --buildブラウザで http://localhost:3000 にアクセス。
環境変数 BASIC_AUTH_USER と BASIC_AUTH_PASSWORD を設定すると、nginx レベルで BASIC 認証がかかります。
BASIC_AUTH_USER=sovereign BASIC_AUTH_PASSWORD=secret \
docker compose -f docker-compose.prod.yml up --build設定しない場合は認証なしで動作します。
環境変数 CARDS で data/ 内のカード定義ファイルを切り替えられます。
# デフォルト(cards.json)
docker compose up
# 別のカードセットを使う場合
CARDS=my_cards.json docker compose updocker compose down同一LAN内なら http://<あなたのIPアドレス>:3000 を共有してください。
インターネット越しにプレイする場合は ngrok や Cloudflare Tunnel などのトンネリングサービスを使います。
# ngrok の例
ngrok http 3000sovereign/
├── server.py # FastAPI サーバー(REST + WebSocket)
├── game_engine.py # ゲームロジック
├── data/
│ └── cards.json # カード定義(差し替え可能)
├── frontend/ # React UI(TypeScript + Vite + CSS Modules)
│ ├── Dockerfile # 本番用(マルチステージ: ビルド → nginx)
│ ├── Dockerfile.dev # 開発用(Vite dev server)
│ ├── nginx.conf # 本番用 nginx 設定
│ ├── vite.config.ts # Vite 設定(プロキシ等)
│ └── src/
│ ├── App.tsx # ルートコンポーネント
│ ├── types.ts # 型定義
│ ├── hooks/ # カスタムフック(WebSocket, ゲーム状態)
│ └── components/ # UI コンポーネント
├── scripts/
│ ├── bot.py # 戦略ボット(--ai フラグで Claude API 使用可)
│ ├── bridge.py # AI エージェント向け WebSocket↔HTTP 橋渡しサーバー
│ └── interactive_play.py # Claude Code がインタラクティブにプレイするクライアント
├── docker-compose.yml # 開発用(Vite HMR + ホットリロード)
├── docker-compose.prod.yml # 本番用(nginx 静的配信)
├── Dockerfile # バックエンド用
└── pyproject.toml
data/cards.json を編集することで、カードの名称・効果・見た目をカスタマイズできます。
ゲームロジック・ボット戦略・UIはカードIDをハードコードしておらず、JSON の内容に自動追従します。
{
"id": "inspector",
"name": "巡察使",
"name_en": "Inspector General",
"type": "action",
"cost": 4,
"description": "帝国全土を巡察し、広く情報を集める。+3 カードを引く, +1 購入",
"icon": "🧭",
"effects": [
{"type": "draw", "amount": 3},
{"type": "buy", "amount": 1}
]
}| type | 説明 | amount |
|---|---|---|
draw |
カードを引く | 枚数 |
action |
アクション追加 | 回数 |
buy |
購入追加 | 回数 |
coin |
コイン追加 | 枚数 |
attack_discard_to |
他プレイヤーの手札を減らす | 残す枚数 |
discard_draw |
捨てて同数引く | 0 |
gain_card_up_to |
コスト以下のカードを獲得 | 最大コスト |
trash |
手札からカードを廃棄 | 最大枚数 |
trash_and_gain |
手札1枚を廃棄し、コスト+N以下のカードを獲得 | コスト加算値 |
trash_treasure_gain_treasure |
財宝を廃棄し、コスト+N以下の財宝を手札に獲得 | コスト加算値 |
trash_copper_for_coin |
最安財宝を廃棄してコインを得る | コイン数 |
opponents_draw |
他プレイヤー全員がカードを引く | 枚数 |
gain_card_to_hand |
コスト以下のカードを手札に獲得→手札1枚をデッキトップに | 最大コスト |
topdeck_from_discard |
捨て札から1枚をデッキトップに置く(任意) | 0 |
discard_top_play_action |
デッキトップ1枚を捨て、アクションならプレイ可 | 0 |
gain_treasure_topdeck_attack_victory |
財宝をデッキトップに獲得、他者は勝利点をデッキトップに | コスト |
reveal_trash_discard_topdeck |
デッキトップN枚を公開し、各々を廃棄/捨て/戻す | 枚数 |
"reaction": "block_attack"手札にこの属性を持つカードがある場合、攻撃を自動的にブロックします。
- 各プレイヤーは初期デッキ(
starting_deckで定義)で開始 - 毎ターン5枚ドロー
- アクションフェーズ: アクションカードを使用(初期1回)
- 購入フェーズ: 財宝カードを出してコインでサプライからカードを購入
- クリーンアップ: 手札・場のカードをすべて捨て札にし、5枚ドロー
- 最高コストの勝利点カードが無くなるか、サプライの3山が空になったらゲーム終了
- 勝利点が最も多いプレイヤーの勝ち
ゲームサーバーは WebSocket で通信しますが、AI エージェントが扱いやすいよう HTTP 長ポーリングで橋渡しする仕組みを用意しています。
[ゲームサーバー :8000] <--WebSocket--> [bridge.py :8765] <--HTTP--> [AI エージェント]
- bridge.py がゲームサーバーに WebSocket 接続し、HTTP サーバーを同時に立ち上げます
- AI エージェントは
GET /waitで自分の手番が来るまでブロック待機し、状態を受け取ります POST /actionでアクションを送信します
| メソッド | エンドポイント | 説明 |
|---|---|---|
GET |
/status |
現在の接続状態・フェーズを返す(ブロックしない) |
GET |
/wait?timeout=300 |
自分の応答が必要になるまでブロック(long-poll)、状態を返す |
POST |
/action |
アクションを送信する |
{
"phase": "action",
"current_player_name": "Claude",
"context": {
"reason": "my_turn",
"phase": "action",
"description": "自分のターン(actionフェーズ)"
},
"me": {
"name": "Claude",
"hand": [
{"id": "shard", "name_en": "Shard", "type": "treasure", "cost": 0, "description": "+1 コイン"},
{"id": "edict", "name_en": "Dominion Edict", "type": "action", "cost": 5, "description": "..."}
],
"coins": 0,
"actions": 1,
"buys": 1,
"vp": 3
},
"opponents": [
{"name": "Player1", "hand_count": 5, "deck_count": 7, "vp": 0}
],
"supply": {
"sigil": {"name_en": "Sigil", "count": 28, "cost": 6, "type": "treasure"},
"realm": {"name_en": "Realm", "count": 8, "cost": 8, "type": "victory"}
},
"endgame": {
"province_remaining": 8,
"empty_piles": 0,
"is_endgame": false
},
"log": ["Claude のターンです", "..."]
}context.reason の値:
| reason | 説明 |
|---|---|
my_turn |
自分のターン(action / buy フェーズ) |
discard |
攻撃による強制捨て札(pending_action.discard_to 枚まで減らす) |
discard_draw |
任意捨て→同数引き直し |
gain |
カード獲得選択(pending_action.max_cost 以下) |
{"action": "play_action", "card_id": "edict"}
{"action": "skip_action"}
{"action": "play_all_treasures"}
{"action": "buy", "card_id": "sigil"}
{"action": "end_turn"}
{"action": "discard_selection", "card_ids": ["shard", "farmland"]}
{"action": "gain_selection", "card_id": "seal"}1. ゲームサーバーを起動
docker compose up2. ゲームを作成
GAME_ID=$(curl -s -X POST http://localhost:8000/api/games | python -c "import sys,json; print(json.load(sys.stdin)['game_id'])")
echo "Game ID: $GAME_ID"3. bridge.py を起動(コンテナ内でバックグラウンド実行)
docker compose exec -d app python scripts/bridge.py $GAME_ID --name Claude --port 87654. 相手がブラウザから参加してゲームを開始
http://localhost:3000 を開き、ゲームIDを入力して参加 → スタート。
5. Claude Code がプレイ
手番を待機して状態を表示します:
docker compose exec app env PYTHONIOENCODING=utf-8 python scripts/interactive_play.py状態が表示されたら、Claude Code がアクションを決めて送信します:
# 例: アクションをスキップ
docker compose exec app env PYTHONIOENCODING=utf-8 python scripts/interactive_play.py '{"action": "skip_action"}'
# 例: 財宝を全て出す
docker compose exec app env PYTHONIOENCODING=utf-8 python scripts/interactive_play.py '{"action": "play_all_treasures"}'
# 例: カードを購入(次の手番まで自動待機)
docker compose exec app env PYTHONIOENCODING=utf-8 python scripts/interactive_play.py '{"action": "buy", "card_id": "sigil"}'
# 例: 捨て札を選択
docker compose exec app env PYTHONIOENCODING=utf-8 python scripts/interactive_play.py '{"action": "discard_selection", "card_ids": ["shard"]}'1回のコマンドで「アクション送信 → 次の手番まで待機 → 状態表示」が完結します。
AI エージェントを使わずにボット対戦したい場合はこちら。
カードのエフェクト(effects フィールド)を動的に読んで戦略を判断するため、カード差し替えにも対応しています。
# ボット同士で対戦
GAME_ID=$(curl -s -X POST http://localhost:8000/api/games | python -c "import sys,json; print(json.load(sys.stdin)['game_id'])")
docker compose exec -d app python scripts/bot.py $GAME_ID --name BotA
docker compose exec app python scripts/bot.py $GAME_ID --name BotB --startANTHROPIC_API_KEY を設定すれば Claude API(Haiku)が各手番を判断します。
docker compose exec -e ANTHROPIC_API_KEY=sk-ant-... app \
python scripts/bot.py $GAME_ID --name Claude --ai --start/wait → 判断 → /action のループを実装するだけです。
import urllib.request, json
BRIDGE = "http://127.0.0.1:8765"
def wait():
with urllib.request.urlopen(f"{BRIDGE}/wait", timeout=310) as r:
return json.loads(r.read())
def act(body: dict):
data = json.dumps(body).encode()
req = urllib.request.Request(f"{BRIDGE}/action", data=data,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
while True:
state = wait()
if state.get("game_over"):
break
action = your_decide(state) # ← 判断ロジック
act(action)