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