Skip to content

Repository files navigation

内卷 · INVOLUTION

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 · 简体中文

site TanStack Start React TypeScript Supabase Tailwind License

内卷 · INVOLUTION

What is this

内卷 · 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.

Highlights

  • 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 +2 Stress 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.

Example showcase

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.

Rulebook

Requirements

  • Node 22.12 or newer, required by TanStack Start. Verified on Node 24.15.
  • Bun for installing dependencies. The repository ships bun.lock and a bunfig.toml supply 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.

Install

git clone https://github.com/ydna564/involution-equation.git
cd involution-equation
bun install

If you prefer npm, the @hookform/resolvers peer range conflicts with the resolved valibot version, so the install needs a flag.

npm install --legacy-peer-deps

Quick start

1. Set up Supabase

Create 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 push

2. Configure the environment

cp .env.example .env

Fill 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.

3. Run it

bun run dev

Open 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.

Configuration

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

How it works

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.

  1. Event. On rounds 3, 6, and 9 the server draws one of six external event cards and applies it before anything else.
  2. 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.
  3. Reveal. Once everyone has locked in, all picks flip simultaneously.
  4. 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.
  5. 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.

The four strategy cards

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

Stress states

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

Final settlement

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.

Project structure

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

Tech stack

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.

Testing

Unit tests cover the pure engine. All 22 pass across 4 files.

bun run test

The 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.mjs

The 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.

Deployment

Nitro produces the server bundle and Cloudflare is the default target.

bun run build

Set 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.

Security notes

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.

Limitations and known issues

  • Most card art does not ship with the repository. The talent, event, title, and recovery images are .asset.json pointers 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 in src/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 from package.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 the valibot peer range that @hookform/resolvers requests.
  • TanStack Start emits deprecation warnings. Every server function uses inputValidator(), which the current version has renamed to validator(). The warnings are noisy but harmless.
  • Room codes are four letters. With enough concurrent rooms, collisions become plausible.

License

Released under the MIT License. See LICENSE.

About

A real-time bilingual browser card game for 3 to 6 players about academic burnout, built with TanStack Start, React 19 and Supabase realtime.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages