diff options
Diffstat (limited to 'internal/casino/blackjack.go')
| -rw-r--r-- | internal/casino/blackjack.go | 188 |
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) } |
