Files
casino/README.md
drjones 3bdb518f9c feat: refund abandoned rounds; full-journey and capacity tests
Fixes the money bug flagged earlier. When an instance died mid-round its
players had already been debited, so their stakes sat with the house:
balanced books, quietly robbed players. Every instance now sweeps for
unresolved rounds and refunds them.

Such a round is marked void, not settled. The schema caught this: the
reveal_is_complete constraint requires a settled round to publish its
seed, and an abandoned round has no outcome to reveal. Void is a distinct
state with its own column and a check that the two are exclusive.
Claiming happens before money moves, so concurrent reconcilers on
different instances refund exactly once.

Adds TestFullPlayerJourney: sign-in with no account, fund, scratch, bet
with an auto target, settle, verify the round independently, check the
ledger history is continuous, transfer to a friend, and confirm the books
still sum to zero. It asserts against the ledger rather than the API's
own summary.

Adds cmd/loadtest. One instance on 4 cores held 25,000 concurrent
websocket connections with zero failures at 586MB RSS, about 26KB per
connection, with the load generator competing for the same CPU.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:18:29 +00:00

170 lines
6.2 KiB
Markdown

# Quantum Arcade
A private physics arcade for one Linux box and the people on your network.
Drop-in crash rounds, instant scratch tickets, a real double-entry ledger, and
outcomes any player can verify on their own phone.
## What it is
Three shared crash games — a rocket fighting gravity, a decaying orbit, and a
stacking tower — plus instant scratch tickets to play between rounds. Set an
auto cash-out target before the round and it closes at exactly your number, or
ride it and tap out by hand. Identity is a keypair your browser generates;
there is no account, no email, and no password.
The house edge is **1%** on everything. That is better than essentially
anything commercial, and it is deliberate: this is a game among friends, not a
revenue stream.
Everything is API-first — the browser client uses the same endpoints anyone
else can. Write a bot in twenty lines; see [docs/API.md](docs/API.md).
Everything runs on one machine: one Go binary with the client embedded,
PostgreSQL, and Redis.
## Running it
```bash
docker compose up -d
```
Then open `http://<your-box>:8080` from any phone on the network.
To play with test funds before Lightning is wired up:
```bash
ARCADE_DEV_FAUCET=1 docker compose up -d
```
The faucet mints through the same ledger path a real deposit uses, so the code
under test is the production code. Leave it off otherwise.
## Development
```bash
docker compose up -d postgres
go test ./...
go run ./cmd/arcade
```
End-to-end tests need a live server:
```bash
ARCADE_DEV_FAUCET=1 go run ./cmd/arcade &
ARCADE_E2E=http://localhost:8080 go test ./cmd/arcade/ -v
```
## How fairness works
Before betting opens, the server generates a random seed and publishes
`SHA-256(seed)`. It is now committed and cannot change its mind.
The client seed is built from the public keys of everyone who joined the round.
The operator does not choose who plays, so it cannot steer the outcome even
knowing its own seed.
The crash point is `HMAC-SHA256(serverSeed, clientSeed || nonce)`, run through
the simulation. After settlement the seed is published, and the Verify tab
recomputes the whole chain in your browser — it asks the server only for the
published values, never for a verdict.
Scratch tickets use the identical pipeline, and their odds tables are generated
from the same data structure that produces outcomes, so the published odds
cannot drift from reality. A test asserts observed frequencies and empirical
return against the published figures across two million plays.
## Architecture
One binary, with enforced internal boundaries:
| Package | Responsibility |
|---|---|
| `pkg/fixed` | Q32.32 fixed-point arithmetic; no floats, so results are identical everywhere |
| `pkg/sim` | Deterministic RNG and the crash curve |
| `pkg/fair` | Commit-reveal protocol and verification proofs |
| `pkg/ledger` | Append-only double-entry accounting |
| `pkg/scratch` | Scratch tickets and their published odds |
| `pkg/identity` | Keypair sign-in via signed challenge |
| `pkg/room` | Round lifecycle, auto cash-out, live broadcast |
Nine services on one machine would buy latency and 3am debugging, so this is
one process. Modules talk through interfaces only; extracting one into its own
service later is a transport change, not a rewrite.
### Ledger invariants
Enforced in the application and again by database constraints and triggers:
- every transaction's postings sum to exactly zero
- no account may go negative, except the Lightning bridge, whose negative
balance is by definition what is owed to players
- rows are never updated or deleted; corrections are compensating entries
`GET /api/health` sums every account. It must return zero. Anything else means
the books are corrupt.
### Round timing
The multiplier follows `m(t) = 1/(1 - t/T)²`, which diverges at exactly 60
seconds. No round can run longer, however extreme the crash point, and the
climb visibly accelerates as it goes — which is where the tension comes from.
## Testing
```bash
make test # unit and integration
make test-race # under the race detector
make cover # coverage per package
make db-reset # wipe the ledger; it is append-only and accumulates
```
Measured capacity: **25,000 concurrent connections on one 4-core instance**,
zero failures, 586MB RSS. Bet throughput is the real ceiling at ~230/sec —
see [docs/SCALING.md](docs/SCALING.md).
Coverage sits around 85%, and the tests found and pinned three real money
bugs: a posting set that minted 18 quintillion millisatoshis by wrapping the
zero-sum check, a crash point that overflowed negative at the rarest seed, and
scratch tables that advertised 98% while paying 56%.
## Scaling
The app is stateless: clone the VM and boot it. An instance generates its own
identity, finds its peers through Redis, and campaigns for the games it will
drive. Exactly one instance runs a given game's rounds; the rest relay its
frames and forward bets to it.
An instance dying is not a special case — its lease expires and a survivor
takes over. Measured at six seconds, unattended, after a `kill -9`.
Clone the **app** VM only. PostgreSQL and Redis stay on one shared machine;
cloning those gives every instance its own ledger and they share nothing.
Full topology in [docs/SCALING.md](docs/SCALING.md).
## Status
Built and tested:
- fixed-point deterministic core, ledger, commit-reveal fairness
- three crash games with live multiplayer rounds
- two scratch tickets with verified-honest odds
- keypair identity, peer-to-peer transfers, transaction history
- auto cash-out targets that pay your exact number
- in-browser verifier
- a documented public API, good enough to write bots against
Not yet built:
- **Lightning deposits and withdrawals.** The bridge account and ledger paths
exist; the node integration does not. The dev faucet stands in for now.
- Tournaments and scheduled events
- Operator dashboard
- In-memory bet reservation, which is what would lift the ~230 bets/sec ceiling
## Scope
This is built to run on a private network among people who know each other.
It is not hardened for, and should not be exposed to, the public internet.
Doing so would make it a public real-money gambling service, which carries
licensing, KYC, and AML obligations this codebase does not address.