魚へん漢字をテーマにしたリアルタイムマルチプレイヤークイズゲームです。プレイヤーは漢字と読み(よみ)を提示され、正しく識別する必要があります。リアルタイム同期、ランキングシステム、時間制限付きラウンドなどの機能を備えています。
- リアルタイム同期: Server-Sent Events (SSE) によるライブゲーム状態の更新
- マルチプレイヤー対応: 複数のプレイヤーが同時に参加可能
- ランキングシステム: スコアを登録して競い合うことができます
- 時間制限ラウンド: 30 秒ごとに問題が自動的にリセットされます
- コンボシステム: 連続正解でコンボポイントを獲得
- NFC 対応: モバイルクライアントで NFC タグから漢字情報を読み取り、回答として送信可能
- モバイル & デスクトップ: レスポンシブデザインで両方のインターフェースに対応
- フレームワーク: Next.js 16.0.3 (App Router)
- UI ライブラリ: React 19.2.0
- 言語: TypeScript 5
- スタイリング: Tailwind CSS 4, Sass/SCSS, DaisyUI 5.5.5
- 状態管理: React hooks (useState, useEffect)
- URL 状態: nuqs 2.8.0
- パッケージマネージャー: pnpm
- リンター/フォーマッター: Biome 2.2.0, dprint
- 言語: Rust (Edition 2024)
- Web フレームワーク: Axum 0.8.7
- 非同期ランタイム: Tokio 1.47.2
- シリアライゼーション: serde, serde_json
- リアルタイム通信: Server-Sent Events (SSE) via async-stream
- HTTP ミドルウェア: tower-http (CORS, tracing)
- ロギング: tracing, tracing-subscriber
- ユーティリティ: dotenvy, uuid, rand, thiserror
osakana/
├── client/ # Next.js フロントエンド
│ ├── app/
│ │ ├── mobile/ # モバイルインターフェース
│ │ │ ├── _components/ # モバイルコンポーネント
│ │ │ │ ├── combo.tsx # コンボ表示
│ │ │ │ └── Keypad.tsx # 入力キーパッド
│ │ │ ├── actions.ts # サーバーアクション
│ │ │ ├── page.tsx # モバイルメインページ
│ │ │ ├── register-user/ # ユーザー登録
│ │ │ ├── register-rank/ # ランキング登録
│ │ │ └── request/ # 回答送信
│ │ ├── screen/ # デスクトップゲーム画面
│ │ │ ├── _components/ # 画面コンポーネント
│ │ │ │ ├── AllClear/ # オールクリアアニメーション
│ │ │ │ ├── BubbleContainer/ # バブルエフェクト
│ │ │ │ ├── Kanji/ # 漢字表示
│ │ │ │ ├── KanjiSplitter/ # 漢字分割エフェクト
│ │ │ │ ├── OceanBackground/ # 海のアニメーション
│ │ │ │ ├── QuestionCard/ # 問題カードコンポーネント
│ │ │ │ └── TimeGauge/ # タイマー表示
│ │ │ ├── page.tsx # 画面メインページ
│ │ │ └── styles/ # 画面スタイル
│ │ ├── layout.tsx # ルートレイアウト
│ │ └── page.tsx # ホームページ
│ ├── public/ # 静的アセット
│ ├── biome.json # Biome 設定
│ ├── dprint.json # dprint 設定
│ ├── next.config.ts # Next.js 設定
│ ├── package.json # 依存関係
│ └── tsconfig.json # TypeScript 設定
│
└── server/ # Rust バックエンド
├── src/
│ ├── domain/ # ドメインロジック (DDD)
│ │ ├── answer.rs # 回答処理
│ │ ├── questions.rs # 問題取得
│ │ ├── ranking.rs # ランキング登録
│ │ └── user.rs # ユーザー作成
│ ├── kanji.rs # 漢字データ読み込み
│ ├── main.rs # アプリケーションエントリーポイント
│ ├── questions.rs # 問題タイプ & ロジック
│ ├── sse_event.rs # SSE イベントタイプ
│ └── user.rs # ユーザータイプ
├── data.csv # 漢字データソース
├── Cargo.toml # Rust 依存関係
├── compose.yaml # Docker Compose 設定
└── Dockerfile # Docker ビルド設定
- 開発環境:
http://localhost:8000 - 本番環境:
FRONTEND_URL環境変数で設定
- メソッド:
POST - パス:
/user - リクエストボディ: なし
- レスポンス:
{ "user_id": "uuid-string" } - 説明: 新しいユーザーを作成し、一意のユーザー ID を返します
- メソッド:
GET - パス:
/questions/current - レスポンス:
{ "current_questions": [ { "index": 0, "kanji": { "unicode": "9b2c", "yomi": "まぐろ", "kanji": "鮪", "difficulty": "Easy" }, "is_solved": false } ] } - 説明: 現在の問題セットを取得します
- メソッド:
POST - パス:
/answer - リクエストボディ:
{ "user_id": "uuid-string", "question_index": 0, "kanji_unicode": "9b2c" } - レスポンス:
{ "is_correct": true, "combo": 5 } - 説明: 回答を送信し、正解かどうかとコンボ数を返します
- ステータスコード:
200 OK: 成功404 NOT_FOUND: ユーザーまたは問題が見つかりません
- メソッド:
POST - パス:
/ranking - リクエストボディ:
{ "user_id": "uuid-string", "username": "PlayerName" } - レスポンス:
200 OK(ボディなし) - 説明: ユーザーのスコアをランキングシステムに登録します
- ステータスコード:
200 OK: 成功404 NOT_FOUND: ユーザーが見つかりません
- メソッド:
GET - パス:
/ranking - レスポンス:
[ { "id": "uuid-string", "username": "PlayerName", "combo": 10 } ] - 説明: ランキング一覧を取得します
-
メソッド:
GET -
パス:
/sse -
レスポンス: イベントストリーム
-
イベントタイプ:
// 問題リロードイベント { "ReloadQuestions": { "questions": [...] } } // 時間更新イベント { "RemainingTimePercentage": { "percentage": 75.5 } } // 回答イベント { "Answer": { "index": 0, "is_correct": true } }
-
説明: ゲーム状態のリアルタイム更新のためのイベントストリーム
-
更新頻度: 時間パーセンテージは 500ms ごとに更新
- 問題数: 1 ラウンドあたり 10 問
- 時間制限: 1 ラウンド 30 秒
- リセット: 時間経過後に自動リセット
- 選択: 漢字データベースからランダム選択
- 難易度: 3 段階 (Easy, Medium, Hard)
- コンボ: 正解するたびに増加
- リセット: 不正解でコンボがリセット(推測)
- ランキング: コンボ数に基づく
- 時間更新: SSE 経由で 500ms ごと
- 回答イベント: 検証後すぐにブロードキャスト
- 問題リロード: ラウンドリセット時にブロードキャスト
- NFC タグ読み取り: モバイルデバイスで NFC タグを読み取ることで回答を送信
- データ形式: NFC タグには回答ページへの URL が記録されています
- URL 形式:
/mobile/request?unicode=<unicode値> - unicode 値は 16 進数形式(例: "9b2c")で指定します
- 例:
/mobile/request?unicode=9b2c
- URL 形式:
- 動作フロー:
- ユーザーが問題番号をキーパッドで選択(1-10)
- NFC タグをデバイスに近づける
- タグに記録された URL が開かれ、クエリパラメータから unicode 情報を取得
- 自動的にサーバーに回答として送信
- 正解/不正解の結果とコンボ数を表示
- 実装: URL のクエリパラメータ(
unicode)をパースして回答処理を行います - 利点: 標準的な NFC URL 形式を使用するため、特別な API やブラウザ対応が不要です
FRONTEND_URL(必須): CORS 設定用のフロントエンド URL- 例:
https://osakana.vercel.app
- 例:
CLOUDFLARED_TOKEN(オプション): Docker Compose 用の Cloudflare Tunnel トークン
NEXT_PUBLIC_BACKEND_URL: バックエンド API URL- 例:
http://localhost:8000(開発環境) または本番 URL
- 例:
cd client
pnpm dev # 開発サーバー起動 (ポート3000)
pnpm build # 本番用ビルド
pnpm start # 本番サーバー起動
pnpm lint # リンター実行
pnpm format # コードフォーマットcd server
cargo run # ビルド & 実行 (ポート8000)
cargo check # ビルドせずにチェック
cargo build # ビルドのみ
cargo fmt # コードフォーマット
cargo clippy # リンター実行
cargo test # テスト実行cd server
docker compose up # サービス起動
docker compose down # サービス停止
docker compose logs # ログ表示- インデント: 2 スペース
- 行幅: 100 文字
- コンポーネント: PascalCase
- 関数/変数: camelCase
- ファイル: コンポーネントは PascalCase、ユーティリティは kebab-case
- スタイリング: CSS Modules (
.module.scss) + Tailwind CSS
- フォーマット:
cargo fmt(rustfmt) - モジュール: snake_case
- 型/構造体: PascalCase
- 関数: snake_case
- エラーハンドリング:
Result<T, E>とthiserrorを使用
-
ゲーム状態管理
- 共有可変状態に
Arc<Mutex<GameState>>を使用 - バックグラウンドタスクが 500ms ごとにタイマーを更新
- SSE イベントは Tokio ブロードキャストチャネル経由でブロードキャスト
- 共有可変状態に
-
漢字データ読み込み
- 起動時に
data.csvから読み込み - グローバルアクセス用に
OnceLockに保存 - CSV 形式:
[index, yomi, level, ?, unicode]
- 起動時に
-
問題リセットロジック
- データベースから 10 個の漢字をランダム選択
- タイマーを 30 秒にリセット
- すべての SSE クライアントにリロードイベントをブロードキャスト
-
回答検証
- 送信された
kanji_unicodeと問題の漢字 unicode を比較 - 正解時にユーザーのコンボを更新
- リアルタイム UI 更新のための回答イベントをブロードキャスト
- 送信された
-
状態管理
- ローカル状態に React hooks (
useState,useEffect) を使用 - ユーザー ID と問題インデックスの永続化に Cookie を使用
- エラーハンドリングとコンボ表示に URL 検索パラメータを使用
- ローカル状態に React hooks (
-
モバイルインターフェース
- API 呼び出しにサーバーアクションを使用
- Cookie ベースの認証
- キーパッドコンポーネントによるフォームベースの入力
- NFC 機能: NFC タグに記録された URL のクエリパラメータから漢字情報を読み取り
- NFC タグには回答ページへの URL が記録されています(例:
/mobile/request?unicode=9b2c) - タグを読み取ると、URL のクエリパラメータから unicode 情報を取得し、自動的に回答として送信されます
- 物理的な NFC タグと連携することで、よりインタラクティブなゲーム体験を提供します
- NFC タグには回答ページへの URL が記録されています(例:
-
デスクトップインターフェース (画面)
- クライアント側状態管理
- タイマーベースの問題リロード
- アニメーション状態: "idle", "entering", "exiting"
-
エラーハンドリング
- 失敗時にエラーパラメータ付きでリダイレクト
- API 呼び出し前の Cookie 検証
- サーバーアクションエラーバウンダリ
-
CORS エラー
FRONTEND_URLが実際のフロントエンド URL と一致していることを確認main.rsの CORS 設定を確認
-
SSE 接続の問題
- SSE エンドポイントがアクセス可能であることを確認
- ブラウザコンソールで接続エラーを確認
- サーバーが実行中でアクセス可能であることを確認
-
ユーザーが見つからないエラー
- user_id Cookie が設定されていることを確認
- ユーザー作成エンドポイントのレスポンスを確認
- ゲーム状態にユーザーが存在することを確認
-
問題が見つからないエラー
- question_index が有効であることを確認 (0-9)
- 現在の問題エンドポイントを確認
- CSV から問題が読み込まれていることを確認
-
NFC 読み取りエラー
- NFC タグに正しい URL 形式が記録されていることを確認(
/mobile/request?unicode=<unicode値>) - URL のクエリパラメータに
unicodeが含まれていることを確認 - unicode 値が 16 進数形式(例: "9b2c")で記録されていることを確認
- デバイスの NFC 機能が有効になっていることを確認
- NFC タグを読み取った際に正しい URL に遷移していることを確認
- NFC タグに正しい URL 形式が記録されていることを確認(
- Git からの自動デプロイ
- Vercel ダッシュボード経由の環境変数
- ビルドコマンド:
pnpm build
Dockerfileから Docker イメージをビルド- パブリックアクセス用の Cloudflare Tunnel
.envファイルまたは Docker Compose 経由の環境変数
リポジトリの LICENSE ファイルを参照してください。
最終更新: 現在のコードベース状態に基づく メンテナー: プロジェクトチーム