aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/baccarat.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/baccarat.go')
-rw-r--r--internal/casino/baccarat.go29
1 files changed, 20 insertions, 9 deletions
diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go
index f4f5fbd..231a011 100644
--- a/internal/casino/baccarat.go
+++ b/internal/casino/baccarat.go
@@ -173,6 +173,8 @@ func (t *baccaratTable) action(name, action string) []Event {
}
func (t *baccaratTable) rebet(name string) (string, int) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
if p := t.players.get(name); p != nil && p.lastBet > 0 {
return p.lastSpot, p.lastBet
}
@@ -182,8 +184,13 @@ func (t *baccaratTable) rebet(name string) (string, int) {
// deal plays out the hand by the punto banco tableau, settles every wager,
// and resets the table for the next round.
func (t *baccaratTable) deal() []Event {
- // The cut-card reshuffle happens between rounds, never mid-hand.
+ // The cut-card reshuffle and the round-coverage guarantee both happen at
+ // this boundary, before a single card of the coup is dealt (deal() is
+ // synchronous, so a wager here has never had a card dealt for it):
+ // draw() rebuilds an exhausted shoe in place, which mid-coup could deal
+ // a card the coup already holds.
t.shoe.shuffleIfDue()
+ t.shoe.ensureForRound(baccaratRoundCards)
player, banker := playTableau(t.shoe)
playerTotal, bankerTotal := baccaratTotal(player), baccaratTotal(banker)
round := t.event("New baccarat hand!", true)
@@ -265,19 +272,20 @@ func (t *baccaratTable) resetRound() {
}
}
-// markDisconnected removes the player from the table, forfeiting any wager
-// they committed (deal only settles seated bettors). An open betting window
-// resolves or resets on the next tick now that they no longer hold it open.
+// markDisconnected removes the player from the table. A committed wager is
+// REFUNDED rather than forfeited: deal() is synchronous, so a seated bettor
+// whose wager is still outstanding has had no card dealt for it (the coup
+// only starts once every connected seat has bet) and nothing is revealed by
+// returning the stake. An open betting window then resolves or resets on the
+// next tick now that they no longer hold it open.
func (t *baccaratTable) markDisconnected(name string) []Event {
t.mu.Lock()
defer t.mu.Unlock()
if !t.players.has(name) {
return nil
}
- // A disconnected player forfeits the round in flight: resolve the
- // stakes now so startup recovery does not refund them later.
if p := t.players.get(name); p != nil && p.HasBet && p.Wallet != nil {
- p.Wallet.ResolveStakes()
+ p.Wallet.RefundOutstanding()
}
t.players.remove(name)
events := []Event{t.event(fmt.Sprintf("%s stands up.", name), true)}
@@ -395,9 +403,11 @@ func baccaratResultMessages(name, result string, payout int) (string, string) {
}
}
-// refundAll resolves every committed wager at this table (graceful
+// refundAll refunds every committed wager at this table (graceful
// shutdown/eviction): the wallet returns every outstanding stake exactly
-// once.
+// once. It then RESETS the round, so a refunded round can never settle (and
+// pay) afterwards: calling it twice, or letting the table keep running,
+// cannot pay the same round twice.
func (t *baccaratTable) refundAll() []Event {
t.mu.Lock()
defer t.mu.Unlock()
@@ -419,6 +429,7 @@ func (t *baccaratTable) refundAll() []Event {
fmt.Sprintf("%s is refunded %d as the casino closes.", name, refund),
fmt.Sprintf("The house refunds your %d stake for the shutdown.", refund)))
}
+ t.resetRound()
return events
}