feat: auto cash-out targets, 1% house edge, terminal aesthetic

Auto cash-out closes a position at exactly the chosen target rather than
the next tick's multiplier, and fires whenever the target is at or below
the crash point. This is the feature that makes the game playable over a
network, where manual timing is at the mercy of latency.

House edge drops from 2% to 1% across crash and scratch. Scratch prize
tables retuned so the published 99% RTP is exact.

Adds docs/API.md: the client uses no private endpoints, so anyone can
write a bot against the same API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-08-05 16:24:31 +00:00
parent dee3becd47
commit 48a9120fe4
14 changed files with 997 additions and 266 deletions

View File

@@ -1,19 +1,20 @@
/* Quantum Arcade — obsidian and indigo, dense ornament, and vivid colour
reserved strictly for the things that matter: the multiplier, the balance,
and the button that takes your money out of danger. */
/* Quantum Arcade — terminal aesthetic.
*
* Phosphor green on black, monospace everywhere, CRT scanlines and a faint
* flicker. Colour is rationed: amber for money at risk, magenta for the
* multiplier, red for the crash. Everything else is green on black, because
* a terminal that shouts about everything says nothing. */
:root {
--void: #05060d;
--obsidian: #0a0c18;
--indigo: #131a3a;
--indigo-hi: #1d2757;
--ink: #c8cbe6;
--muted: #6a719c;
--cyan: #38f2e4;
--magenta: #ff3ec8;
--gold: #ffc857;
--danger: #ff4d6d;
--ornament: #1a2350;
--black: #000000;
--panel: #030806;
--green: #00ff9c;
--green-dim: #0a7a52;
--green-ghost: #063a29;
--amber: #ffb000;
--magenta: #ff2e88;
--red: #ff3355;
--grid: #06181200;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
@@ -21,132 +22,178 @@
html, body {
margin: 0;
min-height: 100%;
background: radial-gradient(ellipse at 50% -10%, var(--indigo) 0%, var(--obsidian) 45%, var(--void) 100%);
color: var(--ink);
font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
background: var(--black);
color: var(--green);
font: 14px/1.5 ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
"Liberation Mono", monospace;
overscroll-behavior: none;
text-shadow: 0 0 6px #00ff9c40;
}
/* The ornament sits behind everything, busy but very low contrast. */
/* Faint hex-grid ornament behind everything. */
.filigree {
position: fixed; inset: 0;
width: 100%; height: 100%;
color: var(--ornament);
opacity: 0.55;
color: #0d2b21;
opacity: 0.5;
pointer-events: none;
z-index: 0;
}
/* CRT scanlines and a slow flicker over the whole page. */
body::after {
content: "";
position: fixed; inset: 0; z-index: 50;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
#00ff9c08 0px, #00ff9c08 1px,
transparent 1px, transparent 3px);
animation: flicker 5s infinite steps(60);
}
@keyframes flicker {
0%, 96%, 100% { opacity: 0.5; }
97% { opacity: 0.75; }
98% { opacity: 0.35; }
}
body > * { position: relative; z-index: 1; }
/* ---------- chrome ---------- */
.topbar {
display: flex; align-items: center; gap: 12px;
padding: 14px 16px calc(14px);
border-bottom: 1px solid #ffffff10;
backdrop-filter: blur(6px);
padding: 10px 14px;
border-bottom: 1px solid var(--green-ghost);
background: #00120c;
}
.brand {
font-weight: 700; letter-spacing: 0.22em; font-size: 13px;
color: var(--ink);
font-weight: 700; letter-spacing: 0.18em; font-size: 12px;
}
.brand span { color: var(--cyan); margin-left: 6px; }
.brand::before { content: "> "; color: var(--green-dim); }
.brand span { color: var(--amber); margin-left: 5px; }
.balance { margin-left: auto; text-align: right; line-height: 1.1; }
.balance { margin-left: auto; text-align: right; line-height: 1.15; }
.balance .label {
display: block; font-size: 9px; letter-spacing: 0.2em;
text-transform: uppercase; color: var(--muted);
display: block; font-size: 9px; letter-spacing: 0.18em;
text-transform: uppercase; color: var(--green-dim);
}
.balance .value {
font-variant-numeric: tabular-nums;
font-size: 18px; font-weight: 700; color: var(--cyan);
text-shadow: 0 0 18px #38f2e455;
font-size: 17px; font-weight: 700; color: var(--amber);
text-shadow: 0 0 14px #ffb00066;
}
.sound {
background: none; border: 1px solid #ffffff20; color: var(--muted);
width: 34px; height: 34px; border-radius: 50%; font-size: 15px;
background: none; border: 1px solid var(--green-ghost); color: var(--green-dim);
width: 32px; height: 32px; border-radius: 3px; font-size: 14px;
}
.sound.on { color: var(--cyan); border-color: var(--cyan); }
.sound.on { color: var(--green); border-color: var(--green); }
/* ---------- panels ---------- */
.panel {
max-width: 460px; margin: 0 auto; padding: 40px 22px;
}
.panel { max-width: 460px; margin: 0 auto; padding: 34px 20px; }
.center { text-align: center; }
h1 { font-size: 26px; margin: 0 0 10px; letter-spacing: 0.02em; }
h2 { font-size: 14px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--muted); margin: 0 0 10px; }
h1 {
font-size: 19px; margin: 0 0 10px; letter-spacing: 0.1em;
text-transform: uppercase;
}
h1::after {
content: "_";
animation: blink 1.1s steps(2) infinite;
color: var(--green);
}
@keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
.muted { color: var(--muted); }
.small { font-size: 12.5px; }
.fineprint { font-size: 11px; color: var(--muted); margin-top: 18px; word-break: break-all; }
h2 {
font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--green-dim); margin: 0 0 10px;
}
h2::before { content: "// "; }
.muted { color: var(--green-dim); }
.small { font-size: 12px; }
.fineprint {
font-size: 10.5px; color: var(--green-dim); margin-top: 16px;
word-break: break-all;
}
input {
width: 100%; padding: 13px 14px; margin: 8px 0;
background: #ffffff08; border: 1px solid #ffffff18; border-radius: 10px;
color: var(--ink); font-size: 16px; /* 16px stops iOS zooming on focus */
width: 100%; padding: 11px 12px; margin: 7px 0;
background: #00140e; border: 1px solid var(--green-ghost); border-radius: 3px;
color: var(--green); font-size: 16px; /* 16px stops iOS zooming on focus */
font-family: inherit;
}
input:focus { outline: none; border-color: var(--cyan); }
input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 10px #00ff9c33; }
input::placeholder { color: var(--green-ghost); }
button {
font: inherit; cursor: pointer; border-radius: 10px;
border: 1px solid #ffffff20; background: #ffffff0c; color: var(--ink);
padding: 11px 16px;
font: inherit; cursor: pointer; border-radius: 3px;
border: 1px solid var(--green-ghost); background: #00140e; color: var(--green);
padding: 10px 14px; letter-spacing: 0.06em;
}
button:active { transform: translateY(1px); }
.primary {
background: linear-gradient(135deg, var(--cyan), #21b6ff);
color: #04121a; border: none; font-weight: 700; letter-spacing: 0.04em;
box-shadow: 0 0 26px #38f2e444;
width: 100%; padding: 15px;
background: #00291c; color: var(--green);
border: 1px solid var(--green); font-weight: 700;
letter-spacing: 0.12em; text-transform: uppercase;
box-shadow: 0 0 18px #00ff9c33, inset 0 0 18px #00ff9c11;
width: 100%; padding: 14px;
}
.primary.big { font-size: 17px; padding: 18px; }
.primary.big { font-size: 15px; padding: 17px; }
/* Money at risk turns amber and pulses — the one element that demands a
decision looks different from everything else. */
.primary.cashout {
background: linear-gradient(135deg, var(--magenta), var(--gold));
color: #1a0512; box-shadow: 0 0 34px #ff3ec866;
animation: urge 900ms ease-in-out infinite;
background: #2a1c00; color: var(--amber); border-color: var(--amber);
box-shadow: 0 0 26px #ffb00055, inset 0 0 18px #ffb00011;
animation: urge 800ms ease-in-out infinite;
}
.primary:disabled { opacity: 0.4; box-shadow: none; animation: none; }
.primary:disabled { opacity: 0.35; box-shadow: none; animation: none; }
@keyframes urge {
0%, 100% { box-shadow: 0 0 26px #ff3ec855; }
50% { box-shadow: 0 0 40px #ff3ec8aa; }
0%, 100% { box-shadow: 0 0 18px #ffb00044, inset 0 0 14px #ffb00011; }
50% { box-shadow: 0 0 34px #ffb000aa, inset 0 0 22px #ffb00022; }
}
/* ---------- tabs ---------- */
.tabs {
display: flex; gap: 6px; padding: 12px 12px 0;
display: flex; gap: 4px; padding: 10px 10px 0;
max-width: 620px; margin: 0 auto;
}
.tab {
flex: 1; padding: 10px 4px; font-size: 12px; letter-spacing: 0.08em;
text-transform: uppercase; background: none; border: none; color: var(--muted);
border-bottom: 2px solid transparent; border-radius: 0;
flex: 1; padding: 9px 3px; font-size: 11px; letter-spacing: 0.1em;
text-transform: uppercase; background: none; border: none;
color: var(--green-dim); border-bottom: 1px solid var(--green-ghost);
border-radius: 0;
}
.tab.active {
color: var(--green); border-bottom-color: var(--green);
text-shadow: 0 0 10px #00ff9c88;
}
.tab.active { color: var(--cyan); border-bottom-color: var(--cyan); }
.view { max-width: 620px; margin: 0 auto; padding: 14px 14px 60px; }
.view { max-width: 620px; margin: 0 auto; padding: 12px 12px 60px; }
/* ---------- crash stage ---------- */
.gamepick { display: flex; gap: 6px; margin-bottom: 12px; }
.gamepick { display: flex; gap: 4px; margin-bottom: 10px; }
.gamepick button {
flex: 1; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
padding: 9px 4px; color: var(--muted);
flex: 1; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
padding: 8px 3px; color: var(--green-dim);
}
.gamepick button.on {
color: var(--green); border-color: var(--green); background: #00291c;
}
.gamepick button.on { color: var(--cyan); border-color: var(--cyan); background: #38f2e412; }
.stage {
position: relative; border-radius: 16px; overflow: hidden;
border: 1px solid #ffffff14;
background: linear-gradient(180deg, #070a16 0%, #05060d 100%);
position: relative; border-radius: 3px; overflow: hidden;
border: 1px solid var(--green-ghost);
background: #000603;
aspect-ratio: 4 / 3;
}
#canvas { width: 100%; height: 100%; display: block; }
@@ -156,100 +203,152 @@ button:active { transform: translateY(1px); }
align-items: center; justify-content: center; pointer-events: none;
}
.multiplier {
font-size: clamp(46px, 17vw, 88px); font-weight: 800;
font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
color: var(--cyan); text-shadow: 0 0 40px #38f2e466;
font-size: clamp(42px, 16vw, 82px); font-weight: 700;
font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
color: var(--magenta); text-shadow: 0 0 30px #ff2e8877;
}
.multiplier.crashed { color: var(--danger); text-shadow: 0 0 40px #ff4d6d66; }
.multiplier.won { color: var(--gold); text-shadow: 0 0 46px #ffc85777; }
.multiplier.crashed {
color: var(--red); text-shadow: 0 0 34px #ff335599;
animation: glitch 260ms steps(2) 3;
}
.multiplier.won {
color: var(--amber); text-shadow: 0 0 40px #ffb000aa;
}
@keyframes glitch {
0% { transform: translate(0, 0); }
25% { transform: translate(-3px, 1px); }
50% { transform: translate(3px, -1px); }
75% { transform: translate(-2px, -2px); }
100% { transform: translate(0, 0); }
}
.state {
font-size: 11px; letter-spacing: 0.24em; text-transform: uppercase;
color: var(--muted); margin-top: 6px;
font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--green-dim); margin-top: 5px;
}
.controls { margin-top: 14px; }
.stakerow { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.chip { flex: 1; font-variant-numeric: tabular-nums; padding: 12px 4px; }
.chip.on { border-color: var(--cyan); color: var(--cyan); background: #38f2e414; }
.unit { font-size: 11px; color: var(--muted); letter-spacing: 0.1em; }
.controls { margin-top: 12px; }
.stakerow { display: flex; align-items: center; gap: 4px; margin-bottom: 8px; }
.chip { flex: 1; font-variant-numeric: tabular-nums; padding: 11px 3px; font-size: 13px; }
.chip.on { border-color: var(--green); color: var(--green); background: #00291c; }
.unit { font-size: 10px; color: var(--green-dim); letter-spacing: 0.1em; }
.hint { min-height: 18px; margin-top: 8px; font-size: 12px; color: var(--muted); text-align: center; }
.hint.bad { color: var(--danger); }
.hint.good { color: var(--gold); }
/* Auto cash-out target row. */
.autorow {
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
border: 1px solid var(--green-ghost); border-radius: 3px; padding: 8px 10px;
}
.autorow label {
font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--green-dim); white-space: nowrap;
}
.autorow input {
margin: 0; padding: 6px 8px; text-align: right; width: 90px;
font-variant-numeric: tabular-nums;
}
.autorow .x { color: var(--green-dim); font-size: 13px; }
.autorow.armed { border-color: var(--amber); }
.autorow.armed label, .autorow.armed .x { color: var(--amber); }
.autorow.armed input { color: var(--amber); border-color: var(--amber); }
.players { margin-top: 14px; display: flex; flex-direction: column; gap: 4px; }
.hint {
min-height: 17px; margin-top: 7px; font-size: 11.5px;
color: var(--green-dim); text-align: center;
}
.hint.bad { color: var(--red); }
.hint.good { color: var(--amber); }
.players { margin-top: 12px; display: flex; flex-direction: column; gap: 3px; }
.player {
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
background: #ffffff06; border: 1px solid #ffffff10; border-radius: 9px;
font-size: 13px;
display: flex; align-items: center; gap: 8px; padding: 7px 10px;
background: #00120c; border: 1px solid var(--green-ghost); border-radius: 3px;
font-size: 12.5px;
}
.player .who { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.player .amt { font-variant-numeric: tabular-nums; color: var(--muted); }
.player.out .amt { color: var(--gold); }
.player .amt { font-variant-numeric: tabular-nums; color: var(--green-dim); }
.player.out { border-color: #4a3300; }
.player.out .amt { color: var(--amber); }
.proof { margin-top: 18px; }
.proof { margin-top: 16px; }
.proof summary {
cursor: pointer; font-size: 11px; letter-spacing: 0.16em;
text-transform: uppercase; color: var(--muted); padding: 8px 0;
cursor: pointer; font-size: 10px; letter-spacing: 0.16em;
text-transform: uppercase; color: var(--green-dim); padding: 7px 0;
}
.kv { display: flex; gap: 10px; font-size: 11px; padding: 4px 0; }
.kv span { color: var(--muted); min-width: 96px; }
.kv code { word-break: break-all; color: var(--ink); opacity: 0.8; }
.kv { display: flex; gap: 8px; font-size: 10.5px; padding: 3px 0; }
.kv span { color: var(--green-dim); min-width: 92px; }
.kv code { word-break: break-all; color: var(--green); opacity: 0.75; }
/* ---------- scratch ---------- */
/* ---------- cards / scratch ---------- */
.card {
background: #ffffff07; border: 1px solid #ffffff14;
border-radius: 14px; padding: 16px; margin-bottom: 14px;
background: #00120c; border: 1px solid var(--green-ghost);
border-radius: 3px; padding: 14px; margin-bottom: 12px;
}
.card h3 {
margin: 0 0 4px; font-size: 15px; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--green);
}
.card h3 { margin: 0 0 4px; font-size: 18px; }
.grid {
display: grid; gap: 8px; margin: 14px 0;
}
.grid { display: grid; gap: 6px; margin: 12px 0; }
.grid.c9 { grid-template-columns: repeat(3, 1fr); }
.grid.c6 { grid-template-columns: repeat(3, 1fr); }
.cell {
aspect-ratio: 1; display: grid; place-items: center;
font-size: 26px; border-radius: 10px;
background: linear-gradient(140deg, var(--indigo-hi), var(--indigo));
border: 1px solid #ffffff14;
transition: transform 160ms ease, background 260ms ease;
font-size: 24px; border-radius: 3px;
background: #001f16; border: 1px solid var(--green-ghost);
color: var(--green-dim);
transition: background 200ms ease, color 200ms ease;
}
.cell.revealed { background: #000603; color: var(--green); border-color: var(--green-ghost); }
.cell.hit {
border-color: var(--amber); color: var(--amber);
box-shadow: 0 0 16px #ffb00055;
}
.cell.revealed { background: #05060d; border-color: #ffffff22; }
.cell.hit { border-color: var(--gold); box-shadow: 0 0 18px #ffc85755; }
.odds { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 8px; }
.odds th, .odds td { text-align: left; padding: 6px 4px; border-bottom: 1px solid #ffffff10; }
.odds th { color: var(--muted); font-weight: 500; font-size: 10px;
letter-spacing: 0.12em; text-transform: uppercase; }
.odds td:last-child, .odds th:last-child { text-align: right; font-variant-numeric: tabular-nums; }
.rtp { color: var(--gold); font-weight: 700; }
.odds { width: 100%; border-collapse: collapse; font-size: 11.5px; margin-top: 8px; }
.odds th, .odds td {
text-align: left; padding: 5px 3px; border-bottom: 1px solid var(--green-ghost);
}
.odds th {
color: var(--green-dim); font-weight: 400; font-size: 9.5px;
letter-spacing: 0.14em; text-transform: uppercase;
}
.odds td:last-child, .odds th:last-child {
text-align: right; font-variant-numeric: tabular-nums;
}
.rtp { color: var(--amber); font-weight: 700; }
.result { text-align: center; padding: 10px 0; font-size: 16px; }
.result.win { color: var(--gold); font-weight: 700; }
.result { text-align: center; padding: 8px 0; font-size: 14px; }
.result.win {
color: var(--amber); font-weight: 700; letter-spacing: 0.08em;
text-transform: uppercase;
}
/* ---------- wallet ---------- */
.pubkey {
display: block; word-break: break-all; font-size: 11px;
background: #00000055; padding: 10px; border-radius: 8px; margin: 8px 0;
color: var(--muted);
display: block; word-break: break-all; font-size: 10.5px;
background: #000603; padding: 9px; border-radius: 3px; margin: 7px 0;
color: var(--green-dim); border: 1px solid var(--green-ghost);
}
.history { display: flex; flex-direction: column; gap: 3px; }
.history { display: flex; flex-direction: column; gap: 2px; }
.entry {
display: flex; gap: 10px; font-size: 12px; padding: 8px 10px;
background: #ffffff05; border-radius: 8px;
display: flex; gap: 8px; font-size: 11.5px; padding: 7px 9px;
background: #000603; border-radius: 3px;
}
.entry .kind { flex: 1; color: var(--muted); }
.entry .kind { flex: 1; color: var(--green-dim); }
.entry .delta { font-variant-numeric: tabular-nums; }
.entry .delta.pos { color: var(--cyan); }
.entry .delta.neg { color: var(--muted); }
.entry .after { font-variant-numeric: tabular-nums; color: var(--muted); min-width: 74px; text-align: right; }
.entry .delta.pos { color: var(--amber); }
.entry .delta.neg { color: var(--green-dim); }
.entry .after {
font-variant-numeric: tabular-nums; color: var(--green-dim);
min-width: 70px; text-align: right;
}
.verify-out { margin-top: 12px; font-size: 12px; }
.verify-out .ok { color: var(--cyan); font-weight: 700; }
.verify-out .bad { color: var(--danger); font-weight: 700; }
.verify-out { margin-top: 10px; font-size: 11.5px; }
.verify-out .ok { color: var(--green); font-weight: 700; }
.verify-out .bad { color: var(--red); font-weight: 700; }
.verify-out .kv code { font-size: 10px; }