aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/machine.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 15:35:52 -0400
committerhistoria <[not public]>2026-09-15 15:35:52 -0400
commit0b0b25222b3131ada79ff78c28eb8bf07d977deb (patch)
tree2ceaef5203595044f66a8f00064cd8e017f8f48d /internal/casino/machine.go
parent396c3facb8287b9a34076cae8f3d7b300d366d99 (diff)
downloadthehouseoficarus-0b0b25222b3131ada79ff78c28eb8bf07d977deb.tar.gz
feat: single-owner game state, transactional lifecycle, durable money on server stop
Diffstat (limited to 'internal/casino/machine.go')
-rw-r--r--internal/casino/machine.go48
1 files changed, 43 insertions, 5 deletions
diff --git a/internal/casino/machine.go b/internal/casino/machine.go
index 64dcfd4..6a7b7aa 100644
--- a/internal/casino/machine.go
+++ b/internal/casino/machine.go
@@ -2,6 +2,7 @@ package casino
import (
"fmt"
+ "math"
"math/rand"
"sync"
)
@@ -100,8 +101,8 @@ func (m *Machine) start(name string, wallet Wallet) []Event {
}
func (m *Machine) beginSpinLocked(name string, amount int, wallet Wallet, publicMessage, privateMessage string) []Event {
- if _, ok := wallet.Wager(amount); !ok {
- return m.private("You don't have enough chips and credits for that bet.")
+ if _, err := wallet.Wager(amount); err != nil {
+ return m.private(walletErrorMessage(err, "You don't have enough chips and credits for that bet."))
}
m.bet = amount
m.spinBet = amount
@@ -203,9 +204,9 @@ func (m *Machine) autoBetTick() []Event {
m.autoBet = false
return m.private("Autobet stopped: no wager is set.")
}
- if _, ok := m.player.Wallet.Wager(m.bet); !ok {
+ if _, err := m.player.Wallet.Wager(m.bet); err != nil {
m.autoBet = false
- return m.private("Autobet stopped: you don't have enough chips and credits for that bet.")
+ return m.private(walletErrorMessage(err, "Autobet stopped: you don't have enough chips and credits for that bet."))
}
m.spinBet = m.bet
m.spin = generateSlotSpinWithProfile(m.rng, m.spinBet, m.profile)
@@ -220,7 +221,12 @@ func (m *Machine) autoBetTick() []Event {
func (m *Machine) finishSpin() []Event {
events := []Event{}
result := m.spin.Payout + m.payoutRemainder
- credits := int(result)
+ // Checked conversion: a runaway profile or overflow must clamp at the
+ // economy cap instead of wrapping through a machine-sized int.
+ if result > MaxCredits || result != result || result < 0 {
+ result = MaxCredits
+ }
+ credits := int(math.Floor(result))
m.payoutRemainder = result - float64(credits)
if credits > 0 && m.player.Wallet != nil {
m.player.Wallet.PayCredits(credits)
@@ -380,3 +386,35 @@ func (m *Machine) privateMenu(prefix string) []Event {
func (m *Machine) menuEvents(prefix string) []Event {
return m.privateMenu(prefix)
}
+
+// refundAll refunds any committed stake at graceful shutdown. Locked.
+func (m *Machine) refundAll() []Event {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ if m.player == nil || m.player.Wallet == nil || m.spinBet <= 0 {
+ return nil
+ }
+ name, refund := m.player.Name, m.spinBet
+ wallet := m.player.Wallet
+ m.spinBet = 0
+ m.bet = 0
+ if err := wallet.PayCredits(refund); err != nil {
+ return []Event{{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ PrivateMessage: "Shutdown refund could not be persisted; the stake is recorded for recovery.",
+ Color: "casino_action", Public: false}}
+ }
+ return []Event{{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ Message: fmt.Sprintf("%s is refunded %d credits as the casino closes.", name, refund),
+ PrivateMessage: fmt.Sprintf("The house refunds your %d credit stake for the shutdown.", refund),
+ Color: "casino_action", Public: true}}
+}
+
+// idle reports whether no player is seated: only an idle machine may have
+// its live rules replaced by new room configuration.
+func (m *Machine) idle() bool {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ return m.player == nil
+}