Skip to content
rfeltisPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Coup

A browser-based, real-time game of influence and deception for 2-6 players. A server-authoritative TypeScript rules engine runs independent tables, while a React / Three.js client renders the table and cards in WebGL.

Run locally

Use Node.js 22 or newer.

npm install
npm run dev

Open http://localhost:5173 and create or join a table. Every new player gets a random name (unique at that table) and one of 20 random avatars. Change either in the lobby and click Save player, or keep the defaults. Everyone must click ready before cards are dealt; undo ready to edit your player again. Rejoining keeps your existing name and avatar.

The lobby's Copy room link button copies the full invitation URL, including the current hostname, port when needed, and ?room=ABCD. Friends can open the link to prefill the room code, or enter the four letters themselves. The link is also selectable for manual copying. A new tab automatically restores your saved seat, so use separate browser profiles or private browsing contexts to try multiple players locally.

On the same network, friends can open http://YOUR-LAN-IP:5173. Vite proxies the HTTP API and WebSocket connection to the game server on port 3001. No account, API key, database, or external asset service is required.

Production

npm run build
npm start

Open http://localhost:3001. One process serves both the built client and the WebSocket endpoint. Deploy behind an HTTPS reverse proxy that supports WebSocket upgrades; the client automatically uses WSS on HTTPS pages.

Setting Default Purpose
PORT 3001 HTTP and WebSocket port
HOST 0.0.0.0 Interface to bind
ALLOWED_ORIGINS empty Optional comma-separated exact browser origins when intentionally hosting the client separately

Browser connections are same-origin by default. If a reverse proxy rewrites the Host header, preserve the public host or explicitly allow the public origin. IP limits use the direct peer address; forwarded IP headers are deliberately not trusted.

An included Dockerfile builds the same single-process deployment:

docker build -t coup .
docker run --rm -p 3001:3001 coup

Azure App Service

Bicep in infra/ and .github/workflows/deploy.yml deploy https://stryke-coup.azurewebsites.net on pushes to main or master, using the existing Linux B1 strykeapps-plan in strykerg, West US 3. The shared plan is referenced, not recreated or scaled.

Azure authentication reuses the existing strykeapps-github identity and matching GitHub secrets from rfeltis/strykeapps, with additional OIDC trust for Coup's two deployment branches. See Azure deployment setup for the configuration. The workflow builds and deploys a production ZIP, checks HTTP/WebSocket availability, and refuses a multi-worker plan. Deployments restart the in-memory game server and end active games.

Playing

The rules drawer is available from the lobby and during play. It includes character abilities, counteractions, challenge consequences, and explanations of when a bluff might help. Character actions are never restricted to the cards you actually hold.

Action and blocking buttons show Truth if you have an unrevealed matching influence, or Bluff if you do not. This guidance is computed locally from your own private hand and is never sent with an intention or broadcast to opponents. Every action lists its effect and possible blocks/challenges, even when disabled or unaffordable. All seven choices remain visible while you wait.

Players sit around a shared table with large, readable controls. Drag your own cards inside your marked playmat; all other players see the movement, but not your hidden card faces. An action notice describes claims without creating an extra card; on small screens, it sits below the table to keep seats unobstructed. In normal Coup, declaring Tax does not expose a Duke card.

When a challenge is proved, the actual influence card moves to the center, through the court, and back as a private replacement. Lost influences flip and burn with flames and embers, then leave their playmats for a separate face-up Lost influence area below the table, grouped by owner. They remain visible and marked LOST, but cannot be dragged or played. Exchanges receive a brief card animation; victory brings gold particles, a highlighted winning seat, and a clear result. These effects respect prefers-reduced-motion and do not replay old outcomes when you rejoin; already-lost cards appear directly in the lost-influence area. Static notices remain available, including in the playable non-WebGL fallback.

Player balances are also rendered as individual coins in stacks on their playmats, including in the HTML fallback. The current playmat has a gold glow, moving shine, and a turn marker. A synthesized chime plays once for each new turn (higher for your own turn). The speaker button persists a local mute preference; browsers need a user gesture to unlock audio. Rejoining, card movement, and vote updates do not replay a turn sound.

The implementation follows the printed base-game Coup rulebook (readable transcription), without Reformation:

  • Three copies of each of five characters; two influences and two coins per player. The first game's starter is random; the previous winner starts rematches. In a two-player game, the starting player receives only one coin.
  • Income, Foreign Aid, Coup, Tax, Assassination, Steal, and Exchange, with their standard costs, claims, and legal counteractions. At ten or more coins, a coup is mandatory.
  • Any living opponent can challenge a character claim, including a counteraction. Only the target may block Steal or Assassination; any opponent may block Foreign Aid.
  • A challenged claimant can show a matching influence, shuffle it back into the court, and draw a replacement; the challenger loses an influence. Alternatively, the claimant can concede and lose an influence, even if they could have proved the claim.
  • An Assassin's three-coin fee is paid on declaration. A successfully challenged Assassin claim refunds the fee; a successful Contessa block does not. An unsuccessful challenge against an assassination, or a caught Contessa bluff, can cost the target both influences.
  • Exchange draws two cards privately and returns two, without restoring lost influence. The last player with influence wins.

Online timing

There is no reaction countdown that can make you lose because of a slow connection. Character actions first offer a challenge-only window. Once the claim is accepted or proved, eligible surviving blockers get a separate counteraction window; blocks then have their own challenge window. Eligible players explicitly pass, challenge, or counteract. Competing responses are processed in server order; stale inputs from earlier windows are rejected with the latest state, while concurrent passes within the same window are accepted. A player cannot withdraw a pass within the same window. A block can be challenged but cannot itself be blocked.

Chosen online policy: the treasury is unlimited. The physical game includes 50 coins but does not specify a shortage rule. Eliminated players return their coins. For a pending successful Steal, the transfer happens before returning the eliminated target's remaining coins, following the rulebook's specific worked example (which conflicts with its general immediate coin-return wording).

The browser saves a private seat token in localStorage and automatically attempts to rejoin when Coup is reopened, including after closing the tab or browser. A per-tab cache preserves an already-open tab's seat, and legacy tab-only tokens are migrated after a successful resume. Opening the same seat in another tab takes over its connection without erasing the shared rejoin token.

In-progress game seats remain reserved when players disconnect, unless voted out. If a disconnected player's turn, reaction, proof, influence loss, or exchange is holding up play, every other connected, non-eliminated player may vote to forfeit them. Unanimous votes are required; spectators cannot vote and connected players cannot be kicked. Votes are canceled when the target reconnects or no longer needs to respond. A dropped voter's vote is removed, the electorate is recalculated, and an existing vote can become unanimous among the remaining voters. Zero voters never constitute approval. A successful kick uses normal forfeiture rules and revokes the seat's rejoin credentials.

Lobby and finished-game seats are cleaned up after two minutes offline. The existing 30-minute idle-room expiry still applies, and restarting the server discards room state. Expired credentials are cleared instead of repeatedly retrying a missing game.

Choosing Leave table intentionally forfeits immediately and clears that seat's saved credentials. If a player forfeits while their own action is pending, its unresolved effect is canceled, but any already-owed influence loss is still resolved unless the game has ended. These disconnect rules are online accommodations, not additions to the tabletop challenge rules.

The host can reopen the lobby after a game; everyone must ready up again. A table with no activity for 30 minutes expires, and its clients receive an expiration message.

Architecture

Area Responsibility
src/shared/ Typed intention/event protocol and public character/action catalog
src/server/game.ts Synchronous rules state machine, court deck, influence resolution, per-player projections
src/server/rooms.ts Independent room allocation, private seat credentials, reconnect, expiration, broadcasts
src/server/protocol.ts Strict Zod validation of every client intent
src/server/app.ts HTTP/static hosting, WebSocket transport, origin checks, rate limits, payload limits, heartbeat/backpressure
src/client/ Browser experience, WebGL presentation, and intention-only controls

The server owns coins, deck shuffling, roles, turns, challenges, targets, and card bounds. Clients submit intentions and target identifiers, never authoritative state or the identity they act as; that identity comes from their authenticated connection. Each connection receives a different projection that omits opponents' hidden roles and private exchange options. Action revisions reject duplicate and out-of-date game commands; movement is separately bounded and rate-limited without invalidating reaction windows.

Room state lives in memory. Independent games do not block each other, and movement broadcasts are small deltas rather than full game snapshots. The process caps rooms, concurrent connections, incoming payloads, request rates, and outbound buffering; abandoned resources are reclaimed. Server restarts intentionally discard active games. This is a single-node deployment, not a durable or horizontally distributed service: adding replicas requires room-affine routing and an explicit shared persistence/ownership design.

Development

npm test
npm run typecheck
npm run build

Tests cover the rules engine, private projections, room lifecycle, input validation, and real HTTP/WebSocket connections. The app contains no downloaded card artwork or rulebook text; its table and cards are original procedural artwork.

The seeded transport stress suite runs ten complete games by default across five connected rooms with 2-6 players each. To run a larger series and save per-game reports, normalized replay events, seeds, and trace digests:

COUP_STRESS_SEED=20260909 COUP_STRESS_GAMES=100 \
COUP_STRESS_ARTIFACT_DIR=test-results/transport \
npm test -- src/server/stress.test.ts

It exercises every action, challenges and blocks, private exchanges, reconnects, eliminated spectators, movement, concurrent passes, stale/duplicate rejection, room isolation, and per-recipient secrecy. Production rate limits remain enabled. Only the test module seeds game randomness; real credentials are neither mocked nor written to artifacts. Repeat the same base seed and preceding rematches to reproduce normalized traces. COUP_STRESS_TIMEOUT_MS defaults to 600000 and can be increased for longer runs.

This is functional multiplayer stress coverage, not a throughput benchmark: five rooms remain connected and interleave, but ordinary command transactions are serialized for reproducibility. Browser scenarios separately cover rendering and disconnect vote races.

Browser scenarios use Playwright:

npx playwright install chromium
npm run test:e2e

To use an already installed Chromium instead, set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH to its executable path.

The optional multi-browser soak scenarios play complete games with 2, 3, 4, 5, and 6 independent browser contexts. They drive the actual buttons, check each recipient's wire projections after every decision, reconnect mid-game, and attach a decision/state replay plus dealt/loss/winner screenshots. Policy decisions use a recorded seed; server shuffling remains random.

COUP_BROWSER_SOAK=1 npm run test:e2e -- --grep "multiplayer soak" --trace on --output test-results/soak

Regular browser scenarios also cover unanimous disconnect votes and changing voters, truthful/bluffing claims, disabled actions, coin updates, turn sounds/muting, WebGL loss/proof/win effects, reduced motion, and saved-seat restoration.

Coup is designed by Rikki Tahta and published by Indie Boards & Cards. This is an unofficial implementation, not affiliated with the designer or publisher.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages