aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/blackjack.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 18:38:35 -0400
committerhistoria <[not public]>2026-09-18 18:38:35 -0400
commit1f118d79e319da4e765502e9ca5b661772de94f1 (patch)
treebca8a3fd1e538f1c302480511e6ba872c92e10b8 /internal/casino/blackjack.go
parent5ad42a51df4dcc5ccc3aa6a6d902085ce2ee4239 (diff)
downloadthehouseoficarus-1f118d79e319da4e765502e9ca5b661772de94f1.tar.gz
refactor: casino table core; fix: stake/migration/validation gaps
Diffstat (limited to 'internal/casino/blackjack.go')
-rw-r--r--internal/casino/blackjack.go102
1 files changed, 21 insertions, 81 deletions
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
index fd0572b..fe212f7 100644
--- a/internal/casino/blackjack.go
+++ b/internal/casino/blackjack.go
@@ -6,16 +6,15 @@ import (
"fmt"
"math/rand"
"strings"
- "sync"
)
-type blackjackPhase string
-
+// Blackjack uses the shared table phases; these aliases keep the rule
+// vocabulary readable at the call sites.
const (
- blackjackWaiting blackjackPhase = "waiting"
- blackjackBetting blackjackPhase = "betting"
- blackjackInsurance blackjackPhase = "insurance"
- blackjackPlaying blackjackPhase = "playing"
+ blackjackWaiting = phaseWaiting
+ blackjackBetting = phaseBetting
+ blackjackInsurance = phaseInsurance
+ blackjackPlaying = phasePlaying
)
type blackjackHand struct {
@@ -50,7 +49,8 @@ func (h *blackjackHand) total() (int, bool) {
func (h *blackjackHand) blackjack(rules BlackjackConfig) bool {
total, _ := h.total()
- return len(h.cards) == 2 && !h.fromSplit && total == 21 || rules.CountsDoubleSplitBlackjack() && h.fromSplit && h.doubled && total == 21
+ return (len(h.cards) == 2 && !h.fromSplit && total == 21) ||
+ (rules.CountsDoubleSplitBlackjack() && h.fromSplit && h.doubled && total == 21)
}
type blackjackPlayer struct {
@@ -65,12 +65,8 @@ type blackjackPlayer struct {
func (p *blackjackPlayer) participantPtr() *Participant { return &p.Participant }
type blackjackTable struct {
- mu sync.Mutex
- tableBase
+ tableCore[*blackjackPlayer]
Rules BlackjackConfig
- players seatSet[*blackjackPlayer]
- phase blackjackPhase
- window bettingWindow
timer countdown
ceiling countdown
activePlayer int
@@ -98,21 +94,19 @@ func newBlackjackTable(roomID int, cfg TableConfig, rng *rand.Rand) *blackjackTa
cfg = cfg.Normalize()
rules := cfg.Blackjack.Normalize()
return &blackjackTable{
- tableBase: tableBase{RoomID: roomID, Config: cfg}, Rules: rules,
- players: newSeatSet[*blackjackPlayer](), phase: blackjackWaiting,
- shoe: newShoe(rules.Decks, rules.ShuffleAt, rng), turnHand: -1,
+ tableCore: newTableCore[*blackjackPlayer](&tableBase{RoomID: roomID, Config: cfg}),
+ Rules: rules,
+ shoe: newShoe(rules.Decks, rules.ShuffleAt, rng),
+ turnHand: -1,
}
}
func (t *blackjackTable) menuEvent(name string) Event {
- e := t.private(name, t.menu())
- e.Color = "casino_menu"
- e.Menu = &MenuView{Kind: MenuBlackjackBet, Commands: []MenuCommand{
+ return t.betMenuEvent(name, MenuBlackjackBet, []MenuCommand{
{Command: "bet", Desc: "Place a wager (min/max accepted)"},
{Command: "bet <amount>", Desc: "Place a specific wager"},
{Command: "stop", Desc: "Leave the table"},
- }}
- return e
+ })
}
func (t *blackjackTable) join(name string) []Event {
@@ -124,8 +118,7 @@ func (t *blackjackTable) join(name string) []Event {
if t.players.count() >= t.Config.MaxPlayers {
return []Event{t.private(name, "That blackjack table is full.")}
}
- t.players.add(name, &blackjackPlayer{Participant: Participant{Name: name, Connected: true}})
- events := []Event{t.playerAction(name, fmt.Sprintf("%s sits down.", name), "You sit down.")}
+ events := []Event{t.addSeatLocked(name, &blackjackPlayer{Participant: Participant{Name: name, Connected: true}})}
events = append(events, t.menuEvent(name))
return events
}
@@ -175,11 +168,7 @@ func (t *blackjackTable) bet(name, spot string, amount int, wallet Wallet) []Eve
}
p.HasBet, p.Wager, p.Wallet, p.lastBet = true, wager, wallet, amount
events := []Event{t.playerAction(name, fmt.Sprintf("%s bets %d credits.", name, amount), fmt.Sprintf("You bet %d credits.", amount))}
- if t.players.count() > 1 && t.phase == blackjackWaiting {
- t.phase = blackjackBetting
- t.window.open(t.Config.BettingTicks)
- events = append(events, t.event(fmt.Sprintf("Blackjack betting is open for %d ticks.", t.Config.BettingTicks), true))
- }
+ events = t.openBettingLocked(events)
if allBet(t.players.participants()) {
events = append(events, t.startRound()...)
}
@@ -191,20 +180,7 @@ func (t *blackjackTable) tick() []Event {
defer t.mu.Unlock()
switch t.phase {
case blackjackBetting:
- sitOuts, resolve, pending := t.window.advance(t.players.participants())
- if pending {
- return nil
- }
- var events []Event
- for _, name := range sitOuts {
- events = append(events, t.event(fmt.Sprintf("%s sits out this blackjack round.", name), true))
- }
- if resolve {
- events = append(events, t.startRound()...)
- } else {
- t.resetRound()
- }
- return events
+ return t.advanceBettingLocked(t.startRound, t.resetRound)
case blackjackInsurance:
if t.insuranceSettled() {
return t.finishInsurance()
@@ -807,10 +783,6 @@ func (t *blackjackTable) snapshot(revealDealer bool) *BlackjackSnapshot {
return snapshot
}
-func (t *blackjackTable) menu() string {
- return "New blackjack hand! Place your bet."
-}
-
func handScore(hand *blackjackHand) string {
total, soft := hand.total()
if !soft || total == 21 {
@@ -859,11 +831,9 @@ func (t *blackjackTable) markDisconnectedLocked(name string) []Event {
p.Wallet.RefundOutstanding()
}
}
- t.players.remove(name)
+ t.removeSeatLocked(name)
events := []Event{t.event(fmt.Sprintf("%s stands up.", name), true)}
- if t.players.count() == 0 {
- events = append(events, t.event("The blackjack table closes.", true))
- }
+ events = t.closeIfEmptyLocked(events)
if t.phase != blackjackPlaying {
// Betting and insurance resolve against the remaining seats on the
// next tick; waiting has no round in flight.
@@ -882,12 +852,6 @@ func (t *blackjackTable) markDisconnectedLocked(name string) []Event {
return append(events, t.finishHand()...)
}
-func (t *blackjackTable) hasParticipant(name string) bool {
- t.mu.Lock()
- defer t.mu.Unlock()
- return t.players.has(name)
-}
-
// refundAll refunds every committed wager at this table (graceful
// shutdown/eviction): the wallet returns every outstanding stake — opening
// bet, doubles, splits, and insurance — exactly once. It then RESETS the
@@ -897,31 +861,7 @@ func (t *blackjackTable) hasParticipant(name string) bool {
func (t *blackjackTable) refundAll() []Event {
t.mu.Lock()
defer t.mu.Unlock()
- var events []Event
- for _, p := range t.players.ordered() {
- if !p.HasBet || p.Wallet == nil {
- continue
- }
- name := p.Name
- chips, credits, err := p.Wallet.RefundOutstanding()
- refund := chips + credits
- if err != nil || refund <= 0 {
- if err != nil {
- events = append(events, t.private(name, "Shutdown refund could not be persisted; the stake is recorded for recovery."))
- }
- continue
- }
- events = append(events, t.playerAction(name,
- fmt.Sprintf("%s is refunded %d as the casino closes.", name, refund),
- fmt.Sprintf("The house refunds your %d stake for the shutdown.", refund)))
- }
+ events := t.refundAllLocked()
t.resetRound()
return events
}
-
-// setRoom updates the event-routing room after a room-ID rekey. Locked.
-func (t *blackjackTable) setRoom(roomID int) {
- t.mu.Lock()
- defer t.mu.Unlock()
- t.tableBase.setRoom(roomID)
-}