Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A social deduction party game where one player in every lobby is secretly an AI, and
everyone else is trying to figure out who. The twist: the AI actually learns how you
write over time, so the more you play the harder it gets to catch.
Built it as a portfolio piece — real-time multiplayer, an AI that has to blend in, and
a game-theory-flavored veto rule that keeps players from leaking information.
Backend is in GameApi, the phone-first React client is in game-client.README 전문 보기 ↗A social deduction party game where one player in every lobby is secretly an AI, and everyone else is trying to figure out who. The twist: the AI actually learns how you write over time, so the more you play the harder it gets to catch.
Built it as a portfolio piece — real-time multiplayer, an AI that has to blend in, and a game-theory-flavored veto rule that keeps players from leaking information.
| Layer | Tech |
|---|---|
| Frontend | React 19 + Vite (plain .jsx, no TypeScript), React Router |
| Backend | ASP.NET Core 8, EF Core 8, SignalR |
| Database | PostgreSQL (Supabase free tier) |
| Auth | ASP.NET Core Identity + JWT bearer |
| AI player | Google Gemini (gemini-2.5-flash, free tier) behind an IAiBrain interface |
| Containers | Docker (multi-stage, serves the client from wwwroot) |
| Deploy | Render (API), Vercel (frontend), GitHub Actions cron (gentle keepalive) |
Backend is in GameApi, the phone-first React client is in game-client.
On your own? The home screen has a solo demo: three bot stand-ins get seated with you and the AI still hides among them. Five rounds, nothing saved, same rules — enough to see the trick without rounding up friends. Hosts also get a QR code + share link in the lobby so phones can scan in instead of typing the code, and every game ends on a recap: the AI's line for each prompt, every accusation and how it went, the style notes the AI was carrying on each player, and a share button that hands your phone a result card. In the solo demo a bot will even accuse someone and get faked-out by another bot, so you see the veto rule play out without anyone explaining it.
There's also a reverse mode: no hidden impostor — everyone's human, and the AI is the one guessing who wrote which (shuffled, anonymous) answer each round. It's the same style-profile tech pointed the other way, so it's gated on having a bit of play history (see the ADRs). The whole client is phone-first and got a dedicated mobile visual sweep so nothing breaks on a small screen.
Every push and PR runs the whole thing through GitHub Actions — the .NET suite (240 tests on EF InMemory, no DB needed), the client lint + build, a Docker image build, and a Playwright game played start to finish across desktop and mobile viewports. Green badge above means all of that passed on main.
There's a k6 script in loadtest/k6-lobbies.js that mimics the path a
real client takes into a lobby — health ping, guest login for a JWT, then the SignalR
/negotiate handshake — and runs it from 20 concurrent virtual users. Point it at a local
API on the in-memory DB + Mock brain (see the header comment in the script), then:
k6 run loadtest/k6-lobbies.js
Latest local run (loopback, in-memory DB, 20 VUs over ~35s):
| Metric | Result |
|---|---|
| Requests | 1,665 total, 0 failed |
| Throughput | ~46 req/s (555 full login→negotiate cycles) |
| Latency | avg 1.3 ms, p95 2.4 ms, max 297 ms |
| Checks | 2,775 / 2,775 passed |
These are a floor, not a headline — it's all over the loopback with an in-memory store, so
there's no network or Postgres in the path. What I actually wanted to prove is that login +
the negotiate handshake stay flat under concurrency and nothing 500s. One gotcha worth
knowing: the API rate-limits 30 req/min per IP, and since k6 all comes from one IP you have
to bump RateLimit__PermitsPerMinute for the run or you just measure the limiter (the
/hubs path is exempt, so negotiate sails through either way).
You need .NET 8 SDK, Node 22, and a Postgres connection string (Supabase free works).
API:
cd GameApi
dotnet ef database update # applies migrations to your DB
dotnet run
Runs on http://localhost:5222 by default.
Client:
cd game-client
npm install
npm run dev
Runs on http://localhost:5173, talks to the API on 5222.
The API reads secrets from env vars (or GameApi/.env locally — gitignored). None of
these are ever committed.
| Var | What it is |
|---|---|
ConnectionStrings__DefaultConnection | Supabase Postgres string, Npgsql key-value format (Host=...;Port=5432;Database=...;Username=...;Password=...;SSL Mode=Require;Trust Server Certificate=true) |
GEMINI_API_KEY | Google AI Studio key for the AI player. Without it the app falls back to the Mock brain, so it still runs. |
JWT_KEY | Signing key for JWTs, 64+ random chars |
Cors__AllowedOrigins__0 | Allowed frontend origin (your Vercel URL in prod). Defaults to http://localhost:5173. |
Ai__Brain | Gemini or Mock. Defaults to Gemini when a key is present, else Mock. |
Client:
| Var | What it is |
|---|---|
VITE_API_URL | Where the API lives, e.g. https://your-service.onrender.com. Leave it unset when the API serves the client itself (the Docker image) — the client then talks same-origin. |
The Docker image is self-contained — it builds the React client and serves it out of
wwwroot, so the image alone is fully playable. In the real deploy the frontend also
lives on Vercel (faster static hosting, its own domain) and points at the Render API.
Dockerfile.ConnectionStrings__DefaultConnectionGEMINI_API_KEYJWT_KEYCors__AllowedOrigins__0 = your Vercel URL (add after Vercel is up, then redeploy)PORT itself (10000 for Docker). The app binds to it automatically —
don't set ASPNETCORE_URLS.game-client (Vercel auto-detects Vite).VITE_API_URL = your Render URL.Cors__AllowedOrigins__0 to the Vercel URL and
redeploy so CORS lets the Vercel origin through..github/workflows/keepalive.yml pings <Render URL>/api/health four times a day
from GitHub Actions. That's enough to reset Supabase's 7-day idle clock and nothing more.[ WAKING UP ] note while that happens.The API doesn't go down. If Postgres doesn't answer at boot, it comes up on an in-memory
store instead: fully playable, nothing saved, a [ TEMP MODE ] banner on the login and
home screens, and storage: "memory" on /api/status and /api/health. Restore the
project in the Supabase dashboard and the API restarts itself onto Postgres within a few
minutes, between games. Details in ADR 0010.
Full click-by-click walkthrough is in DEPLOY.md.
Realtime runs over a single SignalR hub (/hubs/game), JWT passed as a query-string token
on the handshake. A dropped socket (phone lock, tunnel) auto-reconnects and calls Rejoin,
which re-attaches the new connection to the player's seat and returns a full state snapshot
so the screen rebuilds mid-round instead of freezing. Lobby and game state live in memory in a ConcurrentDictionary
keyed by join code, each lobby behind its own lock — a hosted background service ticks the
state machine and fires all timers server-side, so client clocks are never trusted. The DB
only gets written at game end (the game, messages, per-player stats, and each player's
answers harvested into their writing samples).
The AI player sits behind an IAiBrain interface with two implementations: GeminiBrain
(real, calls Gemini over raw HTTP) and MockBrain (canned answers, used by the tests so
they never hit the network). Its answers run through a post-processing pipeline that makes
them match the group's average length, capitalization and punctuation habits, sprinkles in
the occasional typo, and drops anything AI-flavored — the goal is the statistical middle of
the room, never the funniest or most polished. Style profiles are the killer feature:
after each game a player's answers are appended to their sample pool, a background job asks
Gemini to summarize how they write into a compact JSON blob, and those summaries get injected
into the AI's system prompt at the next lobby start. The more you play, the better it copies you.
The whole thing is audited so the AI's identity never leaks in any payload before game end — rosters carry no user IDs, revealed answers are keyed by display name only and shuffled, and the veto rule exists specifically so a veto can't confirm a correct guess.
/admin (Google sign-in on the ADMIN_EMAILS allowlist) has, besides the analytics tiles
and user directory:
CLEANUP to confirm: removes safe-to-delete accounts,
guests past the 30-day retention rule, consumed rewards older than 90 days, and dead
in-memory lobbies. Transcripts are kept (author links nulled).The engine also sweeps dead lobbies on its own: a finished game with nobody attached is dropped at once, an abandoned one after 10 minutes, so memory doesn't grow across the month.
The interesting decisions are written up as short ADRs in docs/adr/ — the
free-tier-only stack, the Gemini → Groq → Cerebras failover chain, why lobby state lives in
memory, the JWT/sessionStorage auth choices, how the AI stays anonymous in every payload,
difficulty as flag records, signed pack share codes, per-user rate limiting, reverse mode, the in-memory fallback that keeps the game playable when the free-tier database is paused, and the solo demo / rejoin design.
The anonymity side has its own threat model — the ways a player could try to unmask the AI (frame sniffing, timing, statistical tells, host abuse, cross-rematch replay) and what the code does about each, plus the ordinary web hardening (auth, rate limits, admin gate, pack-code signing, prompt-injection guardrails).