Files
casino/pkg/fixed/edge_test.go
drjones dee3becd47 fix(sim): cap crash point so extreme seeds cannot overflow or bankrupt
At u=1 the unsigned quotient exceeded int64 and wrapped negative, so the
rarest and most valuable outcome silently became an instant 1.00x loss.
At u=2 it produced a 2.1-billion-times payout the house could never
cover, which would have left settlement failing and the player unpaid.
The crash point is now capped at the largest multiplier the curve can
express, which is unreachable anyway since the round hits its tick
ceiling first.

FromInt now panics outside the Q32.32 integer range instead of wrapping
a positive input into a negative value.

Raises coverage to 88% overall; adds a Makefile with db-reset, since the
append-only ledger steadily consumes bridge headroom across test runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:52:50 +00:00

200 lines
4.8 KiB
Go

package fixed
import (
"math"
"testing"
)
// The simulation's verifiability depends on this arithmetic behaving
// identically everywhere, including at the extremes. These tests attack the
// boundaries.
func TestMulByZeroAndOne(t *testing.T) {
for _, v := range []int64{0, 1, -1, 1000, -1000, 1 << 20} {
a := FromInt(v)
if got := a.Mul(0); got != 0 {
t.Errorf("%d * 0 = %v, want 0", v, got)
}
if got := a.Mul(One); got != a {
t.Errorf("%d * 1 = %v, want %v", v, got, a)
}
}
}
func TestDivByOneAndSelf(t *testing.T) {
for _, v := range []int64{1, -1, 7, -7, 1000, 1 << 20} {
a := FromInt(v)
if got := a.Div(One); got != a {
t.Errorf("%d / 1 = %v, want %v", v, got, a)
}
if got := a.Div(a); got != One {
t.Errorf("%d / %d = %v, want 1", v, v, got)
}
}
}
func TestDivByZeroPanics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("division by zero did not panic")
}
}()
_ = One.Div(0)
}
func TestSqrtOfNegativePanics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("sqrt of a negative did not panic")
}
}()
_ = Sqrt(FromInt(-1))
}
// Multiplication must stay associative-ish and exact for representable values,
// which is what keeps a replayed round identical to the original.
func TestMulIsExactForFractions(t *testing.T) {
cases := []struct {
a, b, want F
}{
{One / 2, One / 2, One / 4},
{One / 4, One / 4, One / 16},
{One / 2, One / 4, One / 8},
{One * 3 / 2, One * 2, One * 3},
}
for _, c := range cases {
if got := c.a.Mul(c.b); got != c.want {
t.Errorf("%v * %v = %v, want %v", c.a, c.b, got, c.want)
}
}
}
// Round-tripping a value through multiply and divide must return it exactly
// for powers of two, where no precision can be lost.
func TestMulDivRoundTripOnPowersOfTwo(t *testing.T) {
for shift := 0; shift < 20; shift++ {
v := FromInt(1 << shift)
for _, by := range []F{One * 2, One * 4, One * 8} {
if got := v.Mul(by).Div(by); got != v {
t.Errorf("2^%d round trip through %v gave %v, want %v", shift, by, got, v)
}
}
}
}
func TestSqrtIsMonotonic(t *testing.T) {
prev := Sqrt(0)
for i := int64(1); i < 5000; i++ {
cur := Sqrt(FromInt(i))
if cur < prev {
t.Fatalf("Sqrt decreased at %d: %v -> %v", i, prev, cur)
}
prev = cur
}
}
// Sqrt must never overshoot: its square must not exceed the input.
func TestSqrtNeverOvershoots(t *testing.T) {
for i := int64(0); i < 20000; i++ {
a := FromInt(i)
r := Sqrt(a)
if r.Mul(r) > a {
t.Fatalf("Sqrt(%d) = %v squares to %v, which exceeds %v", i, r, r.Mul(r), a)
}
}
}
func TestSqrtOfLargeValues(t *testing.T) {
// Values in the range the crash curve actually produces, up to the
// representable maximum.
for _, v := range []int64{1_000_000, 12_960_000, 100_000_000, MaxInt} {
a := FromInt(v)
r := Sqrt(a)
if r <= 0 {
t.Fatalf("Sqrt(%d) = %v, want positive", v, r)
}
if r.Mul(r) > a {
t.Fatalf("Sqrt(%d) overshoots", v)
}
}
}
func TestIntTruncatesTowardNegativeInfinity(t *testing.T) {
cases := []struct {
in F
want int64
}{
{One, 1},
{One + One/2, 1},
{One*2 - 1, 1},
{0, 0},
{-One, -1},
}
for _, c := range cases {
if got := c.in.Int(); got != c.want {
t.Errorf("(%v).Int() = %d, want %d", c.in, got, c.want)
}
}
}
func TestFromIntPanicsOutsideRange(t *testing.T) {
for _, v := range []int64{MaxInt + 1, MinInt - 1, 4_000_000_000, -4_000_000_000} {
func() {
defer func() {
if recover() == nil {
t.Errorf("FromInt(%d) did not panic", v)
}
}()
_ = FromInt(v)
}()
}
// The boundaries themselves must be accepted.
_ = FromInt(MaxInt)
_ = FromInt(MinInt)
}
func TestStringNeverPanicsAcrossRange(t *testing.T) {
values := []F{
0, 1, -1, One, -One, One / 3, math.MaxInt64, math.MinInt64 + 1,
FromInt(4_000_000),
}
for _, v := range values {
if s := v.String(); s == "" {
t.Errorf("String() of %d returned empty", int64(v))
}
}
}
// Addition and subtraction are plain integer ops, but the inverse property is
// what payout arithmetic relies on.
func TestAddSubAreInverse(t *testing.T) {
for _, a := range []F{0, One, -One, One * 12345, One / 7} {
for _, b := range []F{0, One, -One, One * 999} {
if got := a.Add(b).Sub(b); got != a {
t.Errorf("(%v + %v) - %v = %v, want %v", a, b, b, got, a)
}
}
}
}
// Determinism check: the same operations in the same order must produce
// bit-identical results every time, which is the whole premise of replay.
func TestOperationsAreBitStable(t *testing.T) {
compute := func() F {
acc := One
for i := int64(1); i < 500; i++ {
acc = acc.Mul(One + One/F(i+1))
acc = acc.Div(One + One/F(i+2))
acc = acc.Add(FromInt(i % 3))
acc = Sqrt(acc)
}
return acc
}
first := compute()
for i := 0; i < 200; i++ {
if got := compute(); got != first {
t.Fatalf("run %d diverged: %v != %v", i, got, first)
}
}
}