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:
@@ -50,6 +50,12 @@ type Bet struct {
|
||||
StakeMsat int64
|
||||
CashedOutAt fixed.F // zero until they cash out
|
||||
PayoutMsat int64
|
||||
|
||||
// AutoCashOutAt is an optional target set before the round starts. When
|
||||
// the multiplier reaches it the position closes automatically at exactly
|
||||
// that value — not at whatever the next tick happens to show — so the
|
||||
// player gets the number they chose. Zero means no target.
|
||||
AutoCashOutAt fixed.F
|
||||
}
|
||||
|
||||
// Snapshot is what clients render. It carries the seed inputs so a client can
|
||||
@@ -75,6 +81,8 @@ type Player struct {
|
||||
StakeMsat int64 `json:"stake_msat"`
|
||||
CashedOut string `json:"cashed_out,omitempty"`
|
||||
PayoutMsat int64 `json:"payout_msat"`
|
||||
// Auto is true when the position closed on its own target rather than a tap.
|
||||
Auto bool `json:"auto,omitempty"`
|
||||
}
|
||||
|
||||
// Room runs one game's round loop.
|
||||
@@ -188,6 +196,10 @@ func (r *Room) step(ctx context.Context) error {
|
||||
r.mu.Lock()
|
||||
r.tick++
|
||||
reached := sim.MultiplierAt(r.tick)
|
||||
// Close any positions whose target has been met. This happens before
|
||||
// the crash check so a target at or below the crash point always pays,
|
||||
// regardless of where tick boundaries happen to fall.
|
||||
r.triggerAutoCashOutsLocked(reached)
|
||||
// A crash point beyond what the curve expresses would otherwise never
|
||||
// be reached, so the tick ceiling also ends the round.
|
||||
crashed := reached >= r.crashPoint || r.tick >= sim.RoundTicks
|
||||
@@ -321,12 +333,35 @@ func (r *Room) settle(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// triggerAutoCashOutsLocked closes positions whose target the multiplier has
|
||||
// reached. Callers must hold the lock.
|
||||
//
|
||||
// A target above the crash point never fires: the round is already over at
|
||||
// that value. A target at or below it always fires, at exactly the target.
|
||||
func (r *Room) triggerAutoCashOutsLocked(reached fixed.F) {
|
||||
for _, b := range r.bets {
|
||||
if b.CashedOutAt != 0 || b.AutoCashOutAt == 0 {
|
||||
continue
|
||||
}
|
||||
if b.AutoCashOutAt > r.crashPoint {
|
||||
continue // the round ends before this target is reached
|
||||
}
|
||||
if reached >= b.AutoCashOutAt {
|
||||
b.CashedOutAt = b.AutoCashOutAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PlaceBet takes a stake during the betting window. The stake moves to the
|
||||
// house immediately, so a player can never bet money they do not have.
|
||||
func (r *Room) PlaceBet(ctx context.Context, accountID int64, pubkey []byte, nickname string, stakeMsat int64) error {
|
||||
func (r *Room) PlaceBet(ctx context.Context, accountID int64, pubkey []byte, nickname string, stakeMsat int64, autoCashOutAt fixed.F) error {
|
||||
if stakeMsat <= 0 {
|
||||
return ledger.ErrNonPositiveAmount
|
||||
}
|
||||
// A target at or below 1.0 would close instantly for no gain.
|
||||
if autoCashOutAt != 0 && autoCashOutAt <= fixed.One {
|
||||
return fmt.Errorf("auto cash-out target must be above 1.00")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
if r.state != StateBetting {
|
||||
@@ -367,6 +402,7 @@ func (r *Room) PlaceBet(ctx context.Context, accountID int64, pubkey []byte, nic
|
||||
r.bets[accountID] = &Bet{
|
||||
AccountID: accountID, Pubkey: pubkey,
|
||||
Nickname: nickname, StakeMsat: stakeMsat,
|
||||
AutoCashOutAt: autoCashOutAt,
|
||||
}
|
||||
r.order = append(r.order, pubkey)
|
||||
r.mu.Unlock()
|
||||
@@ -423,6 +459,7 @@ func (r *Room) Snapshot() Snapshot {
|
||||
}
|
||||
if b.CashedOutAt != 0 {
|
||||
p.CashedOut = b.CashedOutAt.String()
|
||||
p.Auto = b.AutoCashOutAt != 0 && b.CashedOutAt == b.AutoCashOutAt
|
||||
}
|
||||
players = append(players, p)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user