Cuttle¶
Cuttle is a strategic card-combat example: players race to 21 points while
playing point cards, permanents, one-offs, counters, and scuttles. The canonical
rules live in the framework-agnostic examples/cuttle/CuttleGame.ts, whose
state is a plain mutable object and whose public interface includes
getValidActions(player) and getObservation(player).
Run it¶
npm run cuttle
node --loader ./test/ts-esm-loader.js examples/cuttle/cli.js --variant standard
node --loader ./test/ts-esm-loader.js examples/cuttle/cli.js --variant cutthroat
npm run cuttle:server # authoritative room server
npm run cuttle:client # client (use another terminal)
For the browser UI, open examples/cuttle/web/index.html with a static server.
For serverless CRDT play, run a relay (npm run relay, on the configured port)
and use:
npm run cuttle:crdt:host
npm run cuttle:crdt:client
Three integration modes¶
The local CLI calls CuttleGame.action(player, command) after checking the
valid-action list. The authoritative server binds cuttle:* actions to a
RoomAuthoritativeServer, projects per-player state, and serializes room
dispatches. The CRDT client writes a sanitized snapshot under doc.cuttle and
uses ConsensusCore for sync. Those modes share rules but are not identical
state backends.
The CRDT mode encrypts private hands for their recipients, stores deck
commitments, and uses expectedTurnNumber to reject stale actions. Opponents
receive observations and counts, not another player's plaintext hand. Cuttle's
encrypted mode deliberately has an honest dealer: commitments detect card
substitution, but do not prove a fair shuffle or prevent a dealer from biasing
the deal. Generic network E2E is not automatically enabled by the relay.
Variants and tests¶
Classic is the two-player default. Standard changes several card effects; cutthroat adds three-player rules and Jokers. The encrypted CRDT scope is classic two-player only. Relevant checks are:
npm run test:cuttle:sync
npm run test:cuttle:crypto
npm run test:cuttle:hardening