The Involution Equation. A real-time browser card game for 3 to 6 players about academic burnout, where everyone works harder and nobody moves up.
在线试玩 · What Is This · Quick Start · How It Works · Deployment · 简体中文
内卷 · INVOLUTION is a multiplayer party card game that runs entirely in the browser. No install, no account, no download. One player creates a room, shares a four letter code, and 3 to 6 people play nine semesters of high school before sitting a final exam called the Gaokao. Every round each player secretly picks one of four strategy cards, all picks flip at once, and the results resolve together.
The game exists to make one economic idea playable. When a single player picks Midnight Cramming, every other player gains stress whether they crammed or not. The rational response is to cram back. After nine rounds of everyone cramming, the class has paid enormous stress and the ranking looks almost the same as it did at the start. That is the involution equation, and the game is designed so that players feel it rather than read about it.
It deliberately does not have accounts, matchmaking, persistent ratings, or a mobile app. Rooms are ephemeral, identity is a name you type, and the whole thing is meant to be played once with friends in a shared voice call or a room.
The entire interface is bilingual. Every card, state, event, and rule appears in both English and 简体中文 at the same time, not behind a language switch.
- Zero friction entry. Create a room, share a four letter code, and play. There is nothing to install and no account to make.
- Genuine simultaneous play. All players lock in secretly, then every pick reveals at once. Supabase Realtime keeps every browser on the same round, phase, and score without polling.
- The cramming externality is the core mechanic. Midnight Cramming gives the crammer Academic
+7, and it hands+2Stress to every other player who did not cram. Nobody can opt out of somebody else's ambition. - Eight innate talents. Each player draws two at the start and keeps one. Five are active abilities used once, three are always on passives, and they make identical scores play out differently.
- Stress has teeth. Five escalating states change what you are allowed to do. Apathy forbids cramming outright, and crossing 20 Stress triggers burnout, which costs Academic and forces you to sit out a round.
- Bots fill empty seats. Add computer players to reach the three player minimum. Their picks follow a stress aware policy rather than random choice.
- Reconnects are survivable. Close the tab, lose wifi, or open the rulebook mid round, and you return to the exact game state. Seventeen end to end scripts exist specifically to prove this.
- Fully bilingual. English and 简体中文 sit side by side across the whole interface, including the rulebook.
The homepage above is the entry point. The in game rulebook is a single page that documents every card, state, event, talent, and the settlement formula in both languages.
- Node 22.12 or newer, required by TanStack Start. Verified on Node 24.15.
- Bun for installing dependencies. The repository ships
bun.lockand abunfig.tomlsupply chain guard. - A Supabase project. The game stores all room state in Postgres and pushes updates over Supabase Realtime, so it cannot run without one.
Optional. Playwright, needed only to run the end to end scripts under scripts/e2e/. It is not a declared dependency, so install it separately.
git clone https://github.com/ydna564/involution-equation.git
cd involution-equation
bun installIf you prefer npm, the @hookform/resolvers peer range conflicts with the resolved valibot version, so the install needs a flag.
npm install --legacy-peer-depsCreate a project, then apply the schema. The supabase/migrations/ directory holds 16 ordered migrations that create the tables, the row level security policies, and the room creation function.
supabase link --project-ref your-project-ref
supabase db pushcp .env.example .envFill in the values from Settings, then API, in your Supabase dashboard. The service role key is mandatory. Every server function resolves rounds with an admin client that bypasses row level security, so the game will refuse to create a room without it.
bun run devOpen http://localhost:8080. Type a name, press STAMP & START, and you land in a lobby with a four letter room code. Add two bots to reach the minimum of three players and press start. If the service role key is missing you will see Missing Supabase environment variable(s): SUPABASE_SERVICE_ROLE_KEY on the homepage instead of a room.
| Variable | Required | Exposed to browser | Description |
|---|---|---|---|
VITE_SUPABASE_URL |
yes | yes | Supabase project URL used by the browser client |
VITE_SUPABASE_PUBLISHABLE_KEY |
yes | yes | Anon or publishable key. Public by design, guarded by row level security |
SUPABASE_URL |
yes | no | Same project URL, read by the SSR entry |
SUPABASE_PUBLISHABLE_KEY |
yes | no | Same anon key, read by the SSR entry |
SUPABASE_SERVICE_ROLE_KEY |
yes | no | Bypasses row level security. Server only, never commit it |
The server is the only authority on game state. Browsers never compute a score. Every action is a TanStack Start server function in src/lib/game.functions.ts that reads the room, calls the pure engine, writes the result, and lets Supabase Realtime push the new rows to everyone.
A round has five steps.
- Event. On rounds 3, 6, and 9 the server draws one of six external event cards and applies it before anything else.
- Plan. Every player secretly picks one strategy card. Picks are written with
revealed = false, and a row level security policy hides the card column until reveal, so the secret holds at the database level rather than in the UI. - Reveal. Once everyone has locked in, all picks flip simultaneously.
- Resolve. The engine applies effects in a fixed order. Event, then base card effects, then the cramming externality, then end of round state penalties, then the burnout check.
- Cleanup. Scores are written, the round advances, and the recovery deck reshuffles when it runs dry.
The engine in src/lib/game/engine.ts is pure. It takes players, picks, an event, a recovery deck, and an injectable random source, and it returns the new state with a human readable breakdown of every number. That purity is why the rules are unit testable without a database.
| Card | Academic | Stress | Note |
|---|---|---|---|
| 正常学习 Normal Study | +4 |
+1 |
The baseline |
| 合作学习 Peer Tutoring | +2 |
+1 |
Becomes +5 Academic if at least half the active players pick it |
| 加班学习 Midnight Cramming | +7 |
+3 |
Every other active player who did not cram gains +2 Stress |
| 休息 Rest | +0 |
varies | Draw a recovery card worth −1 to −5 Stress |
Academic is clamped to the range 0 to 49. Stress starts at 0 and has no upper bound.
| State | Stress | Effect |
|---|---|---|
| 健康 Healthy | 0 to 5 | None |
| 焦虑 Anxiety | 6 to 10 | Study cards cost −1 Academic and +1 Stress. End of round +1 Stress |
| 睡眠不足 Sleep Deprived | 11 to 15 | Study cards cost −1 Academic and +1 Stress. Final Academic takes −3 at settlement |
| 冷漠 Apathy | 16 to 20 | Cramming is disabled. End of round +1 Stress |
| 崩溃 Burnout | above 20 | Academic −5, Stress resets to 12, and you sit out the next round |
After round nine the class sits the Gaokao.
Exam Performance = floor(Academic × (1 − Stress / 40))
Happiness = floor(Exam Performance − Stress)
Six titles are then awarded from those two numbers, including 卷王 Involution King for the highest raw Academic and 乐天派 Optimist for the highest Happiness. The two rarely go to the same player, which is the point.
src/
lib/game/ Pure rules and engine. No IO, fully unit tested
lib/game.functions.ts Server functions, the only writer of game state
routes/ index, how-to-play, room.$code, plus a blog page and sitemap
components/game/ Game specific UI such as the player board and chat panel
components/ui/ shadcn primitives built on Radix
integrations/supabase/ Browser client, SSR client, and the admin client
hooks/useRoomState.ts Realtime subscription and room state cache
supabase/migrations/ 16 ordered migrations, schema and RLS policies
scripts/e2e/ 17 Playwright scripts covering realtime and reconnects
docs/ README screenshots
The frontend is React 19 on TanStack Start 1.x, with TanStack Router for file based routing and TanStack Query for the cache that Realtime invalidates. Styling is Tailwind 4 with shadcn components over Radix primitives. The visual language imitates a Chinese exam paper, with seal stamps, red ink, and dotted grid paper.
The backend is Supabase alone. Postgres holds the state, row level security scopes every table to room participants through an x-client-id header, and Realtime pushes changes. There is no separate API server. Nitro builds the deployable output, targeting Cloudflare by default.
Unit tests cover the pure engine. All 22 pass across 4 files.
bun run testThe end to end suite drives real browsers against a running dev server, opening one context per player to verify that separate clients agree on state. Playwright is not a declared dependency, so install it first.
npm install --no-save playwright && npx playwright install chromium
bun run dev
BASE_URL=http://localhost:8080 node scripts/e2e/round-progression.mjsThe 17 scripts cover round progression, talent sync, reconnection during both talent pick and reveal, private room isolation over Realtime, row level security enforcement against a raw anon client, the public room browser, the system log drawer, accessibility of the notice banner, and realtime latency.
Nitro produces the server bundle and Cloudflare is the default target.
bun run buildSet all five environment variables in your host. SUPABASE_SERVICE_ROLE_KEY must be a server side secret and must never reach the client bundle. Only variables prefixed with VITE_ are injected into the browser, which is the boundary that keeps the service role key server side.
Room state lives in Postgres behind row level security. Clients identify themselves with a generated x-client-id header, and a private.is_room_participant() function gates every table so that a non participant cannot read a private room, its players, its chat, or its results. Unrevealed picks are protected by a policy on the card column rather than by the UI, so an attacker with a raw Supabase client still cannot see what you picked before reveal. Two of the end to end scripts exist to verify exactly that.
The anon key is public by design and is shipped in the browser bundle. The service role key is not, and it bypasses every policy above, so treat it as the one real secret in this project.
There is no authentication. Anyone with a room code can join a private room, and names are unverified. This is intentional for a party game, but it means the trust boundary is the room code and nothing more.
- Most card art does not ship with the repository. The talent, event, title, and recovery images are
.asset.jsonpointers to URLs hosted by Lovable that only resolve on Lovable's own hosting. Elsewhere they return 404 and the interface falls back to a text placeholder showing the seal character and the card name. Only the four strategy card images and the hero image are real files insrc/assets/. - The game cannot run without a Supabase project. There is no offline or local only mode.
- Playwright is undeclared. The scripts in
scripts/e2e/import it, but it is absent frompackage.json, so a fresh clone cannot run them until it is installed manually. - npm needs
--legacy-peer-deps. The project is built for Bun. The npm resolver rejects thevalibotpeer range that@hookform/resolversrequests. - TanStack Start emits deprecation warnings. Every server function uses
inputValidator(), which the current version has renamed tovalidator(). The warnings are noisy but harmless. - Room codes are four letters. With enough concurrent rooms, collisions become plausible.
Released under the MIT License. See LICENSE.

