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.go773
1 files changed, 773 insertions, 0 deletions
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
new file mode 100644
index 0000000..a158a24
--- /dev/null
+++ b/internal/casino/blackjack.go
@@ -0,0 +1,773 @@
+package casino
+
+import (
+ "fmt"
+ "math"
+ "math/rand"
+ "strings"
+ "sync"
+)
+
+type blackjackPhase string
+
+const (
+ blackjackWaiting blackjackPhase = "waiting"
+ blackjackBetting blackjackPhase = "betting"
+ blackjackInsurance blackjackPhase = "insurance"
+ blackjackPlaying blackjackPhase = "playing"
+)
+
+type blackjackCard struct {
+ rank string
+ suit string
+}
+
+func (c blackjackCard) String() string { return c.rank + " of " + c.suit }
+
+func (c blackjackCard) value() int {
+ switch c.rank {
+ case "A":
+ return 11
+ case "K", "Q", "J":
+ return 10
+ default:
+ var value int
+ fmt.Sscanf(c.rank, "%d", &value)
+ return value
+ }
+}
+
+type blackjackHand struct {
+ cards []blackjackCard
+ bet int
+ fromSplit bool
+ doubled bool
+ stood bool
+ bust bool
+ surrendered bool
+}
+
+func (h *blackjackHand) total() (int, bool) {
+ total, aces := 0, 0
+ for _, card := range h.cards {
+ total += card.value()
+ if card.rank == "A" {
+ aces++
+ }
+ }
+ for total > 21 && aces > 0 {
+ total -= 10
+ aces--
+ }
+ soft := aces > 0
+ return total, soft
+}
+
+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
+}
+
+type blackjackPlayer struct {
+ Participant
+ hands []*blackjackHand
+ insurance int
+ declinedInsurance bool
+ lastBet int
+}
+
+type blackjackShoe struct {
+ cards []blackjackCard
+ rng *rand.Rand
+ decks int
+}
+
+func newBlackjackShoe(decks int, rng *rand.Rand) *blackjackShoe {
+ s := &blackjackShoe{rng: rng, decks: decks}
+ s.shuffle()
+ return s
+}
+
+func (s *blackjackShoe) shuffle() {
+ ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
+ suits := []string{"clubs", "diamonds", "hearts", "spades"}
+ s.cards = make([]blackjackCard, 0, s.decks*52)
+ for deck := 0; deck < s.decks; deck++ {
+ for _, suit := range suits {
+ for _, rank := range ranks {
+ s.cards = append(s.cards, blackjackCard{rank: rank, suit: suit})
+ }
+ }
+ }
+ s.rng.Shuffle(len(s.cards), func(i, j int) { s.cards[i], s.cards[j] = s.cards[j], s.cards[i] })
+}
+
+func (s *blackjackShoe) draw(rules BlackjackConfig) blackjackCard {
+ if len(s.cards) <= int(float64(s.decks*52)*rules.ShuffleAt) {
+ s.shuffle()
+ }
+ card := s.cards[len(s.cards)-1]
+ s.cards = s.cards[:len(s.cards)-1]
+ return card
+}
+
+type blackjackTable struct {
+ mu sync.Mutex
+ RoomID int
+ Config TableConfig
+ Rules BlackjackConfig
+ participants map[string]*blackjackPlayer
+ order []string
+ phase blackjackPhase
+ timer countdown
+ activePlayer int
+ activeHand int
+ dealer blackjackHand
+ shoe *blackjackShoe
+}
+
+func newBlackjackTable(roomID int, cfg TableConfig, rng *rand.Rand) *blackjackTable {
+ cfg = cfg.Normalize()
+ rules := cfg.Blackjack.Normalize()
+ return &blackjackTable{
+ RoomID: roomID, Config: cfg, Rules: rules,
+ participants: make(map[string]*blackjackPlayer), phase: blackjackWaiting,
+ shoe: newBlackjackShoe(rules.Decks, 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) event(message string, public bool) Event {
+ return Event{RoomID: t.RoomID, TableID: t.Config.ID, Type: EventResult, Message: message, Public: public, Color: "casino_action"}
+}
+
+func (t *blackjackTable) private(name, message string) Event {
+ e := t.event(message, false)
+ e.PrivateTo = name
+ return e
+}
+
+func (t *blackjackTable) playerAction(name, publicMessage, privateMessage string) Event {
+ e := t.event(publicMessage, true)
+ e.PrivateTo = name
+ e.PrivateMessage = privateMessage
+ return e
+}
+
+func (t *blackjackTable) menuEvent(name string) Event {
+ e := t.private(name, t.menu())
+ e.Color = "casino_menu"
+ e.Menu = &MenuView{Kind: MenuBlackjackBet}
+ return e
+}
+
+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}
+ }
+ return []Event{t.menuEvent(name)}
+ }
+ if len(t.participants) >= 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)
+ events := []Event{t.playerAction(name, fmt.Sprintf("%s sits down.", name), "You sit down.")}
+ events = append(events, t.menuEvent(name))
+ return events
+}
+
+func (t *blackjackTable) leave(name string) []Event {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ p := t.participants[name]
+ if p == nil {
+ 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
+ }
+ }
+ events := []Event{t.playerAction(name, fmt.Sprintf("%s stands up.", name), "You stand up.")}
+ if len(t.participants) == 0 {
+ events = append(events, t.event("The blackjack table closes.", true))
+ }
+ return events
+}
+
+func (t *blackjackTable) bet(name string, amount int, wallet Wallet) []Event {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ p := t.participants[name]
+ if p == nil {
+ return []Event{t.private(name, "You aren't playing blackjack.")}
+ }
+ if t.phase != blackjackWaiting && t.phase != blackjackBetting {
+ return []Event{t.private(name, "That blackjack table isn't accepting bets.")}
+ }
+ if p.HasBet {
+ 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))}
+ }
+ wager, ok := wallet.Wager(amount)
+ if !ok {
+ return []Event{t.private(name, "You don't have enough chips and credits for that bet.")}
+ }
+ 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 {
+ t.phase = blackjackBetting
+ t.timer.start(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()) {
+ events = append(events, t.startRound()...)
+ }
+ return events
+}
+
+func (t *blackjackTable) tick() []Event {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ switch t.phase {
+ case blackjackBetting:
+ t.timer.tick()
+ if allBet(t.seats()) {
+ return t.startRound()
+ }
+ if !t.timer.expired() {
+ return nil
+ }
+ var events []Event
+ for _, name := range sitOutNames(t.seats()) {
+ events = append(events, t.event(fmt.Sprintf("%s sits out this blackjack round.", name), true))
+ }
+ if anyBet(t.seats()) {
+ events = append(events, t.startRound()...)
+ } else {
+ t.resetRound()
+ }
+ return events
+ case blackjackInsurance:
+ if t.insuranceSettled() {
+ return t.finishInsurance()
+ }
+ t.timer.tick()
+ if !t.timer.expired() {
+ return nil
+ }
+ return t.finishInsurance()
+ case blackjackPlaying:
+ if t.bettorCount() <= 1 {
+ return nil
+ }
+ t.timer.tick()
+ if !t.timer.expired() {
+ return nil
+ }
+ name := t.activeName()
+ t.participants[name].hands[t.activeHand].stood = true
+ events := []Event{t.private(name, "Your action timed out; standing.")}
+ events = append(events, t.finishHand()...)
+ return events
+ default:
+ return nil
+ }
+}
+
+func (t *blackjackTable) action(name, action string) []Event {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ if t.phase == blackjackInsurance {
+ return t.insuranceAction(name, action)
+ }
+ if t.phase != blackjackPlaying || t.activeName() != name {
+ return []Event{t.private(name, "It's not your turn.")}
+ }
+ p := t.participants[name]
+ hand := p.hands[t.activeHand]
+ t.timer.start(t.Rules.ActionTicks)
+ switch action {
+ case "hit":
+ if fromSplitAces(hand) {
+ return []Event{t.private(name, "Split aces cannot be hit.")}
+ }
+ hand.cards = append(hand.cards, t.shoe.draw(t.Rules))
+ total, _ := hand.total()
+ if total > 21 {
+ hand.bust = true
+ }
+ e := t.playerAction(name, fmt.Sprintf("%s hits and receives %s.", name, hand.cards[len(hand.cards)-1]), fmt.Sprintf("You hit and receive %s.", hand.cards[len(hand.cards)-1]))
+ e.Blackjack = t.snapshot(false)
+ events := []Event{e}
+ if total >= 21 {
+ events = append(events, t.finishHand()...)
+ } else {
+ events = append(events, t.prompt()...)
+ }
+ return events
+ case "stand":
+ hand.stood = true
+ return t.finishHand()
+ case "double":
+ if len(hand.cards) != 2 || hand.doubled || hand.surrendered || fromSplitAces(hand) || (hand.fromSplit && !t.Rules.AllowsDoubleAfterSplit()) {
+ return []Event{t.private(name, "You can only double a qualifying hand.")}
+ }
+ if _, ok := p.Wallet.Wager(hand.bet); !ok {
+ return []Event{t.private(name, "You don't have enough chips and credits to double that hand.")}
+ }
+ hand.bet *= 2
+ hand.doubled = true
+ hand.stood = true
+ hand.cards = append(hand.cards, t.shoe.draw(t.Rules))
+ if total, _ := hand.total(); total > 21 {
+ hand.bust = true
+ }
+ e := t.playerAction(name, fmt.Sprintf("%s doubles down.", name), "You double down.")
+ e.Blackjack = t.snapshot(false)
+ return append([]Event{e}, t.finishHand()...)
+ case "split":
+ return t.splitHand(name, p, hand)
+ case "surrender":
+ if !t.Rules.AllowsSurrender() || hand.fromSplit || len(hand.cards) != 2 {
+ return []Event{t.private(name, "Surrender is not available for this hand.")}
+ }
+ hand.surrendered = true
+ return t.finishHand()
+ default:
+ return []Event{t.private(name, "Unknown blackjack action.")}
+ }
+}
+
+// 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]
+ if p == nil || !p.HasBet {
+ return []Event{t.private(name, "Insurance is only offered to players with a bet this round.")}
+ }
+ if p.insurance > 0 || p.declinedInsurance {
+ return []Event{t.private(name, "You have already decided about insurance.")}
+ }
+ var events []Event
+ if action == "insurance" {
+ amount := p.hands[0].bet / 2
+ if amount <= 0 {
+ return []Event{t.private(name, "Your bet is too small to insure.")}
+ }
+ if _, ok := p.Wallet.Wager(amount); !ok {
+ return []Event{t.private(name, "You don't have enough chips and credits for insurance.")}
+ }
+ p.insurance = amount
+ events = append(events, t.playerAction(name, fmt.Sprintf("%s takes insurance.", name), fmt.Sprintf("You place %d credits on insurance.", amount)))
+ } else {
+ p.declinedInsurance = true
+ events = append(events, t.private(name, "You decline insurance."))
+ }
+ if t.insuranceSettled() {
+ events = append(events, t.finishInsurance()...)
+ }
+ return events
+}
+
+// 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 {
+ continue
+ }
+ if p.insurance == 0 && !p.declinedInsurance {
+ return false
+ }
+ }
+ return true
+}
+
+// finishInsurance resolves the insurance window: a dealer blackjack settles
+// the round immediately, otherwise play begins.
+func (t *blackjackTable) finishInsurance() []Event {
+ if t.dealerBlackjack() {
+ return t.settle()
+ }
+ t.beginPlay()
+ return t.finishHand()
+}
+
+func (t *blackjackTable) splitHand(name string, p *blackjackPlayer, hand *blackjackHand) []Event {
+ if len(hand.cards) != 2 || len(p.hands) >= t.Rules.MaxSplitHands || !splitCompatible(hand.cards[0], hand.cards[1], t.Rules.AllowsUnlikeTenSplit()) {
+ return []Event{t.private(name, "That hand cannot be split.")}
+ }
+ if hand.cards[0].rank == "A" && hand.fromSplit && !t.Rules.AllowsResplitAces() {
+ return []Event{t.private(name, "Aces cannot be re-split at this table.")}
+ }
+ if _, ok := p.Wallet.Wager(hand.bet); !ok {
+ return []Event{t.private(name, "You don't have enough chips and credits to split that hand.")}
+ }
+ left := &blackjackHand{cards: []blackjackCard{hand.cards[0]}, bet: hand.bet, fromSplit: true}
+ right := &blackjackHand{cards: []blackjackCard{hand.cards[1]}, bet: hand.bet, fromSplit: true}
+ left.cards = append(left.cards, t.shoe.draw(t.Rules))
+ right.cards = append(right.cards, t.shoe.draw(t.Rules))
+ index := t.activeHand
+ p.hands = append(p.hands[:index], append([]*blackjackHand{left, right}, p.hands[index+1:]...)...)
+ e := t.playerAction(name, fmt.Sprintf("%s splits the hand.", name), "You split the hand.")
+ e.Blackjack = t.snapshot(false)
+ events := []Event{e}
+ if left.cards[0].rank == "A" {
+ left.stood = !(t.Rules.AllowsResplitAces() && left.cards[1].rank == "A")
+ right.stood = !(t.Rules.AllowsResplitAces() && right.cards[1].rank == "A")
+ events = append(events, t.finishHand()...)
+ } else {
+ events = append(events, t.prompt()...)
+ }
+ return events
+}
+
+func splitCompatible(a, b blackjackCard, unlikeTens bool) bool {
+ if a.rank == b.rank {
+ return true
+ }
+ return unlikeTens && a.value() == 10 && b.value() == 10
+}
+
+func (t *blackjackTable) startRound() []Event {
+ t.dealer = blackjackHand{cards: []blackjackCard{t.shoe.draw(t.Rules), t.shoe.draw(t.Rules)}}
+ for _, name := range t.order {
+ p := t.participants[name]
+ if p == nil || !p.HasBet {
+ continue
+ }
+ p.hands = []*blackjackHand{{cards: []blackjackCard{t.shoe.draw(t.Rules), t.shoe.draw(t.Rules)}, bet: p.Wager.Total}}
+ if p.hands[0].bet/2 <= 0 {
+ // A half-bet that rounds to zero cannot be insured.
+ p.declinedInsurance = true
+ }
+ }
+ roundEvent := t.event("New blackjack hand!", true)
+ roundEvent.Blackjack = t.snapshot(false)
+ events := []Event{roundEvent}
+ if t.dealer.cards[1].rank == "A" {
+ 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 {
+ 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)))
+ }
+ if t.insuranceSettled() {
+ events = append(events, t.finishInsurance()...)
+ }
+ return events
+ }
+ if t.dealerBlackjack() {
+ events = append(events, t.settle()...)
+ return events
+ }
+ t.beginPlay()
+ return append(events, t.finishHand()...)
+}
+
+func (t *blackjackTable) beginPlay() {
+ t.phase = blackjackPlaying
+ t.timer.start(t.Rules.ActionTicks)
+ t.activePlayer, t.activeHand = 0, 0
+}
+
+func (t *blackjackTable) prompt() []Event {
+ name := t.activeName()
+ if name == "" {
+ return t.settle()
+ }
+ p := t.participants[name]
+ h := p.hands[t.activeHand]
+ ranks := make([]string, len(h.cards))
+ for i, card := range h.cards {
+ ranks[i] = card.rank
+ }
+ actions := t.availableActions(p, h)
+ e := t.private(name, fmt.Sprintf("Your turn (%s).\nActions: %s", strings.Join(ranks, ","), strings.Join(actions, ", ")))
+ e.Color = "casino_menu"
+ e.Menu = &MenuView{Kind: MenuBlackjackTurn, Title: fmt.Sprintf("Your turn (%s)", strings.Join(ranks, ",")), Actions: actions}
+ return []Event{e}
+}
+
+func (t *blackjackTable) availableActions(p *blackjackPlayer, hand *blackjackHand) []string {
+ if fromSplitAces(hand) {
+ // Split aces receive one card each and may only be re-split.
+ actions := []string{"stand"}
+ if len(hand.cards) == 2 && len(p.hands) < t.Rules.MaxSplitHands && t.Rules.AllowsResplitAces() &&
+ splitCompatible(hand.cards[0], hand.cards[1], t.Rules.AllowsUnlikeTenSplit()) {
+ actions = append(actions, "split")
+ }
+ return actions
+ }
+ actions := []string{"hit", "stand"}
+ if len(hand.cards) == 2 && !hand.doubled && (!hand.fromSplit || t.Rules.AllowsDoubleAfterSplit()) {
+ actions = append(actions, "double")
+ }
+ if len(hand.cards) == 2 && len(p.hands) < t.Rules.MaxSplitHands && splitCompatible(hand.cards[0], hand.cards[1], t.Rules.AllowsUnlikeTenSplit()) &&
+ !(hand.cards[0].rank == "A" && hand.fromSplit && !t.Rules.AllowsResplitAces()) {
+ actions = append(actions, "split")
+ }
+ if t.Rules.AllowsSurrender() && !hand.fromSplit && len(hand.cards) == 2 {
+ actions = append(actions, "surrender")
+ }
+ return actions
+}
+
+func fromSplitAces(hand *blackjackHand) bool {
+ return hand.fromSplit && len(hand.cards) > 0 && hand.cards[0].rank == "A"
+}
+
+func (t *blackjackTable) finishHand() []Event {
+ t.skipCompletedHands()
+ if t.activeName() == "" {
+ return t.settle()
+ }
+ return t.prompt()
+}
+
+func (t *blackjackTable) skipCompletedHands() {
+ for t.activePlayer < len(t.order) {
+ p := t.participants[t.order[t.activePlayer]]
+ if p == nil || !p.HasBet {
+ t.activePlayer++
+ t.activeHand = 0
+ continue
+ }
+ for t.activeHand < len(p.hands) {
+ h := p.hands[t.activeHand]
+ total, _ := h.total()
+ if h.stood || h.bust || h.surrendered || total >= 21 || h.blackjack(t.Rules) {
+ t.activeHand++
+ continue
+ }
+ return
+ }
+ t.activePlayer++
+ t.activeHand = 0
+ }
+}
+
+func (t *blackjackTable) activeName() string {
+ if t.activePlayer >= len(t.order) {
+ return ""
+ }
+ return t.order[t.activePlayer]
+}
+
+func (t *blackjackTable) dealerBlackjack() bool {
+ return t.dealer.blackjack(t.Rules)
+}
+
+func (t *blackjackTable) settle() []Event {
+ for {
+ total, soft := t.dealer.total()
+ if total > 17 || total == 17 && (!soft || !t.Rules.DealerHitsSoft17()) {
+ break
+ }
+ t.dealer.cards = append(t.dealer.cards, t.shoe.draw(t.Rules))
+ }
+ dealerTotal, _ := t.dealer.total()
+ dealerEvent := t.event("Dealer's hand:", true)
+ 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 {
+ continue
+ }
+ for _, hand := range p.hands {
+ result, payout := settleHand(hand, dealerTotal, dealerBlackjack, t.Rules)
+ if payout > 0 && p.Wallet != nil {
+ p.Wallet.PayCredits(payout)
+ }
+ publicResult, privateResult := blackjackResultMessages(name, result, payout)
+ settlement := t.playerAction(name, publicResult, privateResult)
+ if result == "blackjack" {
+ settlement.Color = "casino_jackpot_win"
+ } else if result == "win" {
+ settlement.Color = "casino_big_win"
+ }
+ events = append(events, settlement)
+ }
+ if p.insurance > 0 {
+ insurancePayout := 0
+ if dealerBlackjack {
+ insurancePayout = int(math.Round(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)))
+ }
+ }
+ t.resetRound()
+ for _, name := range t.order {
+ if p := t.participants[name]; p != nil && p.Connected {
+ events = append(events, t.menuEvent(name))
+ }
+ }
+ return events
+}
+
+func settleHand(hand *blackjackHand, dealerTotal int, dealerBlackjack bool, rules BlackjackConfig) (string, int) {
+ total, _ := hand.total()
+ if hand.surrendered {
+ return "surrender", hand.bet / 2
+ }
+ if hand.bust {
+ return "bust", 0
+ }
+ playerBlackjack := hand.blackjack(rules)
+ if playerBlackjack && dealerBlackjack {
+ return "push", hand.bet
+ }
+ if dealerBlackjack {
+ return "dealer blackjack", 0
+ }
+ if playerBlackjack {
+ return "blackjack", int(math.Round(float64(hand.bet) * (1 + rules.BlackjackPayout)))
+ }
+ if total > 21 || dealerTotal > total && dealerTotal <= 21 {
+ return "loss", 0
+ }
+ if dealerTotal == total {
+ return "push", hand.bet
+ }
+ if dealerTotal > 21 {
+ return "win", hand.bet * 2
+ }
+ return "win", hand.bet * 2
+}
+
+func (t *blackjackTable) resetRound() {
+ t.phase = blackjackWaiting
+ t.timer.stop()
+ for _, p := range t.participants {
+ p.HasBet, p.Wager, p.hands, p.insurance, p.declinedInsurance = false, Wager{}, nil, 0, false
+ }
+}
+
+func (t *blackjackTable) rebetAmount(name string) int {
+ if p := t.participants[name]; p != nil && p.lastBet > 0 {
+ return p.lastBet
+ }
+ return t.Config.MinBet
+}
+
+func (t *blackjackTable) bettorCount() int {
+ count := 0
+ for _, p := range t.participants {
+ if p.HasBet {
+ count++
+ }
+ }
+ return count
+}
+
+func (t *blackjackTable) snapshot(revealDealer bool) *BlackjackSnapshot {
+ snapshot := &BlackjackSnapshot{}
+ for i, card := range t.dealer.cards {
+ if i == 0 && !revealDealer {
+ snapshot.Dealer = append(snapshot.Dealer, BlackjackCardView{Rank: "?", Hidden: true})
+ continue
+ }
+ snapshot.Dealer = append(snapshot.Dealer, BlackjackCardView{Rank: card.rank, Suit: card.suit})
+ }
+ for _, name := range t.order {
+ p := t.participants[name]
+ if p == nil || !p.HasBet {
+ continue
+ }
+ for _, hand := range p.hands {
+ view := BlackjackPlayerView{Name: name, Score: handScore(hand)}
+ for _, card := range hand.cards {
+ view.Cards = append(view.Cards, BlackjackCardView{Rank: card.rank, Suit: card.suit})
+ }
+ snapshot.Players = append(snapshot.Players, view)
+ }
+ }
+ if len(t.dealer.cards) > 1 {
+ snapshot.DealerShownScore = handScore(&blackjackHand{cards: []blackjackCard{t.dealer.cards[1]}})
+ }
+ snapshot.DealerScore = handScore(&t.dealer)
+ snapshot.DealerRevealed = revealDealer
+ return snapshot
+}
+
+func (t *blackjackTable) menu() string {
+ return "New hand! You can bet, bet <amount> (inc. min/max), or stop."
+}
+
+func handScore(hand *blackjackHand) string {
+ total, soft := hand.total()
+ if !soft {
+ return fmt.Sprintf("%d", total)
+ }
+ return fmt.Sprintf("%d or %d", total-10, total)
+}
+
+func blackjackResultMessages(name, result string, payout int) (string, string) {
+ switch result {
+ case "win", "blackjack":
+ return name + " wins!", fmt.Sprintf("You win! +%d credits", payout)
+ case "push":
+ return name + " pushes.", fmt.Sprintf("You push. +%d credits", payout)
+ default:
+ return name + " loses.", "You lose!"
+ }
+}
+
+func (t *blackjackTable) markDisconnected(name string) []Event {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ if p := t.participants[name]; p != nil {
+ p.Connected = false
+ }
+ 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
+ }
+}
+
+func (t *blackjackTable) hasParticipant(name string) bool {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ return t.participants[name] != nil
+}