aboutsummaryrefslogtreecommitdiff
path: root/internal/casino
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino')
-rw-r--r--internal/casino/correct_audit_fixes_test.go23
-rw-r--r--internal/casino/craps.go14
-rw-r--r--internal/casino/machine.go31
-rw-r--r--internal/casino/machine_test.go14
-rw-r--r--internal/casino/video_poker.go4
5 files changed, 80 insertions, 6 deletions
diff --git a/internal/casino/correct_audit_fixes_test.go b/internal/casino/correct_audit_fixes_test.go
new file mode 100644
index 0000000..e5e3bd3
--- /dev/null
+++ b/internal/casino/correct_audit_fixes_test.go
@@ -0,0 +1,23 @@
+package casino
+
+import (
+ "math/rand"
+ "testing"
+)
+
+// Regression: when the shooter leaves and others remain, the next shooter must
+// start a fresh come-out rather than inheriting the previous point and
+// made/seen bonus progress (which blocked line and bonus bets).
+func TestCrapsShooterChangeResetsHand(t *testing.T) {
+ tb := newCrapsTable(1, TableConfig{ID: "craps", Game: GameCraps, BettingTicks: 2}, rand.New(rand.NewSource(7)))
+ tb.join("alice")
+ tb.join("bob")
+ tb.point = 6
+ tb.rolled = true
+ tb.made = map[int]bool{4: true}
+ tb.seen = map[int]bool{4: true}
+ tb.remove("alice")
+ if tb.point != 0 || tb.rolled || len(tb.made) != 0 || len(tb.seen) != 0 {
+ t.Fatalf("new shooter inherited hand: point=%d rolled=%v made=%v seen=%v", tb.point, tb.rolled, tb.made, tb.seen)
+ }
+}
diff --git a/internal/casino/craps.go b/internal/casino/craps.go
index 73c67c4..0abf24b 100644
--- a/internal/casino/craps.go
+++ b/internal/casino/craps.go
@@ -133,7 +133,8 @@ func (t *crapsTable) join(name string) []Event {
return append([]Event{e, t.menu(name)}, timing...)
}
func (t *crapsTable) remove(name string) {
- if len(t.queue) > 0 && t.queue[0] == name {
+ wasShooter := len(t.queue) > 0 && t.queue[0] == name
+ if wasShooter {
// The queued roll belongs to the shooter; a departing shooter cancels it.
t.pendingRollTicks = 0
}
@@ -152,6 +153,15 @@ func (t *crapsTable) remove(name string) {
t.seen = map[int]bool{}
t.phase = phaseWaiting
t.window.close()
+ } else if wasShooter {
+ // The next shooter starts a fresh come-out. Without this he inherits
+ // the previous shooter's point (blocking pass/dontpass and bonus
+ // bets) and made/seen bonus progress. The shared countdown is left
+ // alone: it belongs to the table, not the shooter.
+ t.point = 0
+ t.rolled = false
+ t.made = map[int]bool{}
+ t.seen = map[int]bool{}
}
}
func (t *crapsTable) leave(name string) []Event {
@@ -245,7 +255,7 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event {
}
limit := t.Config.Craps.oddsLimit(b.number, against)
if b.odds.Total > 0 {
- continue
+ return reject("Odds already placed on that bet.")
}
requested := amount
amount = roundUpTo(amount, payoutDen)
diff --git a/internal/casino/machine.go b/internal/casino/machine.go
index 0733384..93ab8aa 100644
--- a/internal/casino/machine.go
+++ b/internal/casino/machine.go
@@ -357,7 +357,9 @@ func (m *Machine) finishSpin() []Event {
m.phase = machineSpinning
m.spin = generateSlotSpinWithProfile(m.rng, m.spinBet, m.profile)
m.reel, m.clock, m.suspense = 0, 0, false
- events = append(events, m.public(fmt.Sprintf("%d free spins remain.", m.freeSpins))...)
+ if m.freeSpins > 0 {
+ events = append(events, m.public(fmt.Sprintf("%d free spins remain.", m.freeSpins))...)
+ }
} else {
m.inFreeSpins = false
if !payFailed && m.player.Wallet != nil {
@@ -382,9 +384,34 @@ func (m *Machine) leave(name string) []Event {
if m.phase != machineReady {
return m.private("You're in the middle of a spin!")
}
+ bank := int(m.featureBank)
+ wallet := m.player.Wallet
+ stakeFailed := m.stakeFailed
m.resetSeat()
- return []Event{{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventLeft, Player: name,
+ events := []Event{{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventLeft, Player: name,
Message: fmt.Sprintf("%s stands up from the slots machine.", name), PrivateMessage: "You stand up from the slot machine.", PrivateTo: name, Color: "casino_action", Public: true}}
+ // Banked free-spin winnings must not vanish when the player stands up:
+ // pay them like markDisconnected/refundAll do, or report the forfeiture.
+ if bank > 0 && wallet != nil {
+ switch {
+ case stakeFailed:
+ events = append(events, Event{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ PrivateMessage: fmt.Sprintf("Your %d credits of banked free-spin winnings were forfeited because the round could not be persisted.", bank),
+ Color: "casino_action", Public: false})
+ case wallet.PayCredits(bank) != nil:
+ events = append(events, Event{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ PrivateMessage: fmt.Sprintf("Your %d credits of free-spin winnings could not be persisted; they were forfeited.", bank),
+ Color: "casino_action", Public: false})
+ default:
+ events = append(events, Event{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ PrivateMessage: fmt.Sprintf("The house settles your %d banked free-spin winnings.", bank),
+ Color: "casino_action", Public: false})
+ }
+ }
+ return events
}
// resetSeat frees the machine, discarding any in-flight spin and autobet
diff --git a/internal/casino/machine_test.go b/internal/casino/machine_test.go
index 1e8cdad..8942936 100644
--- a/internal/casino/machine_test.go
+++ b/internal/casino/machine_test.go
@@ -14,6 +14,20 @@ func TestBetRangeTextFormatsUnlimitedMax(t *testing.T) {
}
}
+// Regression: standing up while a feature was banked silently dropped the
+// credits; leave must settle them like markDisconnected does.
+func TestMachineLeavePaysBankedFeature(t *testing.T) {
+ m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, rand.New(rand.NewSource(7)))
+ w := &testWallet{}
+ m.player = &Participant{Name: "alice", Connected: true, Wallet: w}
+ m.featureBank = 25
+ m.phase = machineReady
+ _ = m.leave("alice")
+ if w.paid != 25 {
+ t.Fatalf("banked feature not paid on leave: paid = %d, want 25", w.paid)
+ }
+}
+
func TestMachineSetBetUnsetDefaultMaxMessage(t *testing.T) {
m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots, MinBet: 1}, rand.New(rand.NewSource(7)))
m.player = &Participant{Name: "alice", Connected: true}
diff --git a/internal/casino/video_poker.go b/internal/casino/video_poker.go
index 4cb9883..98bfda1 100644
--- a/internal/casino/video_poker.go
+++ b/internal/casino/video_poker.go
@@ -279,7 +279,7 @@ func (t *videoPokerTable) bet(name, spot string, amount int, w Wallet) []Event {
if wallet, ok := w.(PokerWallet); ok {
banks, err := wallet.LoadPokerBanks(t.RoomID, t.Config.ID)
if err != nil {
- return []Event{t.private(name, "Machine bank unavailable; try again later: "+err.Error())}
+ return []Event{t.private(name, "Machine bank unavailable; try again later.")}
}
if banks != nil {
t.banks = banks
@@ -308,7 +308,7 @@ func (t *videoPokerTable) bet(name, spot string, amount int, w Wallet) []Event {
}
stake, err := w.Wager(p.cost())
if err != nil {
- return []Event{t.private(name, "The wager could not be placed: "+err.Error())}
+ return []Event{t.private(name, walletErrorMessage(err, "Not enough chips and credits for that wager."))}
}
p.wallet, p.wager, p.lastBet = w, stake, p.loaded
p.bankKey = t.multiplierKey(p)