推しアイデア
30分睡眠 「ショートスリーパー」の商標保有者
30分睡眠 「ショートスリーパー」の商標保有者
時:ホットな男、堀大輔 楽器:オカリナ 伝説: 配信で伝説を作った リメイク -> 堀大輔をネムダのラスボスとしてリメイク 楽器 -> オカリナで眠らせる
無駄なテスト量... ちゃんと寝た方がいいんじゃないですかって言うのやめてもらっていいですか😡
3人の大輔たちを操作し、ショートスリーパー堀大輔の魔の手から世界を救う物語。


ハックツハッカソン〜モサカップ〜2026〜にて作成 テーマ:「伝説・時・リメイク・楽器」
時のひと:堀大輔を救いたい!
ゼルダの伝雑の3勇者のやつを参考に三猿(見ざる・聞かざる・言わざる)を協力ゲームに落とし込みたかった。
よくある協力ゲームは「アタッカー / ヒーラー / タンク」のように 強みで役割を分ける。 本作は逆で、弱み(制約)で役割を分ける。
制約を押し付けるだけだと理不尽なので、それぞれに制約と表裏一体の強みを持たせた。
Pay大輔(言わざる) 「何をすべきか」を知っている ── でも言葉で説明できない ↓ オドルノDaisuke(見ざる) 「そこへ行って何とかする」 ── でも自分では状況が見えない ↓ オラ大輔(せざる) 「普通ではできない方法で実行する」 ── でも普通の操作ができない
答えを一番よく知っている人が、それを一番説明できない。
この構造が、そのまま「3人でやる必然性」になっている。
3人のプレイヤーがボス「堀大輔(睡魔)」の1つのアリーナで戦う、1マップ完結型の3Dボスアクション。
勝利条件はボスのHPを0にすること。
最終形はいきなり3人分割だが、最初から情報と操作を3人へ割ると、ゲームシステム自体が成立しているか検証できない。
そこで Phase 1 では、あえて
1人で操作して普通に成立するボス戦
を先に作り切った。固定カメラ・左回避 / 右回避 / ガード / 攻撃の4操作のみ。
その上で Phase 2 で、この1人が使っていた 視覚情報・聴覚情報・判断・操作 を3人へ分解した。 「面白いボス戦」であることを先に確定させてから制約を乗せる、という順序にした。
「せざる」の力により、Keyboard / Mouse / Controller を一切使えない。
両手を画面前に上げ、Webカメラで追跡してゲーム操作へ変換する。
memo:ここ追記
「言わざる」の力により、本人が何を喋っても他プレイヤーには「わっしょーい」としか聞こえない。
Pay本人: 「右の祭壇!右!右!」 他プレイヤー: 「わっしょーい!わっしょーい!!」
LiveKit(WebRTC)でボイスチャットを実装した上で、Pay大輔だけ生音声をそもそも送信しない。
代わりに、ブラウザ内で声を解析して
intensity(声量)durationMs(長さ)だけを抽出し、reliable data topic nelda.wasshoi.v1 へ送る。受信側がその強さと長さに応じて「わっしょーい」を合成再生する。
{ "type": "...", "intensity": 0.8, "durationMs": 620 }
生音声・PCM・文字起こし・録音URLは一切送らない。 実装は以下で構成した。
| モジュール | 役割 |
|---|---|
voice-activity-detector.ts | 発話区間の検出 |
adaptive-noise-gate.ts | 環境ノイズに追従する可変ゲート |
system-wasshoi-engine.ts | 声量・長さ →「わっしょーい」生成 |
権限もサーバー側で固定した。トークン発行時に Pay へは canPublishSources: [DATA] のみ、Odoruno / Ora へ [MICROPHONE, DATA] を付与する。
クライアントから渡された role を信用しない設計にしてあるので、Pay大輔が細工して生声を流すことはできない。
情報量は最も多いのに、伝達能力は最も低い。 だから Ping・ジェスチャー・声量の強弱を駆使することになる。
マイクから吹いたオカリナの音程を判定し、ド・ミ・ソをゲームコマンドへ対応付けている。
マイク → pitchy(ピッチ検出) → NoteClassifier → NoteStabilizer → OcarinaCommand
pitchy で基本周波数を取り、12音階へ分類し、ゆらぎを安定化させてからコマンドにする。
ここで意識したのは 「対象外の音を丸めない」 こと。
/** * 音声基盤は12音階すべてを返す。ここは「ゲームが使う音だけ拾う」層で、 * 対象外の音を C / E / G へ丸めない。D を無理に DO や MI にすると、 * 吹き間違いが正解入力として通ってしまう。 */ export type OcarinaCommand = 'DO' | 'MI' | 'SO'; export const IGNORE = 'IGNORE' as const;
「近い音を拾ってあげる」親切心は、吹き間違いを正解として通してしまうので、明確に IGNORE を返す設計にした。
Player Input ↓ Game Logic (Pure TypeScript) ↓ Game State / Events ↓ ┌───────────────┐ │ Rendering │ │ Audio │ │ HUD / UI │ └───────────────┘
React Component / R3F Component が戦闘ルールそのものを持たないようにした。判定は純粋関数で書き、結果を Game Event として発行し、描画・音・UIはそれを購読する。
resolvePlayerAction({ attack, action, inputTime }) // → { result: "PERFECT_DODGE", nextState: "COUNTER_WINDOW" }
これは方針ではなく、lint で機械的に検証している。
src/game/** から three / @react-three/* / react を import すると pnpm lint が落ちる(.oxlintrc.json の no-restricted-imports)。
src/ ├─ game/ Game Logic(Three.js・Reactへ依存しない) ├─ input/ DOM/カメラ/マイク → PlayerAction ├─ rendering/ React Three Fiber ├─ audio/ Audio Cue ├─ ui/ HUD / イベントUI └─ store/ Zustand(表示状態のみ)
この分離のおかげで、Phase 2 で1人用を3人プレイへ分解するとき、戦闘ルール本体をほとんど書き換えずに済んだ。
入力源がキーボードからARマーカーや音声へ増えても、GameAction という共通の契約に変換するだけで済む。
副次的な効果として、戦闘ロジックがブラウザ非依存になるのでテストが速く書ける。
開発初期はモーション定義が3箇所へ散っていた。Blenderのビルドスクリプト、表示層の MOTION_CLIPS、character-models.ts の clip。
どれか1つを足し忘れるとキャラが一切動かなくなるが、気付く手段が無かった。
そこで assets/motion-manifest.json 1本へまとめ、Blender(Python)側とTypeScript側の両方が同じファイルを読むようにした。
scripts/motion_manifest.py が「ビルドで作るクリップ」と clips の一致を確認motion-manifest.test.ts が「clips」と「実際のGLBの中身」の一致を確認両方向から突き合わせるので、まだ生成できないクリップを書くと両方のテストが落ちる。
状態からクリップを決める部分は、総当たり表ではなく優先順ルールにした。
ボスだけで BossPhase(6) × AttackPhase(4) あるが、意味のあるマスはごく一部なので、実際に使う行だけを短く持てる。
アリーナは半径24の円。この数字は雰囲気で決めていない。移動速度4 units/秒から逆算した。
| 距離 | 値 | 所要 | 何のための値か |
|---|---|---|---|
| 中心 → 端 | 24 | 6.0秒 | ブルーライト照射から逃げ続けられる長さ |
| 端 → 端 | 48 | 12.0秒 | 散ったあと合流し直せる長さ |
| 装置 → 隣の装置 | 29.4 | 7.4秒 | 結界で「間に合うか」の緊張が出る距離 |
| 最遠地点 → 最寄りの安全地帯 | 17.3 | 4.3秒 | ボス攻撃の予兆時間の下限。これより短いと端にいる人が詰む |
座標はすべて src/game/arena/arena.ts に Pure TypeScript で持ち、描画と判定の両方が同じ値を参照する。
ARENA_RADIUS を1箇所直せば、床・岩の壁・木立・装置・安全地帯がすべて追従する。

"react": "19.2.8", "three": "0.186.0", "@react-three/fiber": "9.7.0", "@react-three/drei": "10.7.8", "zustand": "5.0.15", "typescript": "7.0.2", "vite": "8.3.0"
"@mediapipe/tasks-vision": "1.0.1", // 手の検出 "pitchy": "4.1.0", // ピッチ検出(オカリナ) "livekit-client": "2.17.2", // ボイスチャット "ws": "8.18.0" // WebSocket(権威サーバー)
cloudflare workers: 認証サーバーのデプロイ render: websocketのデプロイ
BlenderMCP でAI(Astraとか)が制作し、GLBへ書き出している。座標系の事故が多発したので、ルールをドキュメント化して固定した。
| 空間 | 上方向 | キャラの正面 | | Blender の編集 | +Z | -Y | | GLB(書き出し) | +Y | +Z |
向きが揃っていないモデルは、ゲーム側で回さずBlenderで直す。表示側に個別の補正を積むと、モデルが増えるたびに補正が散らばるため。
https://www.mixamo.com を使用してモーションを取得し、純粋なモデルとモーションをそれぞれトリ合体しました

| 分類 | 採用 |
| Runtime | Node.js 24.19.0 |
| Package Manager | pnpm 11.25.0 |
| Lint / Format | oxlint / oxfmt(Rust製で高速) |
| Test | Vitest + React Testing Library |
| 環境固定 | Nix (flake.nix) + direnv |
| Git hooks | lefthook |
| CI | GitHub Actions |
| デプロイ | Cloudflare Workers (Static Assets) |
環境は Nix で Node.js と pnpm のバージョンごと固定した。nix develop すれば全員が同じ環境になる。
CI は PR と main への push で以下を順に実行する。
install → lint → format:check → typecheck → test → build
デプロイは Cloudflare Workers Builds(Git連携)が担当し、PR作成時にブランチプレビューURLが自動発行されてPRコメントへ投稿されるので、レビュー時に実物を触って確認できる。
前日にあったフロントエンドカンファレンスでテスト大事だなぁって思ったのでAGENTS.mdにテスト周りの整備をしました
テスト名には実装名ではなく「保証したい動作」を書くルールにしました。「storeを別のものに替えたら / 関数名を変えたら名前が嘘になるなら、それは実装の名前になっている」という判定基準まで明文化して、AIエージェントが it('setSleepinessが呼ばれる') みたいなテストを量産するのを防ぎます
バグ修正は「再現テストを書く → 失敗することを確認 → 直す → 通ることを確認」の手順を原則化。
本作はB級3DアクションRPGの「壊れて見えるが内部はちゃんと動いている」手触りを狙っているので、意図して残すバグは *.bug.test.ts に隔離しています。「現在どのバグが意図なのか」がファイル一覧で読める状態にしておかないと、善意のリファクタで演出が消えてしまうのでカバレッジ率はKPIにしません。壊れると影響が大きいロジック・境界値・状態遷移・過去に発生したバグがテストされていることの方が大事
方針全体は docs/testing-strategy.md 書き方の手順は docs/testing-guide.md に分離して、AGENTS.md からは参照だけ貼る構成にする src/AGENTS.md に置くことで、src/** を編集するコーディングエージェントに確実にルールが届くようにしているのが地味にポイントです。
最終カバレッジ
AGENTS.md / docs/ に仕様と設計方針を先に書き、Agentがそれを参照しながら実装する運用にした。
特に効いたのが、前述の 「Game Logic に three / react を import させない」を lint ルールにしたこと。 方針をドキュメントへ書くだけだと、AIも人間もいずれ破る。機械的に落ちるようにしておくと、設計が勝手に守られる。
同様に、モーション定義のマニフェスト突き合わせテストも「壊れたら気付ける」ための仕組みとして入れている。
1週間配信30分睡眠フィナーレ
https://www.youtube.com/live/uFPpeBlsWOg?si=VS3ig_geGSPrgiCJ