OhMyGame
OhMyGame은 AI로 게임을 만들 수 있는 오픈소스 스튜디오로, 디자인·구축·플레이테스트·출판을 하나의 앱에서 모델 키와 함께 수행합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A daily AI murder mystery. Everyone gets the same case each day. Read the file, study the evidence, interrogate four AI-played suspects (20 questions in total), then make one accusation.
▶ Play it: dailyalibi.vercel.app · English + Turkish · no sign-up
Built with Next.js 16 (App Router), TypeScript, Supabase (Postgres + Row Level Security + anonymous auth), Gemini's free tier, Zod, next-intl and Tailwind. It runs on Vercel Hobby and Supabase Free and costs $0 a month.
A chatbot that plays a murder suspect is easy to build. A game built on one has three harder problems:
The sections below explain how each one is handled.
flowchart LR
subgraph Browser
UI["Case file · Evidence<br/>Interrogation · Notebook"]
end
subgraph Vercel["Next.js on Vercel (server)"]
RSC["Server Components<br/>(public briefing only)"]
API["Route Handlers<br/>/api/interrogate · /api/hint · /api/accuse"]
PROMPT["Suspect prompt builder<br/>(one suspect's notes only)"]
GUARD["Output guard<br/>(streamed, 40-char holdback)"]
CRON["Vercel Cron<br/>daily publish"]
ADMIN["Admin panel<br/>(Server Actions)"]
end
subgraph Supabase
DB[("Postgres + RLS<br/>cases.secret: no browser grant")]
AUTH["Anonymous auth<br/>+ Turnstile"]
end
subgraph Offline["Offline (GitHub Actions / CLI)"]
GEN["Generator → Zod → 14 logic checks<br/>→ blind solver → repair"]
end
LLM["Gemini<br/>(free tier, behind a provider-agnostic module)"]
UI -- "public data" --> RSC
UI -- "question (≤ length limit)" --> API
API -- "counter, rate limit (SQL functions)" --> DB
API --> PROMPT --> LLM --> GUARD -- "streamed answer" --> UI
UI <-- "own rows only (RLS)" --> DB
UI --> AUTH
CRON --> DB
ADMIN -- "approve · schedule" --> DB
ADMIN -. "dispatch" .-> GEN
GEN --> LLM
GEN -- "draft + review" --> DB
One game, step by step: the case page is a Server Component that reads only the public briefing (cached). Each question is a POST to a Route Handler. The server checks the session and rate limits, reserves a question in Postgres (the counter lives in a SQL function, not in the browser), loads the case itself, builds the prompt for that one suspect, and streams the answer back through the output guard. The accusation is checked and scored on the server. Only after that is the solution sent to this player.
The rule: the killer, the motive, the true timeline and every suspect's secrets never reach the browser, in any form.
src/lib/case/schema.ts) has a public part and a secret part. Every module that touches secret imports server-only, so importing it from a Client Component breaks the build.cases.secret has no column grant for the anon/authenticated roles. A player who copies the publishable key and queries Supabase directly gets permission denied. Players can read their own games and messages, but can only write them through SQL functions (start_game, reserve_question, finish_game, …) that only the server's key may execute. Every table has RLS enabled.start_game function and every server read require it. Without this, a case scheduled for next week could be played early.src/server/interrogation/prompt.ts) takes the public briefing, one suspect's private notes and an isKiller boolean. The solution and the other suspects' notes are not parameters, so they cannot end up in the prompt. An innocent suspect does not know who the killer is, so no jailbreak can get it out of them.npm run check:leaks builds the app and searches every client JS file and pre-rendered HTML/RSC payload for every secret string of every case (115 at the last live check). Result: 0 leaks.npm run check:prompts builds every suspect's real prompt and fails if it contains another suspect's secret or (for innocents) the solution.npm run smoke plays a full game as a real user, then tries to cheat with the same credentials: reading secret, resetting the counter, calling the RPCs directly, reading another player's notes.revealWhen, such as "when shown the telephone log". The rule is that a condition beats a lie, and a condition that lists several things needs all of them. With the Show evidence button, a player can put a piece of evidence in front of a suspect. The evidence text comes from the case, never from the player.answer-guard.ts, guarded-stream.ts) holds back the last 40 characters. When an answer breaks character, echoes the prompt or (for the killer) confesses, it stops the model and replaces the unsent part with an in-character line. What is saved is exactly what the player saw.| Eval | What it does | Result |
|---|---|---|
Prompt injection (npm run eval) | 50 attacks (40 single-turn + 10 multi-turn, 10 categories, EN + TR) against every suspect role in 4 cases, using the game's real prompts | 199/200 defended (model alone 198/200). The one failure was a false alarm on manual review. Right language: 200/200 |
Consistency (npm run eval:consistency) | 6-question interviews per suspect, EN + TR, including evidence confrontations, graded by a judge model against the private notes | 192 answers: consistent 99.5%, no invented facts 100%, no early reveals 98.4%, reveals on cue 99.5%, in character 99.5% |
The evals found real bugs that were then fixed. A killer gave up their motive after seeing one piece of evidence when two were required. An internal [Reply in Turkish.] instruction leaked into an answer. A Turkish pattern in the guard was too strict.
LLM-written mysteries fail in two ways: they contradict themselves, or they can be solved from the case file alone, without any interrogation. The pipeline (src/server/generator/) checks both:
npm run check:solver checks that the solver's input really is blind.Generation with a thinking model takes 3-12 minutes, longer than Vercel's 300-second function limit. The admin panel therefore starts a GitHub Actions workflow and shows its recent runs.
ChatModel interface (src/server/ai/), so the model is set in one place. If the main model hits its quota, errors, or sends nothing for 10 seconds, the same request goes to a fallback model.fetch is enough. Gemini (SSE streaming) and Turnstile are called directly. The app has 8 runtime dependencies.cp .env.example .env.local # fill in Supabase + Gemini keys (see comments in the file)
npm install
# apply supabase/migrations/*.sql to your Supabase project, then:
npm run db:seed # loads the hand-written sample case
npm run dev
Without GEMINI_API_KEY, development uses a mock model.
| Command | Purpose |
|---|---|
npm run check:leaks | Search the production build for secret case text |
npm run check:prompts / check:solver | Verify prompt and solver isolation |
npm run check:guard | Output-guard unit checks |
npm run smoke | Full game + cheating attempts against a running app |
npm run eval / eval:consistency | AI evals (use GEMINI_OFFLINE_API_KEY) |
npm run generate -- --difficulty=hard | Generate and check a new case |
src/app/[locale]/ pages (EN/TR), incl. case, interrogate, accuse, result, archive, lab, admin
src/app/api/ route handlers: interrogate, hint, accuse, game, player, cron
src/lib/case/ Zod case schema (public / secret split)
src/server/ai/ provider-agnostic model interface, Gemini, fallback, retry
src/server/interrogation/ suspect prompt, output guard, game store (Supabase RPCs)
src/server/generator/ case generator, logic checks, blind solver
supabase/migrations/ schema, RLS, game functions, rate limits
scripts/ leak/isolation checks, smoke test, evals, generator CLI