aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/craps_clock_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-30 05:33:45 -0400
committerhistoria <[not public]>2026-09-30 05:33:45 -0400
commit0fcb03633e3008169173d76c8408f5410592b3eb (patch)
tree814797fb33a129b1c49f8ae309c6647027f5e528 /internal/casino/craps_clock_test.go
parent735428f0667f67c12fdf3caca872d7dc65e2ce26 (diff)
downloadthehouseoficarus-0fcb03633e3008169173d76c8408f5410592b3eb.tar.gz
feat: bank craps added to casino
Diffstat (limited to 'internal/casino/craps_clock_test.go')
-rw-r--r--internal/casino/craps_clock_test.go309
1 files changed, 309 insertions, 0 deletions
diff --git a/internal/casino/craps_clock_test.go b/internal/casino/craps_clock_test.go
new file mode 100644
index 0000000..0e68e6d
--- /dev/null
+++ b/internal/casino/craps_clock_test.go
@@ -0,0 +1,309 @@
+package casino
+
+import (
+ "strings"
+ "testing"
+ "time"
+)
+
+func crapsEventText(events []Event) string {
+ var s strings.Builder
+ for _, e := range events {
+ s.WriteString(e.Message)
+ s.WriteByte('\n')
+ s.WriteString(e.PrivateMessage)
+ }
+ return s.String()
+}
+func TestCrapsMultiplayerClockAndLateBets(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ tb.join("bob")
+ w := &testWallet{chips: 1000}
+ tb.bet("alice", "pass", 10, w)
+ if tb.view().ShootIn != 10 || tb.view().DealerIn != 70 {
+ t.Fatal(tb.view())
+ }
+ if text := crapsEventText(tb.tickElapsed(6 * time.Second)); strings.Contains(text, "Last call") {
+ t.Fatal("early warning")
+ }
+ events := tb.tickElapsed(time.Second)
+ if !strings.Contains(crapsEventText(events), "Last call for bets! 3 seconds until alice can shoot!") {
+ t.Fatal(events)
+ }
+ if !events[0].Public {
+ t.Fatal("warning not broadcast")
+ }
+ tb.action("alice", "roll")
+ if tb.dice != [2]int{} {
+ t.Fatal("early shoot")
+ }
+ tb.tickElapsed(3 * time.Second)
+ if tb.view().ShootIn != 0 || tb.view().DealerIn != 60 {
+ t.Fatal(tb.view())
+ }
+ tb.bet("alice", "field", 10, w)
+ if len(w.open) != 2 {
+ t.Fatal("late bet rejected")
+ }
+ tb.action("alice", "unbet field")
+ if len(tb.players.get("alice").bets) != 1 {
+ t.Fatal("late removal rejected")
+ }
+ if strings.Contains(crapsEventText(tb.tickElapsed(59*time.Second)), "Last call") {
+ t.Fatal("warning repeated")
+ }
+ if tb.dice != [2]int{} {
+ t.Fatal("early forced roll")
+ }
+ tb.tickElapsed(time.Second)
+ if tb.dice == [2]int{} {
+ t.Fatal("60 second dealer fallback missing")
+ }
+}
+func TestCrapsSoloAndRosterTransitions(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ w := &testWallet{chips: 1000}
+ tb.bet("alice", "fire", 1, w)
+ tb.tickElapsed(500 * time.Second)
+ if tb.dice != [2]int{} || tb.view().DealerIn != 0 || tb.view().ShootIn != 0 {
+ t.Fatal("solo deadline", tb.view())
+ }
+ tb.action("alice", "roll")
+ if tb.dice == [2]int{} {
+ t.Fatal("solo could not roll")
+ }
+ tb.join("bob")
+ if tb.view().ShootIn != 10 {
+ t.Fatal("join did not give bettors a window")
+ }
+ tb.tickElapsed(4 * time.Second)
+ tb.bet("alice", "field", 10, w)
+ if tb.view().ShootIn != 6 {
+ t.Fatal("bet extended timer")
+ }
+ tb.leave("bob")
+ if tb.view().ShootIn != 0 || tb.view().DealerIn != 0 {
+ t.Fatal("departure left forced timer")
+ }
+ tb.join("bob")
+ tb.action("bob", "sitout")
+ if tb.view().ShootIn != 0 {
+ t.Fatal("sit-out counts as playing")
+ }
+ tb.refundAll()
+ if tb.hasLiveBets() {
+ t.Fatal("refund retained wagers")
+ }
+}
+func TestCrapsAutorollPaceAndStop(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ w := &testWallet{chips: 1000}
+ tb.bet("alice", "fire", 1, w)
+ tb.action("alice", "autoroll")
+ tb.tickElapsed(2999 * time.Millisecond)
+ if tb.dice != [2]int{} {
+ t.Fatal("autoroll fired early")
+ }
+ tb.tickElapsed(time.Millisecond)
+ if tb.dice == [2]int{} {
+ t.Fatal("autoroll did not fire")
+ }
+ if p := tb.players.get("alice"); p.HasBet && p.autoElapsed != 0 {
+ t.Fatal("pace not reset")
+ }
+ handle := &Table{tableBase: tb.tableBase, game: tb}
+ tb.players.get("alice").autoRoll = true
+ if _, stopped := handle.StopAutoRoll("alice"); !stopped {
+ t.Fatal("stop failed")
+ }
+ before := tb.dice
+ tb.tickElapsed(500 * time.Second)
+ if tb.dice != before || !tb.hasParticipant("alice") {
+ t.Fatal("stop rolled or departed")
+ }
+ if _, stopped := handle.StopAutoRoll("alice"); stopped {
+ t.Fatal("manual stop incorrectly consumed")
+ }
+ // A one-roll bet ends autoroll immediately, regardless of the outcome.
+ tb = crapsFixture()
+ tb.join("alice")
+ w = &testWallet{chips: 100}
+ tb.bet("alice", "field", 10, w)
+ tb.action("alice", "autoroll")
+ tb.tickElapsed(3 * time.Second)
+ if tb.players.get("alice").autoRoll || tb.players.get("alice").HasBet {
+ t.Fatal("autoroll survived last bet")
+ }
+ before = tb.dice
+ tb.tickElapsed(3 * time.Second)
+ if tb.dice != before {
+ t.Fatal("extra roll after completion")
+ }
+}
+func TestCrapsAutorollRespectsOtherPlayers(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ tb.join("bob")
+ w := &testWallet{chips: 100}
+ tb.bet("alice", "field", 10, w)
+ tb.action("alice", "autoroll")
+ tb.tickElapsed(3 * time.Second)
+ if tb.dice != [2]int{} {
+ t.Fatal("autoroll stole multiplayer betting time")
+ }
+ tb.tickElapsed(7 * time.Second)
+ if tb.dice == [2]int{} {
+ t.Fatal("autoroll failed when ready")
+ }
+ // A non-shooter's opt-in never takes the dice away from the current shooter.
+ tb = crapsFixture()
+ tb.join("alice")
+ tb.join("bob")
+ w = &testWallet{chips: 100}
+ tb.bet("bob", "field", 10, w)
+ tb.action("bob", "autoroll")
+ tb.tickElapsed(11 * time.Second)
+ if tb.dice != [2]int{} {
+ t.Fatal("non-shooter autorolled")
+ }
+}
+func TestCrapsManagerUsesElapsedCadence(t *testing.T) {
+ for _, step := range []time.Duration{100 * time.Millisecond, 600 * time.Millisecond, time.Second} {
+ m := NewManager(4)
+ cfg := TableConfig{ID: "c", Game: GameCraps}
+ m.Join(1, cfg, "alice")
+ w := &testWallet{chips: 100}
+ m.Bet(1, "c", "alice", "field", 10, w)
+ m.Action(1, "c", "alice", "autoroll")
+ tb := m.Table(1, "c").game.(*crapsTable)
+ for elapsed := step; elapsed < 3*time.Second; elapsed += step {
+ m.TickElapsed(step)
+ if tb.dice != [2]int{} {
+ t.Fatal("early", step)
+ }
+ }
+ m.TickElapsed(step)
+ if tb.dice == [2]int{} {
+ t.Fatal("wrong cadence", step)
+ }
+ }
+}
+func TestCrapsConsolidatedResultsAndBetNumbers(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ w := &testWallet{chips: 1000}
+ events := tb.bet("alice", "pass", 10, w)
+ if !strings.Contains(crapsEventText(events), "ROLL RESULTS ON YOUR BETS") {
+ t.Fatal("no post-bet roll table")
+ }
+ tb.bet("alice", "dontpass", 10, w)
+ tb.resolveRoll(2, 3)
+ events = tb.resolveRoll(3, 4)
+ balances, settlements := 0, 0
+ for _, e := range events {
+ if e.ShowBalance {
+ balances++
+ }
+ if strings.Contains(e.Message, "Pass [5]:") || strings.Contains(e.Message, "Pass [5]") {
+ settlements++
+ if !strings.Contains(e.Message, "Don't pass [5]") {
+ t.Fatal("not combined", e.Message)
+ }
+ }
+ }
+ if balances != 1 || settlements != 1 {
+ t.Fatal("duplicate balance or separate results", balances, settlements, crapsEventText(events))
+ }
+ text := crapsEventText(events)
+ for _, ugly := range []string{"returned (stake", "Craps: bets open", "Phase: waiting"} {
+ if strings.Contains(text, ugly) {
+ t.Fatal(ugly)
+ }
+ }
+ if !strings.Contains(text, "{casino_result_loss}") || !strings.Contains(text, "{casino_result_win}") {
+ t.Fatal("missing outcome styles")
+ }
+}
+
+func TestCrapsUnbetAllAndRollTableFormat(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ w := &testWallet{chips: 1000}
+ tb.bet("alice", "fire", 1, w)
+ tb.bet("alice", "pass", 10, w)
+ tb.resolveRoll(3, 3)
+ tb.bet("alice", "odds pass", 20, w)
+ tb.bet("alice", "place 4", 10, w)
+ tb.bet("alice", "field", 10, w)
+ before := w.paid
+ events := tb.action("alice", "unbet all")
+ text := crapsEventText(events)
+ if !strings.Contains(text, "take down {casino_chips}40{/}") || !strings.Contains(text, "contracts stay up") {
+ t.Fatal(text)
+ }
+ if w.paid != before+40 {
+ t.Fatal("refund amount", w.paid-before)
+ }
+ bets := tb.players.get("alice").bets
+ if len(bets) != 2 || bets[0].kind != "fire" || bets[1].kind != "pass" || bets[1].odds.Total != 0 {
+ t.Fatal("contracts or odds mishandled", bets)
+ }
+ if tb.hasLiveBets() != true || len(w.open) != 2 {
+ t.Fatal("pass and fire stakes must stay pending", w.open)
+ }
+ tb.action("alice", "unbet all")
+ if w.paid != before+40 {
+ t.Fatal("second unbet all moved money", w.paid)
+ }
+ // Roll table formatting: grouped totals, single-level tags only.
+ tb2 := crapsFixture()
+ tb2.join("alice")
+ w2 := &testWallet{chips: 100}
+ tb2.bet("alice", "pass", 1, w2)
+ table := tb2.rollTable(tb2.players.get("alice"))
+ for _, tag := range []string{"{casino_payout}7{/}", "{casino_result_win}+1{/}", "{casino_payout}2{/}", "{casino_result_loss}-1{/}"} {
+ if !strings.Contains(table, tag) {
+ t.Fatal("missing", tag, table)
+ }
+ }
+ if strings.Contains(table, "{casino_muted}") || strings.Contains(table, "{casino_result_win}+{") {
+ t.Fatal("nested or unknown tag", table)
+ }
+}
+
+func TestCrapsRollTableHardPairLabels(t *testing.T) {
+ tb := crapsFixture()
+ tb.join("alice")
+ w := &testWallet{chips: 100}
+ tb.bet("alice", "hard 8", 10, w)
+ tb.bet("alice", "hop 2 3", 10, w)
+ tb.point = 8
+ table := tb.rollTable(tb.players.get("alice"))
+ for _, want := range []string{"{casino_payout}4+4{/}", "{casino_result_win}+90{/}", "{casino_payout}2+3{/}", "{casino_result_win}+150{/}", "{casino_payout}easy 8{/}", "{casino_payout}any other{/}"} {
+ if !strings.Contains(table, want) {
+ t.Fatal("missing", want, table)
+ }
+ }
+ // Bets paying on the same total combine into one row.
+ tb2 := crapsFixture()
+ tb2.join("alice")
+ w2 := &testWallet{chips: 100}
+ tb2.bet("alice", "field", 10, w2)
+ tb2.bet("alice", "place 8", 12, w2)
+ tb2.point = 8
+ table = tb2.rollTable(tb2.players.get("alice"))
+ if strings.Count(table, "{casino_payout}8{/}") != 1 || !strings.Contains(table, "{casino_result_win}+4{/}") {
+ t.Fatal("same-label payouts must combine", table)
+ }
+ // Hard bets alone on the come-out pay nothing and say so.
+ tb3 := crapsFixture()
+ tb3.join("alice")
+ w3 := &testWallet{chips: 100}
+ tb3.bet("alice", "hard 8", 10, w3)
+ if text := tb3.rollTable(tb3.players.get("alice")); !strings.Contains(text, "OFF on the come-out") {
+ t.Fatal(text)
+ }
+}