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>
170 lines
6.2 KiB
Markdown
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.
|