diff options
Diffstat (limited to 'internal/casino')
| -rw-r--r-- | internal/casino/baccarat.go | 9 | ||||
| -rw-r--r-- | internal/casino/baccarat_test.go | 7 | ||||
| -rw-r--r-- | internal/casino/blackjack.go | 23 | ||||
| -rw-r--r-- | internal/casino/blackjack_test.go | 21 | ||||
| -rw-r--r-- | internal/casino/machine.go | 3 | ||||
| -rw-r--r-- | internal/casino/machine_test.go | 18 | ||||
| -rw-r--r-- | internal/casino/types.go | 18 |
7 files changed, 85 insertions, 14 deletions
diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go index 6a72caa..800b5e6 100644 --- a/internal/casino/baccarat.go +++ b/internal/casino/baccarat.go @@ -86,10 +86,13 @@ func (t *baccaratTable) join(name string) []Event { func (t *baccaratTable) leave(name string) []Event { t.mu.Lock() defer t.mu.Unlock() - if !t.players.has(name) { + p := t.players.get(name) + if p == nil { return nil } - if t.phase != baccaratWaiting { + // A wager is committed for the round in flight; everyone else may stand + // up whenever they like. + if p.HasBet { return []Event{t.private(name, "You're in the middle of a baccarat round!")} } t.players.remove(name) @@ -120,7 +123,7 @@ func (t *baccaratTable) bet(name, spot string, amount int, wallet Wallet) []Even return []Event{t.private(name, "You have already bet this round.")} } if amount < t.Config.MinBet || amount > t.Config.MaxBet { - return []Event{t.private(name, fmt.Sprintf("Baccarat bets must be between %d and %d.", t.Config.MinBet, t.Config.MaxBet))} + return []Event{t.private(name, fmt.Sprintf("Baccarat bets must be %s.", betRangeText(t.Config.MinBet, t.Config.MaxBet)))} } wager, ok := wallet.Wager(amount) if !ok { diff --git a/internal/casino/baccarat_test.go b/internal/casino/baccarat_test.go index fc12e4b..522f875 100644 --- a/internal/casino/baccarat_test.go +++ b/internal/casino/baccarat_test.go @@ -336,6 +336,13 @@ func TestBaccaratLeaveAndTableFull(t *testing.T) { if events := busy.leave("alice"); !hasMessage(events, "middle of a baccarat round") { t.Fatalf("leave during betting not blocked: %+v", events) } + // A non-bettor may stand up mid-window; the bettor's round continues. + if events := busy.leave("bob"); !hasMessage(events, "You stand up.") { + t.Fatalf("non-bettor leave blocked during betting: %+v", events) + } + if busy.players.count() != 1 { + t.Fatalf("bob's seat was not freed: count=%d", busy.players.count()) + } } func TestBaccaratMenuAndActions(t *testing.T) { diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go index 90d6feb..442d4a2 100644 --- a/internal/casino/blackjack.go +++ b/internal/casino/blackjack.go @@ -111,18 +111,21 @@ func (t *blackjackTable) join(name string) []Event { func (t *blackjackTable) leave(name string) []Event { t.mu.Lock() defer t.mu.Unlock() - if !t.players.has(name) { + p := t.players.get(name) + if p == nil { return nil } - if t.phase != blackjackWaiting { + // A wager is committed for the round in flight; everyone else may stand + // up whenever they like. Non-bettor removal reuses the disconnect path, + // which fixes up the active seat while a round is in flight. + if p.HasBet { return []Event{t.private(name, "You're in the middle of a blackjack round!")} } - t.players.remove(name) - events := []Event{t.playerAction(name, fmt.Sprintf("%s stands up.", name), "You stand up.")} - if t.players.count() == 0 { - events = append(events, t.event("The blackjack table closes.", true)) + events := t.markDisconnectedLocked(name) + if len(events) == 0 { + return nil } - return events + return append(events, t.private(name, "You stand up.")) } func (t *blackjackTable) bet(name, spot string, amount int, wallet Wallet) []Event { @@ -142,7 +145,7 @@ func (t *blackjackTable) bet(name, spot string, amount int, wallet Wallet) []Eve return []Event{t.private(name, "You have already bet this round.")} } if amount < t.Config.MinBet || amount > t.Config.MaxBet { - return []Event{t.private(name, fmt.Sprintf("Blackjack bets must be between %d and %d.", t.Config.MinBet, t.Config.MaxBet))} + return []Event{t.private(name, fmt.Sprintf("Blackjack bets must be %s.", betRangeText(t.Config.MinBet, t.Config.MaxBet)))} } wager, ok := wallet.Wager(amount) if !ok { @@ -683,6 +686,10 @@ func blackjackResultMessages(name, result string, payout int) (string, string) { func (t *blackjackTable) markDisconnected(name string) []Event { t.mu.Lock() defer t.mu.Unlock() + return t.markDisconnectedLocked(name) +} + +func (t *blackjackTable) markDisconnectedLocked(name string) []Event { index := t.players.indexOf(name) if index < 0 { return nil diff --git a/internal/casino/blackjack_test.go b/internal/casino/blackjack_test.go index 510077d..0f7527c 100644 --- a/internal/casino/blackjack_test.go +++ b/internal/casino/blackjack_test.go @@ -323,6 +323,27 @@ func TestBlackjackDisconnectEarlierSeatPreservesTurn(t *testing.T) { } } +func TestBlackjackNonBettorMayLeaveMidRound(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 2}, newTestRand()) + table.join("alice") + table.join("bob") + wa := &testWallet{credits: 100} + table.bet("alice", "", 10, wa) // opens the betting window + if events := table.leave("bob"); !hasMessage(events, "You stand up.") { + t.Fatalf("non-bettor leave blocked mid-round: %+v", events) + } + if table.players.count() != 1 { + t.Fatalf("bob's seat was not freed: count=%d", table.players.count()) + } + // The bettor's round still proceeds on the next tick. + if events := table.tick(); !hasSnapshot(events) { + t.Fatalf("round did not start after the non-bettor left: %+v", events) + } + if events := table.leave("alice"); !hasMessage(events, "middle of a blackjack round") { + t.Fatalf("bettor leave not blocked: %+v", events) + } +} + func hasMessage(events []Event, fragment string) bool { for _, event := range events { if contains(event.Message, fragment) || contains(event.PrivateMessage, fragment) { diff --git a/internal/casino/machine.go b/internal/casino/machine.go index c1e93fe..2f8b753 100644 --- a/internal/casino/machine.go +++ b/internal/casino/machine.go @@ -36,6 +36,7 @@ type Machine struct { } func newMachine(roomID int, cfg MachineConfig, rng *rand.Rand) *Machine { + cfg = cfg.Normalize() profile := DefaultHuffAndPuffProfile() if cfg.Payout > 0 { profile = ScaleToRTP(profile, cfg.Payout) @@ -70,7 +71,7 @@ func (m *Machine) setBet(name string, amount int, wallet Wallet) []Event { return m.private("You're in the middle of a spin!") } if amount < m.Config.MinBet || amount > m.Config.MaxBet { - return m.private(fmt.Sprintf("Slot machine bets must be between %d and %d.", m.Config.MinBet, m.Config.MaxBet)) + return m.private(fmt.Sprintf("Slot machine bets must be %s.", betRangeText(m.Config.MinBet, m.Config.MaxBet))) } return m.beginSpinLocked(name, amount, wallet, fmt.Sprintf("%s sets the bet to %d and pulls the lever.", name, amount), diff --git a/internal/casino/machine_test.go b/internal/casino/machine_test.go index 79d38fc..53fe2e6 100644 --- a/internal/casino/machine_test.go +++ b/internal/casino/machine_test.go @@ -5,6 +5,24 @@ import ( "testing" ) +func TestBetRangeTextFormatsUnlimitedMax(t *testing.T) { + if got := betRangeText(1, maxBetUnlimited); got != "1 or more (no maximum)" { + t.Fatalf("unlimited range=%q", got) + } + if got := betRangeText(5, 100); got != "between 5 and 100" { + t.Fatalf("bounded range=%q", got) + } +} + +func TestMachineSetBetUnlimitedMaxMessage(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} + events := m.setBet("alice", 0, &testWallet{credits: 100}) + if !hasMessage(events, "Slot machine bets must be 1 or more (no maximum).") { + t.Fatalf("unexpected bet-range message: %+v", events) + } +} + func TestMachineFreeSpinsUseProfileCount(t *testing.T) { m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, rand.New(rand.NewSource(7))) m.profile.FreeSpinCount = 5 diff --git a/internal/casino/types.go b/internal/casino/types.go index 52c4bfa..4d5159a 100644 --- a/internal/casino/types.go +++ b/internal/casino/types.go @@ -1,5 +1,19 @@ package casino +import "fmt" + +// maxBetUnlimited is the MaxBet sentinel meaning "no wager cap". +const maxBetUnlimited = int(^uint(0) >> 1) + +// betRangeText describes the accepted wager range; the unlimited sentinel is +// rendered as "no maximum" instead of a machine-sized integer. +func betRangeText(minBet, maxBet int) string { + if maxBet == maxBetUnlimited { + return fmt.Sprintf("%d or more (no maximum)", minBet) + } + return fmt.Sprintf("between %d and %d", minBet, maxBet) +} + type GameName string const ( @@ -113,7 +127,7 @@ func (c MachineConfig) Normalize() MachineConfig { c.MinBet = 1 } if c.MaxBet <= 0 { - c.MaxBet = int(^uint(0) >> 1) + c.MaxBet = maxBetUnlimited } if c.SpinTicks <= 0 { c.SpinTicks = 2 @@ -138,7 +152,7 @@ func (c TableConfig) Normalize() TableConfig { c.MinBet = 1 } if c.MaxBet <= 0 { - c.MaxBet = int(^uint(0) >> 1) + c.MaxBet = maxBetUnlimited } return c } |
