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:
@@ -278,3 +278,67 @@ func itoa(v int64) string {
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
// An auto cash-out target must be accepted by the API and reflected in the
|
||||
// round, and an invalid one must be refused before any money moves.
|
||||
func TestAutoCashOutThroughTheAPI(t *testing.T) {
|
||||
c := newClient(t)
|
||||
c.signIn("autoplayer")
|
||||
start := c.fund(50_000_000)
|
||||
|
||||
var out map[string]any
|
||||
// A target at or below 1.00 is meaningless and must be rejected.
|
||||
code := c.do("POST", "/api/bet", map[string]any{
|
||||
"game": "rocket", "stake_msat": 1_000_000, "auto_cashout": 1.0,
|
||||
}, &out)
|
||||
if code == 200 {
|
||||
t.Fatal("a 1.00x auto cash-out target was accepted")
|
||||
}
|
||||
|
||||
// An absurd target must be refused rather than overflowing the conversion.
|
||||
code = c.do("POST", "/api/bet", map[string]any{
|
||||
"game": "rocket", "stake_msat": 1_000_000, "auto_cashout": 1e12,
|
||||
}, &out)
|
||||
if code == 200 {
|
||||
t.Fatal("an absurd auto cash-out target was accepted")
|
||||
}
|
||||
|
||||
// Neither rejection may have moved money.
|
||||
var bal struct {
|
||||
BalanceMsat int64 `json:"balance_msat"`
|
||||
}
|
||||
c.do("GET", "/api/balance", nil, &bal)
|
||||
if bal.BalanceMsat != start {
|
||||
t.Fatalf("balance = %d after rejected bets, want %d", bal.BalanceMsat, start)
|
||||
}
|
||||
|
||||
// A sensible target should be accepted during a betting window.
|
||||
deadline := time.Now().Add(90 * time.Second)
|
||||
placed := false
|
||||
for time.Now().Before(deadline) && !placed {
|
||||
var games struct {
|
||||
Rooms []struct {
|
||||
Game string `json:"game"`
|
||||
State string `json:"state"`
|
||||
} `json:"rooms"`
|
||||
}
|
||||
c.do("GET", "/api/games", nil, &games)
|
||||
for _, rm := range games.Rooms {
|
||||
if rm.Game == "rocket" && rm.State == "betting_open" {
|
||||
var res map[string]any
|
||||
if code := c.do("POST", "/api/bet", map[string]any{
|
||||
"game": "rocket", "stake_msat": 1_000_000,
|
||||
"auto_cashout": 2.5, "nickname": "autoplayer",
|
||||
}, &res); code == 200 {
|
||||
placed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !placed {
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
if !placed {
|
||||
t.Fatal("could not place an auto cash-out bet within 90s")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user