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