Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Crash the Party

A live text-argument game. Pick a faction from an Indian show rivalry, shape your own character (2 of 5 traits + 1 of 3 relationships), talk your way past the bouncer, run an errand inside, and face the host. Your choices get scored from 0 to 100.

Four built-in rivalries:

Show Rivalry
Mirzapur Kaleen Bhaiya vs Guddu Bhaiya
Sacred Games Ganesh Gaitonde vs Inspector Sartaj Singh
The Family Man Srikant Tiwari vs Raji
Heeramandi Mallikajaan vs Fareedan

The bouncer, the ally who vouches for you, and the host who runs the party are all canon characters from docs/traits.json. Your character is separate from all three — you are a player-persona aligned with the ally, not the ally themselves.

See docs/PRD.md, docs/TechSpec.md, and docs/PHASE.md for the spec.

Repo layout

backend/   Node.js + Express + ws + Mongoose
frontend/  React + Vite
docs/      PRD, TechSpec, PHASE, traits.json (canon source of truth)

docs/traits.json is the single source of truth for canon rivalries, canon character traits, the player trait pools (5 per ally), and the player-to-ally relationship pools (3 per ally). The server reads it directly; the client never sees it raw.

Prerequisites

  • Node.js 18+ (tested with 24)
  • npm 9+
  • A MongoDB instance (optional for local dev — the backend boots and serves /health even without one, falling back to in-memory session storage)
  • An OpenRouter API key for live agent calls

Backend

cd backend
cp .env.example .env       # fill in OPENROUTER_API_KEY
npm install
npm run dev                # nodemon, watches src/, listens on :5000

.env keys:

Key Purpose
OPENROUTER_API_KEY Required for live Bouncer / Ally / Host / Recap calls
OPENROUTER_MODEL Main model — fast, low-latency preferred
OPENROUTER_ALLY_MODEL Optional faster model for the ally's at-the-door line
OPENROUTER_ALLY_API_KEY Optional separate key for the ally model
OPENROUTER_TIMEOUT_MS Per-agent-call ceiling. Default 45000
MONGODB_URI Mongo connection string. Empty → in-memory fallback
PORT Default 5000
ENABLE_DEV_ROUTES Internal: gates the dev-only test routes

By default the backend listens on http://localhost:5000 and exposes /health. WebSocket upgrades are accepted on /ws/session/:id.

Frontend

cd frontend
npm install
npm run dev                # vite dev server on http://localhost:5173

The Vite dev server proxies /api and /ws to the backend on port 5000, so the front end hits fetch('/api/...') same-origin during development.

How a session flows

1. /setup
   pick scenario  →  pick faction (a or b)  →  first name
   → pick 2 of 5 player-persona traits
   → pick 1 of 3 relationships to your ally
   → POST /api/session

2. /bouncer
   short chat. bouncer is dry and skeptical; ally may interject.
   five tension bands (calm → nearly caught).
   let_in  → continue.    turned_away → recap + retry.

3. /inside
   one of four task types is rolled:
     - password_clues   (guest drops hints, guess the password)
     - turn_npc         (NPC from the rival faction; threaten / bribe / befriend)
     - mcq              (3 quick reads on the host)
     - trust_choice     (two guests, conflicting claims)
   familiarity is your standing in the room. clear the hidden threshold
   to win. on win, you pick up an intel fragment — a piece of leverage
   about the host that gets revealed to you for a few seconds.

4. /host-conversation
   3 turns with the host. your intel fragment (if any) gives your second
   answer more bite. outcome band:
     respected    (sum of turnScores >= +4)
     awkward      (sum -1..+3)
     barely_scraped (sum <= -2)

5. /recap
   score 0–100 + a 2–4 line screenshot card. optional photo upload +
   server-rendered victory card PNG (data URL, no native deps).

API surface

All routes are JSON in / JSON out. The session id is opaque.

GET  /api/scenarios                                       — list of all rivalries
GET  /api/scenarios/:scenarioId/pools?faction=a|b         — trait + relationship pool for the chosen ally
POST /api/session                                         — create: { scenarioId, faction, playerName, traits[2], relationship }
POST /api/session/:id/argue                               — one user line at the bouncer
GET  /api/session/:id                                     — debug view (raw suspicion / threshold)
GET  /api/session/:id/task?taskType=…                     — roll + return the inside task (idempotent)
POST /api/session/:id/task                                — submit one choice; resolves against hidden config
POST /api/session/:id/host-conversation                   — drive one of the 3 host turns
GET  /api/session/:id/recap                               — final score + recap card
POST /api/session/:id/victory-card                        — render a PNG victory card data URL
WS   /ws/session/:id                                      — live transcript stream

POST /api/session is strict. The validator enforces:

  • scenarioId is a known scenario
  • faction is exactly a or b
  • playerName is a non-empty string after trim
  • traits is exactly 2 entries, both from the chosen ally's playerPersonaTraits pool, no duplicates
  • relationship is exactly 1 id from the chosen ally's playerRelationshipOptions pool

Anything else → 400 { "error": "..." }.

Two-character model

The session doc carries two halves, separately:

{
  canon: {
    allyName, hostName, allyTraits, hostTraits, rivalry, show
  },
  playerPersona: {
    name, faction, allyName, hostName, show,
    playerTraits: [t1, t2],
    playerRelationship: { id, label, description }
  }
}

The AI prompts (Bouncer, Ally, Host) receive both halves. They are instructed:

  • Canon traits of the ally and the host are immutable. They never change based on player choices.
  • The player is a person aligned with the ally, NOT the ally.
  • The player's traits come from a 5-option pool belonging to the chosen ally. They are the player's traits, not the ally's.
  • The player's relationship to the ally is one of 3 options belonging to the chosen ally. It is NOT the canon rivalry between the ally and the host.

Adding a new rivalry

  1. Add a new scenario to docs/traits.json with two characters, each having 5 canonTraits, 5 playerPersonaTraits, and 3 playerRelationshipOptions.
  2. Add a SCENARIOID_TO_TRAITSID entry in backend/src/lib/personaConfig.js mapping the existing scenario id to the new traits.json id.
  3. Add the scenario id to frontend/src/state/characterImages.js so the Setup screen gets a portrait / show backdrop.

Nothing else changes — scenarios list, validation, prompts, and the session doc all derive from traits.json.

Server-side test scripts

cd backend

# Validates POST /api/session's 5-arg payload against every bad shape
# (1 trait, 3 traits, duplicate trait, foreign trait, foreign
# relationship, wrong-ally trait, missing firstName, etc.) and confirms
# canon + player halves persist separately on the doc.
node scripts/test-persona-validation.js

# Confirms the runner's POST shape works against POST /api/session and
# that the session has canon + player halves persisted.
node scripts/test-play-create.js

# Walks every (show × faction × taskType) = 32 combos. Uses the
# resolver tables to pick winning choices on every task, so every row is
# a guaranteed win. Logs the intel fragment that landed per combo.
node play-all-tasks.js

play-all-tasks.js runs server-side, so it can read correctIndex / truthIndex from the resolver and always log a win. It is the canonical way to verify that every (show, faction) combo produces a coherent run.

Browser-side test scripts

Open http://localhost:5173/ and paste into the console:

// One (show, faction, taskType). Picks 2 traits + 1 relationship from
// the server's pool, creates the session, redirects to /inside.
await window.__playOneTask('mirzapur', 'a', 'password_clues');

// All 32 combos back-to-back, 10s between each, with one row per combo
// in console.table.
await window.__playAllAutomated({ delayMs: 10000 });

The source for these is at frontend/public/play-one-task.js and frontend/public/play-all-automated.js.

Project conventions

  • Two layers of canon + player never merge. The AI prompts read both halves separately. See backend/src/prompts/{bouncer,ally,hostConvo}.js.
  • The server is the single source of truth for trait / relationship pools. The client never sees docs/traits.json directly.
  • Hidden game state (bouncer threshold, insider threshold, correctIndex, truthIndex) is server-only and never sent in HTTP responses.
  • Idempotent GET /task and POST /host-conversation — refresh-safe.
  • Mongo is optional. Without a live connection the backend keeps the session in an in-process Map so dev works against any OPENROUTER_* config.

About

A live text-argument game. Pick a faction from an Indian show rivalry, shape your own character (2 of 5 traits + 1 of 3 relationships), talk your way past the bouncer, run an errand inside, and face the host. Your choices get scored from 0 to 100.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages