aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/blackjack.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/blackjack.go')
-rw-r--r--internal/casino/blackjack.go188
1 files changed, 91 insertions, 97 deletions
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
index 3c4dc09..90d6feb 100644
--- a/internal/casino/blackjack.go
+++ b/internal/casino/blackjack.go
@@ -56,13 +56,15 @@ type blackjackPlayer struct {
lastBet int
}
+func (p *blackjackPlayer) participantPtr() *Participant { return &p.Participant }
+
type blackjackTable struct {
mu sync.Mutex
tableBase
Rules BlackjackConfig
- participants map[string]*blackjackPlayer
- order []string
+ players seatSet[*blackjackPlayer]
phase blackjackPhase
+ window bettingWindow
timer countdown
activePlayer int
activeHand int
@@ -75,21 +77,11 @@ func newBlackjackTable(roomID int, cfg TableConfig, rng *rand.Rand) *blackjackTa
rules := cfg.Blackjack.Normalize()
return &blackjackTable{
tableBase: tableBase{RoomID: roomID, Config: cfg}, Rules: rules,
- participants: make(map[string]*blackjackPlayer), phase: blackjackWaiting,
+ players: newSeatSet[*blackjackPlayer](), phase: blackjackWaiting,
shoe: newShoe(rules.Decks, rules.ShuffleAt, rng),
}
}
-func (t *blackjackTable) seats() []*Participant {
- seats := make([]*Participant, 0, len(t.order))
- for _, name := range t.order {
- if p := t.participants[name]; p != nil {
- seats = append(seats, &p.Participant)
- }
- }
- return seats
-}
-
func (t *blackjackTable) menuEvent(name string) Event {
e := t.private(name, t.menu())
e.Color = "casino_menu"
@@ -104,22 +96,13 @@ func (t *blackjackTable) menuEvent(name string) Event {
func (t *blackjackTable) join(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
- if p := t.participants[name]; p != nil {
- p.Connected = true
- if t.phase != blackjackWaiting {
- e := t.private(name, "You resume your place.")
- if t.phase != blackjackBetting {
- e.Blackjack = t.snapshot(false)
- }
- return []Event{e}
- }
+ if t.players.has(name) {
return []Event{t.menuEvent(name)}
}
- if len(t.participants) >= t.Config.MaxPlayers {
+ if t.players.count() >= t.Config.MaxPlayers {
return []Event{t.private(name, "That blackjack table is full.")}
}
- t.participants[name] = &blackjackPlayer{Participant: Participant{Name: name, Connected: true}}
- t.order = append(t.order, name)
+ 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 = append(events, t.menuEvent(name))
return events
@@ -128,22 +111,15 @@ func (t *blackjackTable) join(name string) []Event {
func (t *blackjackTable) leave(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
- p := t.participants[name]
- if p == nil {
+ if !t.players.has(name) {
return nil
}
if t.phase != blackjackWaiting {
return []Event{t.private(name, "You're in the middle of a blackjack round!")}
}
- delete(t.participants, name)
- for i, n := range t.order {
- if n == name {
- t.order = append(t.order[:i], t.order[i+1:]...)
- break
- }
- }
+ t.players.remove(name)
events := []Event{t.playerAction(name, fmt.Sprintf("%s stands up.", name), "You stand up.")}
- if len(t.participants) == 0 {
+ if t.players.count() == 0 {
events = append(events, t.event("The blackjack table closes.", true))
}
return events
@@ -152,7 +128,7 @@ func (t *blackjackTable) leave(name string) []Event {
func (t *blackjackTable) bet(name, spot string, amount int, wallet Wallet) []Event {
t.mu.Lock()
defer t.mu.Unlock()
- p := t.participants[name]
+ p := t.players.get(name)
if p == nil {
return []Event{t.private(name, "You aren't playing blackjack.")}
}
@@ -174,12 +150,12 @@ 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 len(t.participants) > 1 && t.phase == blackjackWaiting {
+ if t.players.count() > 1 && t.phase == blackjackWaiting {
t.phase = blackjackBetting
- t.timer.start(t.Config.BettingTicks)
+ t.window.open(t.Config.BettingTicks)
events = append(events, t.event(fmt.Sprintf("Blackjack betting is open for %d ticks.", t.Config.BettingTicks), true))
}
- if allBet(t.seats()) {
+ if allBet(t.players.participants()) {
events = append(events, t.startRound()...)
}
return events
@@ -190,18 +166,15 @@ func (t *blackjackTable) tick() []Event {
defer t.mu.Unlock()
switch t.phase {
case blackjackBetting:
- t.timer.tick()
- if allBet(t.seats()) {
- return t.startRound()
- }
- if !t.timer.expired() {
+ sitOuts, resolve, pending := t.window.advance(t.players.participants())
+ if pending {
return nil
}
var events []Event
- for _, name := range sitOutNames(t.seats()) {
+ for _, name := range sitOuts {
events = append(events, t.event(fmt.Sprintf("%s sits out this blackjack round.", name), true))
}
- if anyBet(t.seats()) {
+ if resolve {
events = append(events, t.startRound()...)
} else {
t.resetRound()
@@ -217,15 +190,26 @@ func (t *blackjackTable) tick() []Event {
}
return t.finishInsurance()
case blackjackPlaying:
- if t.bettorCount() <= 1 {
+ name := t.activeName()
+ if name == "" {
+ return nil
+ }
+ active := t.players.get(name)
+ if active == nil {
+ // Unreachable while mid-round leaves are blocked; keep the game moving.
+ t.activePlayer++
+ return t.finishHand()
+ }
+ if active.Connected && t.bettorCount() <= 1 {
+ // A lone connected player is never timed out. A disconnected
+ // active player always is, so the table can never stall.
return nil
}
t.timer.tick()
if !t.timer.expired() {
return nil
}
- name := t.activeName()
- t.participants[name].hands[t.activeHand].stood = true
+ active.hands[t.activeHand].stood = true
events := []Event{t.private(name, "Your action timed out; standing.")}
events = append(events, t.finishHand()...)
return events
@@ -243,7 +227,7 @@ func (t *blackjackTable) action(name, action string) []Event {
if t.phase != blackjackPlaying || t.activeName() != name {
return []Event{t.private(name, "It's not your turn.")}
}
- p := t.participants[name]
+ p := t.players.get(name)
hand := p.hands[t.activeHand]
t.timer.start(t.Rules.ActionTicks)
switch action {
@@ -301,7 +285,7 @@ func (t *blackjackTable) action(name, action string) []Event {
// insuranceAction handles commands during the insurance offer window:
// "insurance" buys cover for half the base bet; any other action declines.
func (t *blackjackTable) insuranceAction(name, action string) []Event {
- p := t.participants[name]
+ p := t.players.get(name)
if p == nil || !p.HasBet {
return []Event{t.private(name, "Insurance is only offered to players with a bet this round.")}
}
@@ -332,9 +316,8 @@ func (t *blackjackTable) insuranceAction(name, action string) []Event {
// insuranceSettled reports whether every connected bettor has taken or
// declined insurance.
func (t *blackjackTable) insuranceSettled() bool {
- for _, name := range t.order {
- p := t.participants[name]
- if p == nil || !p.HasBet || !p.Connected {
+ for _, p := range t.players.ordered() {
+ if !p.HasBet || !p.Connected {
continue
}
if p.insurance == 0 && !p.declinedInsurance {
@@ -392,9 +375,8 @@ func splitCompatible(a, b card, unlikeTens bool) bool {
func (t *blackjackTable) startRound() []Event {
t.dealer = blackjackHand{cards: []card{t.shoe.draw(), t.shoe.draw()}}
- for _, name := range t.order {
- p := t.participants[name]
- if p == nil || !p.HasBet {
+ for _, p := range t.players.ordered() {
+ if !p.HasBet {
continue
}
p.hands = []*blackjackHand{{cards: []card{t.shoe.draw(), t.shoe.draw()}, bet: p.Wager.Total}}
@@ -410,12 +392,11 @@ func (t *blackjackTable) startRound() []Event {
t.phase = blackjackInsurance
t.timer.start(t.Rules.ActionTicks)
events = append(events, t.event("The dealer shows an ace. Insurance is on offer.", true))
- for _, name := range t.order {
- p := t.participants[name]
- if p == nil || !p.HasBet || !p.Connected || p.declinedInsurance {
+ for _, p := range t.players.ordered() {
+ if !p.HasBet || !p.Connected || p.declinedInsurance {
continue
}
- events = append(events, t.private(name, fmt.Sprintf("Insure your %d-credit bet for %d credits? Type \"insurance\" to buy, or any other action to decline.", p.hands[0].bet, p.hands[0].bet/2)))
+ events = append(events, t.private(p.Name, fmt.Sprintf("Insure your %d-credit bet for %d credits? Type \"insurance\" to buy, or any other action to decline.", p.hands[0].bet, p.hands[0].bet/2)))
}
if t.insuranceSettled() {
events = append(events, t.finishInsurance()...)
@@ -441,7 +422,7 @@ func (t *blackjackTable) prompt() []Event {
if name == "" {
return t.settle()
}
- p := t.participants[name]
+ p := t.players.get(name)
h := p.hands[t.activeHand]
ranks := make([]string, len(h.cards))
for i, card := range h.cards {
@@ -506,8 +487,8 @@ func (t *blackjackTable) finishHand() []Event {
}
func (t *blackjackTable) skipCompletedHands() {
- for t.activePlayer < len(t.order) {
- p := t.participants[t.order[t.activePlayer]]
+ for t.activePlayer < t.players.count() {
+ p := t.players.get(t.players.nameAt(t.activePlayer))
if p == nil || !p.HasBet {
t.activePlayer++
t.activeHand = 0
@@ -528,10 +509,7 @@ func (t *blackjackTable) skipCompletedHands() {
}
func (t *blackjackTable) activeName() string {
- if t.activePlayer >= len(t.order) {
- return ""
- }
- return t.order[t.activePlayer]
+ return t.players.nameAt(t.activePlayer)
}
func (t *blackjackTable) dealerBlackjack() bool {
@@ -551,9 +529,8 @@ func (t *blackjackTable) settle() []Event {
dealerEvent.Blackjack = t.snapshot(true)
events := []Event{dealerEvent}
dealerBlackjack := t.dealerBlackjack()
- for _, name := range t.order {
- p := t.participants[name]
- if p == nil || !p.HasBet {
+ for _, p := range t.players.ordered() {
+ if !p.HasBet {
continue
}
for _, hand := range p.hands {
@@ -561,8 +538,8 @@ func (t *blackjackTable) settle() []Event {
if payout > 0 && p.Wallet != nil {
p.Wallet.PayCredits(payout)
}
- publicResult, privateResult := blackjackResultMessages(name, result, payout)
- settlement := t.playerAction(name, publicResult, privateResult)
+ publicResult, privateResult := blackjackResultMessages(p.Name, result, payout)
+ settlement := t.playerAction(p.Name, publicResult, privateResult)
if result == "blackjack" {
settlement.Color = "casino_jackpot_win"
} else if result == "win" {
@@ -573,18 +550,18 @@ func (t *blackjackTable) settle() []Event {
if p.insurance > 0 {
insurancePayout := 0
if dealerBlackjack {
- insurancePayout = int(math.Round(float64(p.insurance) * (1 + t.Rules.InsurancePayout)))
+ insurancePayout = int(math.Floor(float64(p.insurance) * (1 + t.Rules.InsurancePayout)))
}
if insurancePayout > 0 {
p.Wallet.PayCredits(insurancePayout)
}
- events = append(events, t.private(name, fmt.Sprintf("Insurance pays %d credits.", insurancePayout)))
+ events = append(events, t.private(p.Name, fmt.Sprintf("Insurance pays %d credits.", insurancePayout)))
}
}
t.resetRound()
- for _, name := range t.order {
- if p := t.participants[name]; p != nil && p.Connected {
- events = append(events, t.menuEvent(name))
+ for _, p := range t.players.ordered() {
+ if p.Connected {
+ events = append(events, t.menuEvent(p.Name))
}
}
return events
@@ -606,7 +583,8 @@ func settleHand(hand *blackjackHand, dealerTotal int, dealerBlackjack bool, rule
return "dealer blackjack", 0
}
if playerBlackjack {
- return "blackjack", int(math.Round(float64(hand.bet) * (1 + rules.BlackjackPayout)))
+ // Winnings round down to whole credits; never bet less than two.
+ return "blackjack", hand.bet + int(math.Floor(float64(hand.bet)*rules.BlackjackPayout))
}
if total > 21 || dealerTotal > total && dealerTotal <= 21 {
return "loss", 0
@@ -622,14 +600,15 @@ func settleHand(hand *blackjackHand, dealerTotal int, dealerBlackjack bool, rule
func (t *blackjackTable) resetRound() {
t.phase = blackjackWaiting
+ t.window.close()
t.timer.stop()
- for _, p := range t.participants {
+ for _, p := range t.players.ordered() {
p.HasBet, p.Wager, p.hands, p.insurance, p.declinedInsurance = false, Wager{}, nil, 0, false
}
}
func (t *blackjackTable) rebet(name string) (string, int) {
- if p := t.participants[name]; p != nil && p.lastBet > 0 {
+ if p := t.players.get(name); p != nil && p.lastBet > 0 {
return "", p.lastBet
}
return "", t.Config.MinBet
@@ -637,7 +616,7 @@ func (t *blackjackTable) rebet(name string) (string, int) {
func (t *blackjackTable) bettorCount() int {
count := 0
- for _, p := range t.participants {
+ for _, p := range t.players.ordered() {
if p.HasBet {
count++
}
@@ -654,13 +633,12 @@ func (t *blackjackTable) snapshot(revealDealer bool) *BlackjackSnapshot {
}
snapshot.Dealer = append(snapshot.Dealer, CardView{Rank: card.rank, Suit: card.suit})
}
- for _, name := range t.order {
- p := t.participants[name]
- if p == nil || !p.HasBet {
+ for _, p := range t.players.ordered() {
+ if !p.HasBet {
continue
}
for _, hand := range p.hands {
- view := BlackjackPlayerView{Name: name, Score: handScore(hand)}
+ view := BlackjackPlayerView{Name: p.Name, Score: handScore(hand)}
for _, card := range hand.cards {
view.Cards = append(view.Cards, CardView{Rank: card.rank, Suit: card.suit})
}
@@ -698,25 +676,41 @@ func blackjackResultMessages(name, result string, payout int) (string, string) {
}
}
+// markDisconnected removes the player from the table, forfeiting any wager
+// they committed (settle only pays seated bettors). Removing a seat shifts
+// the seat indices the active turn points into, so mid-round play is fixed
+// up and handed to the next bettor (or settled) immediately.
func (t *blackjackTable) markDisconnected(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
- if p := t.participants[name]; p != nil {
- p.Connected = false
+ index := t.players.indexOf(name)
+ if index < 0 {
+ return nil
}
- return nil
-}
-
-func (t *blackjackTable) markConnected(name string) {
- t.mu.Lock()
- defer t.mu.Unlock()
- if p := t.participants[name]; p != nil {
- p.Connected = true
+ t.players.remove(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))
+ }
+ if t.phase != blackjackPlaying {
+ // Betting and insurance resolve against the remaining seats on the
+ // next tick; waiting has no round in flight.
+ return events
}
+ switch {
+ case index < t.activePlayer:
+ // The same player stays active, one seat earlier.
+ t.activePlayer--
+ case index == t.activePlayer:
+ // The next bettor slides into the active seat.
+ t.activeHand = 0
+ }
+ t.timer.start(t.Rules.ActionTicks)
+ return append(events, t.finishHand()...)
}
func (t *blackjackTable) hasParticipant(name string) bool {
t.mu.Lock()
defer t.mu.Unlock()
- return t.participants[name] != nil
+ return t.players.has(name)
}