コードジェンガ

https://github.com/rnxo/code-jenga/

Next.js

TypeScript

React

Docker

TailwindCSS

テストを崩さないようにコードを抜き合うジェンガ型対戦ゲーム

ふじー

ふぇふぇ

Nezumi111

ようた

推しアイデア

ジェンガの「1本抜いて崩れたら負け」を、コードの「1行消してテストが落ちたら負け」に置き換えた。抜けそうな行を見抜く読解力と、相手に危ない行を残す駆け引きが同時に生まれる。Geminiが毎回違う関数とテストを出すので、同じ勝負は二度とない。

作った背景

「コードを1行消しても動くか」はレビューや保守で毎日起きる判断。それをジェンガのルールに乗せ、テストの通る限界を探り合う対戦ゲームにした。読む・消す・検証するを遊びにして、コードの依存関係を体で覚える体験を目指した。

推し技術

コードとテストをGeminiで生成し、Piston(隔離サンドボックス)でVitestを実行して1手ごとにセーフ/アウトを判定。Supabase Realtimeで手番・判定を全員に同期し、盤面はCSS 3Dの積み木として崩壊まで演出。

プロジェクト詳細

コードジェンガ

その1行、消して大丈夫? テストが崩れたら、君の負けだよ☆

image

概要

ジェンガの「1本抜いて崩れたら負け」を、コードの「1行消してテストが落ちたら負け」に置き換えた対戦ゲームです。 お題の関数とテストは毎回 Gemini が生成し、プレイヤーは交互に1行ずつ削除します。削除のたびに隔離環境で本物の Vitest が走り、セーフかアウトかを判定。アウトを出した人が負けです。

作った背景

「この行、消しても動くのか?」はコードレビューや保守で毎日起きる判断です。 それを遊びにすれば、コードの依存関係や「どの行が本当に効いているか」を体で覚えられるのではないか、という発想から始めました。読む・消す・検証する、を対戦の緊張感の中で繰り返します。

遊び方

  1. ホストがルームを作り、6桁のルームコードを相手に伝える
  2. 参加者がコードで入室し、ホストが開始(お題生成に約20秒)
  3. 自分の手番で、コードの行をクリックして「この行を削除する」
  4. テストが走り、セーフなら次の人へ。アウトなら崩壊して負け
  5. 制限時間は1手60秒。時間切れもアウト扱い

image

難易度(手番ごとにランダム)

  • EASY:どの行でも削除できる
  • NORMAL:空行・コメント・記号だけの行は削除できない
  • HARD: 宣言・制御(function / const / if / return など)を含む行しか削除できない

見どころ

  • 1行=木片1本の3Dタワー: コードが CSS 3D の積み木として積まれ、ドラッグで回せる。行数が多いお題は自動で段を詰める
  • 崩壊演出: アウトの瞬間に木片が崩れ落ち、光と効果音、そして自由の女神と奈良の大仏がせり上がる
  • おせっかいくん: 盤面の右下で状況に応じて茶々を入れるマスコット。タイトル画面では「始める」ボタンに被さって邪魔をする(黙らせると押せる)
  • 結果画面: 崩れたタワーがそのまま残り、勝敗を持った像が立つ。ホストは再戦ボタンで次局へ、参加者は自動でロビーに移動
  • 敗北代行: うちのふぇふぇ君が責任もって謝罪してくれます!負けても大丈夫!

技術スタック

フロント(Next.js 16 / React / Tailwind CSS)

画面全体。App Router、Server Component と Client Component を分離

コード表示(Monaco Editor)

読む・選ぶためのエディタ表示(選択行のハイライト)

3D 演出(CSS 3D transform)

木片・崩壊・光。ライブラリなし

タイトル演出(matter-js)

積み木が落ちて積み上がる物理演算

画像認識(MediaPipe Hand Landmarker)

Web カメラで手を検出し、指さし・つまみで操作

お題生成(Gemini API)

関数と Vitest テストをペアで生成

コード実行(Piston)

プレイヤーのコードを隔離環境で実行

テスト(Viston)

お題の検証と1手ごとの判定

DB / 認証 / 同期 (Supabase)

ルーム・試合・手番の保存と、全員への即時同期

デプロイ(Vercel / Cloudflare Tunnel)

本番は Vercel、Piston は発表者 PC の Docker をトンネルで公開

システム

アーキテクチャ

image

Piston周りのシーケンス図

image

特に苦労したSupabase Realtime

対戦相手の一手をリアルタイムに反映する部分で、「購読は成功しているのにイベントが届かない」問題に2度ぶつかった。

  1. 匿名認証の JWT が Realtime に渡る前に購読していたため RLS に弾かれていた →セッション取得後に setAuth してから購読する方針で解決
  2. turns テーブルの filter 付き購読だけが沈黙していた → filter を外してクライアント側で game_id を絞る形で回避

ふじー

@wisteriale