Scene Hunter

https://github.com/yashikota/scene-hunter

GitHub

TypeScript

React

Python

Remix

AIのヒントで同じ場所を探し撮影、写真一致度で競うゲーム

こた

推しアイデア

ゲームマスターが撮影した写真からAIが特徴を抽出し、ハンターはその文章のヒントだけを頼りに同じ場所を見つけて写真を撮る。写真の一致率によってスコアが決まり、最も高いスコアを出したハンターが勝利するゲーム。

作った背景

AIを活用したゲームを作りたかったから! ハッカソンで終わり!じゃないプロダクトを作りたかったから!

推し技術

フロントエンドもバックエンドも全部Cloudflareで動かしています!

プロジェクト詳細

image

https://scene-hunter.yashikota.com で遊べます !

ゲームルール

image

ゲームマスターが撮影した写真からAIが特徴を抽出し、ハンターはその文章のヒントだけを頼りに同じ場所を見つけて写真を撮る。写真の一致率によってスコアが決まり、最も高いスコアを出したハンターが勝利するゲーム。

  1. ゲームマスターが部屋を建てる
  2. ハンター は部屋番号を入力して部屋に入る
  3. 全員が揃ったらゲームがスタートする
  4. ゲームマスターが任意の場所を撮る
  5. AIが写真から特徴を5つ文章で説明する
  6. 10秒ごとにヒントが1つずつ公開されていき、そのヒントをもとにハンターはゲームマスターがとった場所を予想し同じような写真を撮る
  7. 写真の類似度と早く撮れたかの合算でスコアを算出する
  8. これを3ラウンド行い、最もスコアが高いハンターの勝利

↑ ここまでがゲームを遊ぶ時の説明です!

↓ ここから技術的な説明です!

仕様

  • プレイ人数は3人以上
  • ラウンド数は1回以上
  • ルームには数字6桁のルームIDが付与される
  • ハンターは匿名アカウント
  • ハンターはルームIDを入力してゲームに参加する
  • 1ターンは60秒
  • ヒントは5つ
  • ヒントは最初に1つ、以後10秒ごとに時間経過で1つずつ出てくる
  • ラウンド数とゲームマスターは変更可能
  • スコアは0.00から100.00までの値を取る
  • 撮影するまでの秒数も結果に関与する
  • スコア + 残りの秒数がポイントとなる
  • 最終的に全ラウンドの合計ポイント数で勝者を決定する
  • 同点の場合は同率順位になる
  • ラウンドごとにハンターは自由に参加・退出可能
  • ハンターの名前は自由に登録/変更可能で、上限12文字
  • ネットワーク切断が発生した場合はそのラウンドは0ポイントとなる
  • 内部ID(UUIDv7)はハンターから見えずに、変更不可
  • ハンターID(英数字12文字以内)はハンターに公開され、重複はなしで、変更可能
  • ハンター名(12文字以内)は公開され重複してもよく、変更可能

ゲームの流れ

  1. ゲームマスターが任意の場所を撮影する
  2. 撮影された写真からAIが特徴を文章として書き出す
  3. ハンターはAIが出力した文章のみからゲームマスターが撮影した場所を特定して同じような写真を撮る
  4. ゲームマスターとハンターの写真を比較して一致率を算出して、最も高い一致率を出したハンターの勝利

ゲームの遊び方

本ゲームは2つの遊び方を提供する

  1. ローカル環境で集まって遊ぶパーティゲーム 1つの部屋や会場に複数人が集まって遊ぶ形態。 この場合は機器はスマートフォンになる。

  2. VR上で遊ぶリモートプレイ VRChatのような空間を共有して遊ぶ形態。 この場合は機器はHMDやPCとなる。

アーキテクチャ

システム構成

image

ディレクトリ構成

scene-hunter/ ├── api/ # API定義 │ ├── spec/ # TypeSpec │ └── openapi/ # OpenAPI ├── server/ # バックエンドサーバー │ ├── game/ # ゲームロジック (TS/Hono, Workers) │ ├── user/ # ユーザー管理・認証 (TS/Hono, Supabase) │ ├── notify/ # 通知 (TS/Hono, Workers) │ ├── image/ # 画像処理 (TS/Hono, Workers) │ └── match/ # 特徴マッチング (Python, AppRun) ├── web/ # フロントエンド (TS/RR7, Workers) └── docs/ # ドキュメント

システムアーキテクチャ

image

コンポーネント詳細

ゲーム

  • 役割: ゲームのビジネスロジック処理、AI特徴抽出
  • 技術: Workers, Hono
  • 機能: ルーム管理、ゲームフロー制御、スコア計算、AI特徴抽出

ユーザー

  • 役割: ハンター認証・ユーザー情報管理
  • 技術: Workers, Hono, D1, Supabase Auth
  • 機能: JWT認証, 匿名認証, セッション管理, ハンター情報管理

通知

  • 役割: リアルタイムイベント配信
  • 技術: Workers, Hono, WebSocket
  • 機能: ヒント配信、ゲーム状態更新通知

画像処理

  • 役割: 画像の管理と処理
  • 技術: Workers, Hono, Wasm
  • 機能: 画像アップロード、変換、保存、取得

特徴マッチング

  • 役割: 画像間の一致度計算
  • 技術: Python
  • 機能: スコア計算

データストア

  • Supabase(PostgreSQL): ハンター情報、ゲーム履歴、スコア記録
  • Durable Objects(Cloudflare): ルーム情報、接続状況、一時データ
  • R2(Cloudflare): 画像ストレージ

AI活用点

今回のハッカソンでは様々なところで沢山AIを活用した。 1つ目はCopilot, Cursor, Cline等で、活用してコードの自動生成やテストの自動生成を行なった。 バックエンドではCopilot、CursorやClineでコードをほぼ自動生成している。ただし何もないところからプロンプトだけいじっても満足のいくコードが生成できるわけではないので、ドメイン知識としてゲームのルールをまとめた docs/rule.md や ソフトウェアアーキテクチャをまとめた docs/architectrure.md を整備した。そして ユビキタス言語として docs/rule.md や OpenAPIとして、api/specの整備を行なった。これによりゲームの知識を取り入れて適切なコードを提案してくれるエージェントを作成できた。 また特にフロントエンド担当のメンバーは完全に未経験だった。前日まで環境構築もやってなかったぐらいの未経験で、当日もルーティングを知らなかったり、Gitの使い方を知らずに複数人で同じブランチで作業していたり、npmやpnpmがわからずにいたが、AIのおかげで午後にはバリバリ実装できる人材に成長しており、フロントのデザインからAPIを叩くまで成長した。 他にもGitHub上でPRを出すとCopilotとGeminiからコードレビューを受けることができ、品質の向上に寄与した。 また、アイコンの生成にChatGPTを使用したりもした。 2つ目はゲームそのものの仕組みとしてGeminiを導入して使用した。 ゲームマスターが送ってきた写真を画像解析して5つのヒントとして文章を生成する部分で使用した。ここで単にプロンプトを工夫するだけではLLMはランダム性が高い出力を行うので、不安定である。そこStructured Outputsを適用することで安定してJSONを出力させるようにした。またプロンプト自体もハンターが正解を探しやすいようなヒントを出力するように調整した。具体的には、要素のレイアウトと視覚的配置を中心に出力するようにし、天候や照明などの情報を含めないようにした。

工夫した点

インフラ

特に今回工夫した・こだわった点で、ほぼ全てのインフラをCloudflareでホスティングしている。 理由は大きく2つあり、1つ目はコストをかけずにサービスを運用できることである。サーバー、フロントエンド、データベース、オブジェクトストレージ、状態管理を無料で使うことができ、さらに全てサーバーレスなためスケールも容易で、無料枠も大きいため今回採用を決めた。 ただそれによる苦労点も多く、サーバーは10ms以内に処理を行う必要があり、画像変換も今回行っているが普通にsharpのようなライブラリを使うと時間制限オーバーになった。そこでwasmを使用して、高速に変換をおこなえるようにすることで対処した。 またフロントエンドもNext.jsがメジャーだが、Cloudflareで動くか怪しかったので今回は技術的な挑戦も含めReact Router v7を使用した。ただv6とv7とで互換性がかなりなく古い情報がインターネットには多いため、AIも古い情報を提案してくるので苦労した。さらに今回フロントエンドを担当しているのが完全に未経験なメンバーに担当してもらったためさらに苦労した。 2つ目はありきたりなインフラ構成にしたくないというわがままである。正直AWS LambdaやCloud Runで構築すれば無料枠でDockerも使え、IaCもあり、豊富な前例もあるので楽だが、いつも聞くような せっかくのハッカソンという今まで触ってこなかったものに最大限触れられるチャンスということで今回全面的に採用した。

image インフラ構成図

ドキュメント

APIドキュメントを整備するにあたって、TypeSpecによる生成を行った。 TypeSpecはTypeScriptライクにAPIを書くことができ、生のyamlやjsonを書くより遥かに書きやすく、使いやすいため採用した。 また生成AIのためにドメイン知識としてゲームのルールをまとめた docs/rule.md や ソフトウェアアーキテクチャをまとめた docs/architectrure.md を整備した。そして ユビキタス言語として docs/rule.md や OpenAPIとして、api/specの整備を行なった。これによりゲームの知識を取り入れて適切なコードを提案してくれるエージェントを作成できた。

image TypeSpecの様子

フロントエンド

Cloudflare Workersで動かすためにNext.jsを選択するのではなく、React Router v7を採用した。 ただし、Workers用に用意されている素のReact Router v7だとshadcn/uiが使えなかったり、honoが使えなかったりするので別途導入することにした。ただしこれも簡単にはいかずvite.config.tsをいじったり、プラグインを導入したり苦労したが、先端的な技術スタックを持ったWebアプリケーションにすることができた。 また、UI全体のデザインについても、おしゃれで直感的な見た目になるように細部まで配慮した。配色やフォントサイズ、余白の取り方、コンポーネントの並びなどを丁寧に調整し、見た目の印象が柔らかく親しみやすくなるよう工夫した。さらに、撮影画面に関しては、ユーザー体験を向上させるためにさまざまなインタラクションを導入した。具体的には、撮影前のカウントダウンタイマーを単なる数値表示ではなく、シークバーとして画面上部に表示することで、残り時間を視覚的に把握できるようにした。このシークバーは進捗状況に合わせてスムーズに伸縮するアニメーションを実装し、視覚的にも自然で心地よい体験となるよう工夫した。また、撮影時に表示されるヒントについても、コメントのように吹き出し形式で表示・非表示を切り替える仕組みを導入した。これにより、プレイヤーが必要に応じてヒントを見ることができ、UIとしても邪魔にならず、没入感を損なわないデザインとした。

バックエンド

各サービスをマイクロサービスアーキテクチャにすることで、AIによる支援を受けやすくした。 マイクロサービスアーキテクチャだと一つ一つのサービスが小さいのでコンテキストに収まるし、レビューも影響範囲も小さくなる デメリットであるサービス維持管理も、サービス間の技術スタックの違いもLLMが吸収してくれるからほぼ無くなりつつあるため今回採用した。 またバックエンドのラインタイムにCloudflare Workers、フレームワークにHonoを使うことによりHono RPCでフロントエンドのとの型共有が行えより安全かつスマートに開発できるようにもなった。

image バックエンドサーバーを動かしている様子

image

画像アップロード/取得/画像削除/バケットの削除を司るサーバー。初期段階ではWorkersに10msの制限があるため別途GoでCloudRunでホストしていたが、これだけCloudRunにすると管理が煩雑になるのでどうにかWorkersに移行できないかと調査とPoCの作成をした。その結果WebAssembly (WASM)だと高速に画像変換が行えると確認できたのでかなり事例が少なかったがWasmでの画像変換を行うことにした。これはLLMの提案もあまり当てにならないのでドキュメントを読み込み作成した。おかげでWorkersでも動作する画像変換サーバーを動かすことができた。

image 画像サーバーを動かしている様子

user

ユーザーの管理と認証認可を行うサーバー。認証自体はSupabase Authを利用している。これを採用した理由はゲームなのでぱっと遊べる体験が重要だが個人の特定は行いたいため匿名認証を行えるものを必須として剪定した。また同時にシェア率の高いGoogleとゲーマー向けDiscord両方をサポートし、JSのSDKが用意されているものを選択した。その結果Supabase Authとなった。25000人までは無料で認証できるが、匿名ログイン対応のためユーザーが多くなりがちなので、若干の不安があるので、将来的にはSupabaseからも脱却してD1オンリーでユーザの管理・認証認可を行えるようにしたい。

notify

ゲームに入っているユーザーの状態を更新・同期するためのリアルタイムイベント状態更新を行うサーバー。技術的にはWebSocketを使用している。ただしステートレスがうりなサーバーレスでWebSocketを使うのは本来想定された用途ではないので簡単にはいかなかった。そこでWorkersで使える強整合性をもつストレージであるDurable Objectsを導入した。ちなみに4月に無料化されたばかりのプロダクトである。これによりサーバーレスでありながら状態を持つことができ、WebSocketも運用することができた。さらにこれをサーバーレスで管理することでスケーリングの問題も保守の問題も解決できるので今回気合を入れて実装した部分である。

image 通知サーバーを動かしている様子

game

ゲームサーバーでは、各ゲームルームの状態を管理するために、RoomObjectというDurable Objectを導入している。ルームの初期化、ハンターの参加・退出、ゲームマスターの変更、ハンター名の更新、ラウンドの開始・終了、ルーム情報やランキングの取得といった内部処理をすべてこのオブジェクト内で扱っている。 APIのルーティングにはHonoを使用しており、/rooms や /rooms/:room_id/join、などの外部向けエンドポイントを定義している。これらのリクエストは、対応するRoomObject内のハンドラ関数に転送され、実際のロジックが実行される。 RoomObject内部では、ルームへの参加、ラウンドの開始、写真の投稿、リーダーボードの取得など、各ゲームアクションに対応するハンドラ関数を実装している。これにより、ゲームの状態をDurable Objectのストレージに保持しながら進行できるようにしている。 ハンターはラウンド中に写真を投稿できるようになっており、handleRoundPhotoハンドラでは、外部の画像比較APIを用いてスコアを算出する。この際、残り時間も加味して最終的なスコアが決定され、投稿データと共に保存される。また、/uploadルートではGoogle GenAI APIと連携し、画像からヒントを生成する機能も実装している。 image 画像からヒントを生成している様子 さらに、ルームやラウンドの状態はDurable Object内で一元的に管理されており、notifyRoomEventというユーティリティ関数を使って、ゲームイベントを外部の通知APIに送信する仕組みも整えている。

match

2つの画像間の類似度を計算するAPIを提供するサーバー。FastAPIを使用して構築されており、画像のベクトル化と距離計算にはimgsimライブラリを使用している。

image 画像類似度推定している様子

imgsimライブラリを採用した理由は、他のアルゴリズム(ORB特徴量抽出やSSIMによる構造的類似度計算)よりも高精度かつ高速であったためである。

アルゴリズムの概要

  1. クライアントから送信された2つの画像URLを受け取る。
  2. 各画像を指定されたURLからダウンロードし、OpenCVを使用してデコードする。
  3. 画像を256x256にリサイズ。
  4. imgsim.Vectorizerを使用して画像をベクトル化。
  5. ベクトル間の距離をimgsim.distanceで計算する。
  6. 距離に基づいてカスタム減衰関数を適用し、類似度スコアを計算する。

類似度スコアの計算方法

類似度スコアは以下の手順で計算される:

  1. 距離計算
    imgsim.distanceを使用して、2つの画像ベクトル間の距離を計算する。

  2. カスタム減衰関数
    距離に基づいて以下の減衰関数を適用:

    def custom_decay(x): n = 2.17 # 減衰の指数 k = 0.00241 # 減衰のスケール係数 return math.exp(-k * (x ** n))

    この関数は、距離が大きいほどスコアが急激に減少するように設計されている。

  3. ** スコア計算 ** 減衰関数の結果に100を掛けて、最終的な類似度スコアを計算:

    score = 100 * custom_decay(dist)
  4. ** スコアの丸め ** スコアは小数点以下4桁に丸められ、クライアントに返される。

パラメータ調整

減衰関数のパラメータnとkは、画像の特性や用途に応じて調整可能となっている。これらの値を変更することで、スコアの感度や減衰の挙動をカスタマイズできる。

デプロイにはさくらのクラウドのAppRunを使用している。 使用した理由としては無料でDockerのサーバーレスアプリケーションをデプロイできる点が魅力点だった。また国産クラウドサービスの普及を後押ししたい点があり、採用した。

展望

今後はVRで遊べるようなゲームにしたいと考えている。 VRにすると現実世界の欠点であるゲームマスターが撮影している場面を目撃されたり、人や車等の移動物体が含まれることにより結果が変わってしまうといったことがなくなるため、より一層楽しく遊べるゲームになると思う。
VRChatのような空間を共有して遊ぶ形態で、この場合は機器はHMDやPCとなる。

また、サービスを継続的に運用することで末長く愛されるゲームにしていきたいと思う。

こた

@kota