○×ゲームを進化させた、戦略ボードゲーム
A strategic board game evolved from Tic-Tac-Toe
機能 • ルール • 技術スタック • アーキテクチャ • セットアップ
| プラットフォーム | リポジトリ | 技術スタック |
|---|---|---|
| Web 版(本リポジトリ) | CircleTactics |
React 18 + Vite、AWS Amplify + Lambda + DynamoDB |
| Android 版 | circle-tactics-android |
React Native 0.81 + Expo SDK 54 |
| iOS 版 | circle-tactics-ios |
SwiftUI ネイティブ(iOS 16+) |
オンライン対戦バックエンド(AWS Lambda + DynamoDB)は本リポジトリの infra/ が管理し、3 プラットフォームで共有。
CircleTactics は、古典的な○×ゲーム(Tic-Tac-Toe)に革新的なルールを追加した戦略ボードゲームです。
4×4マス・3サイズのコマ (S / M / L)・2種類の勝利条件・最大4人対戦 で、より奥深い駆け引きと戦略性を実現しました。同一端末で 1〜4 人の対戦に加え、オンライン対戦(ルームコード or QRで合流)もサポート。空席は AI が自動で担当します。
CircleTactics is a strategic board game that adds modern rules to the classic Tic-Tac-Toe. Up to 4 players can play locally on the same device or online via a 6-digit room code / QR. A 4×4 board, 3 piece sizes, 2 win conditions, and AI-filled empty seats keep every match dynamic.
- 🎮 1〜4人マルチプレイ (ローカル + オンライン) — タイトル画面でモード選択。空席は AI が自動補充
- 🌐 オンライン対戦 — ホストが 6 桁ルームコードを発行、参加者はコード入力 / QR スキャン /
?room=XXXXXX共有 URL から合流 - 🚀 途中参加 & 観戦キュー — ゲーム開始前・開始後を問わずルームに入室可能。4席が埋まっていれば自動的に観戦キューに並び、空きが出次第プレイヤーに昇格
- 🔄 観戦者の自動昇格 — 切断プレイヤーの席が AI になると、待機中の観戦者が優先的に昇格(AI より観戦者を優先)。現在ターン中の席が空いた場合は、昇格した観戦者がそのままターンを引き継ぐ
- 🎲 先攻決定ルーレット (全員同期) — スタート時・Play Again 時に色がぐるぐる回って先攻を決めるアニメーション。ホストだけでなく非ホスト・観戦者全員に表示
- 🧠 戦略的 AI — 「勝てる手 → 相手をブロック → 有効手」の優先度で思考。サイズ選択 → 配置の 2 段階で動き、人間らしいテンポ
- ⏱ 30秒ターン制限 (オンラインのみ) — 残り秒数表示付き、超過すると駒を置かずに次のプレイヤーへ
- 🏆 2種類の勝利条件 — マス内勝利 & 4マス1列勝利
- ✨ 勝利マスのハイライト — 勝利条件を満たしたセルが白〜黄色のグロウで発光し、決め手が一目で分かる
- 🔁 進行中ゲーム自動復帰 — タブを閉じても 6 時間以内ならルームに復帰可能。元いた席が AI のままなら同じ色に戻り、他プレイヤーが取っていれば別の AI 席に移る
- 🤖 AI takeover — オンラインで切断したプレイヤーは 30 秒後に AI が引き継ぎ。観戦者がいる場合は AI ではなく観戦者が優先昇格
- 🎨 3サイズのコマ — S / M / L(サイズ差を大きく取り、一目で区別可能)
- 🤝 引き分け判定 — 全 16×3 スロットが埋まると自動的にドローでゲーム終了
- 💡 有効マスハイライト — 現在のターンプレイヤーが置けるマスのみ枠がパルスで光る
- ⏭ ターン自動スキップ — 手札不足や置き場所がない場合は「SKIP」アニメーションを表示してターンを自動でパス
- 🎵 BGM — Web Audio API で合成した著作権フリーの 8 小節ループ BGM(Cメジャーペンタトニック、BPM 98)。デフォルトは ON
- 🔊 効果音 (SE) — コマ置き(ポヨン)・選択・先攻発表・ルーレット・スキップ・勝利・引き分け、加えて 全ボタン共通のタップ音 までアクションごとに効果音。デフォルトは ON
- ⚙️ メニューから即時切替 — 常時表示の「メニュー」ボタン(右上)から BGM / SE を ON/OFF トグル。設定は localStorage に保存
- 🌐 15言語対応 — 日本語・英語・中国語(簡体/繁体)・韓国語・スペイン語・フランス語・ドイツ語・イタリア語・ポルトガル語・ロシア語・アラビア語・ヒンディー語・トルコ語・インドネシア語。左上の言語ボタンで即時切替
- 📋 常時表示メニュー — 全画面の右上に「メニュー」ボタンを固定表示。タイトルへ戻る・新しいゲーム(ローカル中のみ)・BGM / SE 設定を一箇所に集約。観戦待機画面なども離脱動作はメニュー経由に統一されており、画面ごとの導線分岐がない
- 👥 入室人数バッジ — ヘッダーのルームコード横に対戦者+観戦者の合計人数をリアルタイム表示
- 📊 手札サマリ — 画面上部に全プレイヤーの残り駒数を一覧表示。
[You]/[AI]バッジで役割が一目瞭然 - 🎯 手札ハイライトは自分のターンのみ — 黄色のアクティブ枠・カウントダウン警告は自分のターンにのみ表示。他プレイヤーのターン中は残り秒数を小さく表示
- 📱 レスポンシブデザイン — PC・タブレット・スマートフォン対応。盤面は手札表・手札ボードと常に同じ横幅を維持。ブラウザのナビゲーションバーが表示されてもスクロールで対応し盤面が縮まない
- 🍞 トースト通知 + 押下フィードバック — API エラー等をトーストで明示。全ボタンに統一の押下感
- 🔄 Play Again / Menu — 終了後にホスト権限不要で誰でも再開、またはタイトルへ戻る
- 🎬 タイトル画面デモ盤面 — タイトル下部に AI 同士が自動対戦するデモ盤面を表示。ゲームのイメージを直感的に伝える
各プレイヤーは固定の色を持ち、S / M / L の3サイズの駒をそれぞれ4個ずつ(計12個)所持します。
| プレイヤー | 役割 (例: 1人モード) | コマ数 |
|---|---|---|
| 🔴 RED | あなた | 各サイズ4個 = 12個 |
| 🔵 BLUE | AI | 各サイズ4個 = 12個 |
| 🟡 YELLOW | AI | 各サイズ4個 = 12個 |
| 🟢 GREEN | AI | 各サイズ4個 = 12個 |
プレイヤー数を増やすと色順 (RED → BLUE → YELLOW → GREEN) でプレイヤーが担当します。
ターン順はゲーム開始時に毎回ランダムでシャッフルされます。
L ●●● (大)
M ●● (中)
S ● (小)
① Cell Win(マス内勝利)
1つのマス内に、同じプレイヤーの3サイズ全て (S / M / L) が揃う
② Board Win(ボード勝利)
盤面全体で、4つのマスが縦・横・斜めのいずれかに一列に揃う
1. タイトルで Local Play / Online Play を選択
2. ローカル: プレイヤー数 (1〜4) を選択
オンライン: Create Room でコード発行 → 共有 → Start
3. ルーレットでランダムに先攻プレイヤーが決定
4. ターンプレイヤーが S / M / L を選び、4×4 のマスをタップして配置
5. シャッフルされたターン順で次のプレイヤーへ
6. 勝利条件を満たしたプレイヤーの勝ち!決め手のマスが光って表示
| カテゴリ | 技術 |
|---|---|
| フレームワーク | React 18 |
| 言語 | TypeScript 5 |
| ビルドツール | Vite 7 |
| スタイリング | CSS Modules + CSS Custom Properties |
| 状態管理 (ローカル) | React useReducer |
| 状態管理 (オンライン) | サーバ集中 + ポーリング (画面別 1.5〜3 秒間隔) |
| QR 生成 | qrcode |
| デプロイ | AWS Amplify Hosting |
| カテゴリ | 技術 |
|---|---|
| API | Amazon API Gateway HTTP API |
| コンピュート | AWS Lambda (Node.js 22) |
| データベース | DynamoDB (TTL によるスライディング自動削除) |
| IaC | AWS CDK (TypeScript) |
| 共有ロジック | tsconfig paths でフロントエンドの src/logic を直接 import |
Browser (Vite SPA, Amplify Hosting)
│
│ HTTPS/JSON (ポーリング 1.5〜3 秒 + アクション)
▼
API Gateway (HTTP API) → Lambda (gameHandler) → DynamoDB (TTL 2h)
│
└─ 共有純粋関数 (winConditions, ai, seating)
を src/logic から esbuild バンドルで取込
ポーリング間隔は画面ごとに調整: ロビー待機は 1.5 秒・ホスト画面は 3 秒・対戦/観戦中は 2 秒(FINISHED 後は 5 秒に減速)。
- サーバが 唯一の真実 (server authoritative) : 勝利判定 / ターン進行 / AI 着手をすべて Lambda で確定
- 各 AI 着手は「サイズ選択 → 配置」の 2 ステップに分かれ、ポーリングごとに 1 ステップ進行 → ローカル版と同じ "考えてから置く" テンポ
- 切断検出・AI 交代・観戦者昇格・host 移譲・ターンタイムアウトはすべて
getGameポーリングの副作用で実行されるため、追加スケジューラ不要 - 観戦キュー:
waitQueue[]を DynamoDB に保持。AI 席が発生した瞬間に先頭の観戦者を昇格(WAITING 中は空きスロットへ追加、PLAYING 中は AI 席を引き継ぎ) - 全員同期ルーレット: クライアントは
startedAtの変化を監視し、ラウンド開始 / Play Again のたびに先攻決定ルーレットをリプレイ。ホスト・非ホスト・観戦者で表示が一致する
.
├── src/
│ ├── App.tsx # モード分岐 (Title / Local / Online lobby / OnlineGame / Spectator)
│ ├── components/
│ │ ├── TitleScreen.tsx # Local / Online + プレイヤー数選択
│ │ ├── Game.tsx # ローカル対戦
│ │ ├── OnlineGame.tsx # オンライン対戦 (ポーリング + 楽観的更新)
│ │ ├── SpectatorView.tsx # 観戦・待機キュー画面
│ │ ├── Board.tsx # 4×4 盤面
│ │ ├── Cell.tsx # セル (勝利ハイライト + :active 押下感)
│ │ ├── Piece.tsx # 駒 (3 サイズ + 配置アニメ)
│ │ ├── PlayerHand.tsx # 現ターンの手札 (S / M / L 選択)
│ │ ├── HandsSummary.tsx # 全員の残り駒数表 + [You] / [AI] バッジ
│ │ ├── Toast.tsx # 統一トースト通知
│ │ └── Lobby/
│ │ ├── LobbyScreen.tsx # Create / Join 分岐
│ │ ├── HostScreen.tsx # ルームコード + QR + Start
│ │ ├── JoinScreen.tsx # コード入力 (URL からの自動入力対応)
│ │ └── WaitingRoom.tsx # ゲスト待機画面 (席確定・ゲーム開始待ち)
│ ├── logic/
│ │ ├── gameReducer.ts # ローカル状態遷移
│ │ ├── seating.ts # 人数→人間プレイヤー割当 + ターン順シャッフル
│ │ ├── winConditions.ts # 勝利判定 (種別 + 達成セル位置)
│ │ └── ai.ts # AI 思考エンジン
│ ├── online/
│ │ ├── api.ts # fetch クライアント + エラー翻訳
│ │ ├── types.ts # GameSession 型
│ │ ├── clientId.ts # localStorage UUID
│ │ ├── activeGame.ts # 進行中ゲームの保存・復帰 (gameId / roomCode / color)
│ │ ├── usePolling.ts # ポーリング (1.5〜3 秒 / visibilitychange / FINISHED 減速)
│ │ └── useHeartbeat.ts # 10秒毎 heartbeat
│ └── types/index.ts # 共有型 (Player, GameState, WinInfo, ...)
└── infra/ # AWS CDK (Lambda + DynamoDB + HTTP API)
├── bin/circletactics-infra.ts
├── lib/circletactics-stack.ts
└── lambda/
├── gameHandler.ts # ルーター + 観戦キュー昇格ロジック
└── domain/
├── session.ts # GameSession ドメイン型 + DDB 変換
├── moves.ts # selectSize / placePiece
├── aiTakeover.ts # AI 連鎖・ホスト移譲・ターンタイムアウト
└── roomCode.ts # 6 桁コード生成 + 衝突チェック
- Node.js 18.0.0 以上
- npm
- (バックエンドを動かす場合) AWS CLI + AWS CDK
# 1. リポジトリをクローン
git clone https://github.com/Yuuga2001/CircleTactics.git
cd CircleTactics
# 2. 依存関係をインストール
npm install
# 3. 環境変数を設定 (オンライン対戦を使う場合)
echo 'VITE_API_BASE_URL=https://hsqetnkx8k.execute-api.ap-northeast-1.amazonaws.com' > .env
# 4. 開発サーバを起動
npm run dev # → http://localhost:5173オンライン対戦機能を使わずローカル対戦のみ動かすなら
.envは不要です。
npm run test # unit / integration テスト (Vitest, ~470件)
npm run test:coverage # カバレッジレポート生成
npm run test:e2e # E2E テスト (Playwright, 4ファイル) ※開発サーバが必要テストはテストピラミッド構成で、src/test/ 以下に配置されています。
src/test/
├── unit/ # ゲームロジック・API・オーディオなど純粋関数
├── integration/ # React コンポーネント・フック (Testing Library)
└── e2e/ # ブラウザ全体の E2E フロー (Playwright)
cd infra
npm install
npm run synth # CloudFormation テンプレートの生成
npm run deploy:dev # CircleTacticsStack-Dev をデプロイ
# 出力された ApiUrl をフロント側の .env / Amplify env に設定| コマンド | 説明 |
|---|---|
npm run dev |
開発サーバを起動 |
npm run build |
tsc 型チェック (tsconfig.build.json、テストは除外) + Vite 本番ビルド |
npm run preview |
ビルド結果をプレビュー |
npm run lint |
ESLint でコードをチェック |
npm run test |
unit / integration テストを実行 (Vitest) |
npm run test:watch |
テストをウォッチモードで実行 |
npm run test:coverage |
カバレッジレポートを生成 |
npm run test:e2e |
E2E テストを実行 (Playwright) |
npm run test:e2e:ui |
Playwright UI モードで実行 |
cd infra && npm run synth |
CDK シンセサイズ |
cd infra && npm run deploy:dev |
Dev スタックのデプロイ |
cd infra && npm run deploy:prod |
Prod スタックのデプロイ |
Made with ❤️ using React + TypeScript + Vite + AWS CDK
