feat: playable arcade — rooms, identity, client, deployment

Round length is now bounded: the multiplier follows a hyperbolic curve
diverging at 60s, replacing an exponential one where a 275x crash point
produced a two-and-a-half minute round.

Fixes seed reveal, which silently failed every round because pgx cannot
encode a fixed-size byte array as bytea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-08-05 15:34:52 +00:00
parent 41c1bb2fdf
commit 2a2a1db8de
17 changed files with 2885 additions and 25 deletions

View File

@@ -95,3 +95,33 @@ func TestTicksToMultiplierRoundTrips(t *testing.T) {
}
}
}
// No round may outlast the ceiling, however extreme the crash point.
func TestRoundLengthIsBounded(t *testing.T) {
if got := MultiplierAt(RoundTicks); got != MaxMultiplier() {
t.Fatalf("curve past the ceiling = %v, want %v", got, MaxMultiplier())
}
// Even the most extreme crash point settles within the ceiling.
worst := fixed.FromInt(4_000_000_000)
if tick := TicksToMultiplier(worst); tick > RoundTicks {
t.Fatalf("extreme crash point needs %d ticks, ceiling is %d", tick, RoundTicks)
}
}
// Timings that matter for how the game feels.
func TestCurveTimings(t *testing.T) {
for _, c := range []struct {
multiplier int64
maxSeconds float64
}{
{2, 20}, // the common case should arrive quickly
{10, 45},
{100, 56},
} {
tick := TicksToMultiplier(fixed.FromInt(c.multiplier))
secs := float64(tick) / TickHz
if secs > c.maxSeconds {
t.Errorf("%dx takes %.1fs, want under %.0fs", c.multiplier, secs, c.maxSeconds)
}
}
}