From d3a305d1d7962f82af2397760cf8008758bddd1d Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 2 Oct 2026 01:54:37 -0400 Subject: feat: better 'list' bet table, multiple bets in baccarat --- internal/casino/baccarat_multi_wager_test.go | 118 +++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 internal/casino/baccarat_multi_wager_test.go (limited to 'internal/casino/baccarat_multi_wager_test.go') diff --git a/internal/casino/baccarat_multi_wager_test.go b/internal/casino/baccarat_multi_wager_test.go new file mode 100644 index 0000000..2a56e63 --- /dev/null +++ b/internal/casino/baccarat_multi_wager_test.go @@ -0,0 +1,118 @@ +package casino + +import "testing" + +// TestBaccaratSideBetsRideWithMain pins the multi-wager contract: a side bet +// is committed before the main wager, both settle in the same round, and only +// the main wager deals the cards. +func TestBaccaratSideBetsRideWithMain(t *testing.T) { + cfg := TableConfig{ID: "baccarat", Game: GameBaccarat, Baccarat: BaccaratConfig{PairBets: true}} + table := newBaccaratTestTable(cfg) + table.join("alice") + forceShoe(table, baccaratCards("5 5 5 5 5 5")...) + w := &testWallet{credits: 1000} + if events := table.bet("alice", "player_pair", 10, w); hasMessage(events, "New baccarat hand!") { + t.Fatal("side bet alone dealt a round") + } + if table.players.get("alice").HasBet { + t.Fatal("side bet counted as the round's main wager") + } + events := table.bet("alice", "player", 10, w) + if !hasMessage(events, "New baccarat hand!") { + t.Fatalf("main wager did not deal: %+v", events) + } + // Player and banker tie at 5; the player main pushes and the player pair wins 11:1. + if w.paid != 130 { + t.Fatalf("paid=%d, want 130", w.paid) + } + var labels []string + for _, e := range events { + for _, line := range e.Settlement { + labels = append(labels, line.Label) + } + } + if len(labels) != 2 || labels[0] != "player" || labels[1] != "player_pair" { + t.Fatalf("settlement labels=%v, want [player player_pair]", labels) + } + // The snapshot carries both bets for the table view. + var snapshot *BaccaratSnapshot + for _, e := range events { + if s, ok := e.Snapshot.(*BaccaratSnapshot); ok && len(s.Bets) > 0 { + snapshot = s + } + } + if snapshot == nil || len(snapshot.Bets) != 2 || + snapshot.Bets[0].Spot != "player" || snapshot.Bets[0].Amount != 10 || + snapshot.Bets[1].Spot != "player_pair" || snapshot.Bets[1].Amount != 10 { + t.Fatalf("snapshot bets=%+v", snapshot) + } +} + +// A side wager may not follow the main wager: the main is the deal trigger. +func TestBaccaratSideBetAfterMainRefused(t *testing.T) { + table := newBaccaratTestTable(TableConfig{ID: "baccarat", Game: GameBaccarat, Baccarat: BaccaratConfig{PairBets: true}, MaxPlayers: 2}) + table.join("alice") + table.join("bob") + w := &testWallet{credits: 1000} + table.bet("alice", "player", 10, w) + if events := table.bet("alice", "player_pair", 10, w); !hasMessage(events, "before your main wager") { + t.Fatalf("side bet after main accepted: %+v", events) + } +} + +// A bare "bet" repeats the remembered side bets and the main wager together. +func TestBaccaratRebetAll(t *testing.T) { + cfg := TableConfig{ID: "baccarat", Game: GameBaccarat, Baccarat: BaccaratConfig{PairBets: true}} + table := newBaccaratTestTable(cfg) + table.join("alice") + forceShoe(table, baccaratCards("5 5 5 5 5 5")...) + table.bet("alice", "player_pair", 10, &testWallet{credits: 1000}) + table.bet("alice", "player", 10, &testWallet{credits: 1000}) + forceShoe(table, baccaratCards("5 5 5 5 5 5")...) + w := &testWallet{credits: 1000} + events := table.rebetAll("alice", w) + if !hasMessage(events, "New baccarat hand!") || w.paid != 130 { + t.Fatalf("rebet all did not replay both wagers: paid=%d events=%+v", w.paid, events) + } +} + +// unbet returns the main wager and every side bet in one step. +func TestBaccaratUnbetReturnsAllWagers(t *testing.T) { + table := newBaccaratTestTable(TableConfig{ID: "baccarat", Game: GameBaccarat, Baccarat: BaccaratConfig{PairBets: true}}) + table.join("alice") + w := &testWallet{credits: 1000} + table.bet("alice", "player_pair", 10, w) + table.bet("alice", "banker_pair", 15, w) + if events := table.unbetAllLocked("alice"); !hasMessage(events, "take back 25") { + t.Fatalf("unbet did not return both side stakes: %+v", events) + } + if w.paid != 25 || len(w.open) != 0 { + t.Fatalf("paid=%d open=%d, want 25/0", w.paid, len(w.open)) + } +} + +// A side-only seat is not part of the round: the timer returns the side stake +// and the other seat's main wager still deals. +func TestBaccaratSideOnlySeatRefundedOnTimeout(t *testing.T) { + cfg := TableConfig{ID: "baccarat", Game: GameBaccarat, Baccarat: BaccaratConfig{PairBets: true}, MaxPlayers: 2, BettingTicks: 2} + table := newBaccaratTestTable(cfg) + table.join("alice") + table.join("bob") + wa, wb := &testWallet{credits: 1000}, &testWallet{credits: 1000} + table.bet("alice", "player_pair", 10, wa) + table.bet("bob", "player", 10, wb) + forceShoe(table, baccaratCards("5 5 5 5 5 5")...) + var events []Event + for i := 0; i < 4 && !hasMessage(events, "New baccarat hand!"); i++ { + events = append(events, table.tick()...) + } + if !hasMessage(events, "New baccarat hand!") { + t.Fatalf("side-only seat held the round open: %+v", events) + } + if wa.paid != 10 { + t.Fatalf("alice side stake not returned: paid=%d", wa.paid) + } + if !hasMessage(events, "side bets is returned") { + t.Fatalf("missing side-bet refund notice: %+v", events) + } +} -- cgit v1.2.3