package casino import ( "strings" "testing" ) func TestCrapsPointPromptOnlyForPassBettors(t *testing.T) { tb := crapsFixture() tb.join("alice") // shooter tb.join("bob") w := &testWallet{chips: 1000} tb.bet("alice", "pass", 10, w) tb.bet("bob", "field", 10, w) events := tb.resolveRoll(2, 2) // establishes point 4 publicPoint := false var privateTo []string for _, e := range events { if e.Public && strings.Contains(e.Message, "POINT 4") { publicPoint = true if strings.Contains(e.Message, "Make your point") { t.Fatalf("public point event still urges the whole room: %q", e.Message) } } if e.PrivateTo != "" && strings.Contains(e.Message, "Make your point") { privateTo = append(privateTo, e.PrivateTo) } } if !publicPoint { t.Fatal("no public point event") } if len(privateTo) != 1 || privateTo[0] != "alice" { t.Fatalf("Make your point targeted %v, want [alice]", privateTo) } } func TestCrapsVigOnlyOnWinsRoundedDown(t *testing.T) { buy := &crapsBet{kind: "buy", number: 4, stake: Wager{Total: 20}} if got := crapsReturn(buy, 2, 1, 0, false); got != 59 { t.Fatalf("winning buy = %d, want 59", got) } if got := crapsReturn(buy, -1, 1, 0, false); got != 0 { t.Fatalf("losing buy returned %d, want 0", got) } // A directly-constructed odd stake proves the vig rounds down, not up: // 5% of 21 is 1.05, so the house keeps 1 credit, not 2. odd := &crapsBet{kind: "buy", number: 4, stake: Wager{Total: 21}} if got := crapsReturn(odd, 2, 1, 0, false); got != 62 { // 21 + 42 - 1 t.Fatalf("buy vig rounded up: %d, want 62", got) } } func TestCrapsVigUnitsRoundUp(t *testing.T) { for _, tc := range []struct { kind string n int want int }{ {"buy", 4, 20}, {"buy", 6, 20}, {"lay", 4, 40}, {"lay", 5, 30}, {"lay", 6, 24}, {"lay", 2, 120}, {"lay", 3, 60}, {"lay", 12, 120}, } { if got := crapsVigUnit(tc.kind, tc.n); got != tc.want { t.Fatalf("%s %d unit = %d, want %d", tc.kind, tc.n, got, tc.want) } } for _, tc := range []struct { spot string amount int want int }{ {"buy 6", 3, 20}, {"buy 6", 5, 20}, {"buy 6", 20, 20}, {"buy 4", 3, 20}, {"buy 4", 21, 40}, {"lay 6", 30, 48}, {"lay 6", 24, 24}, } { tb := crapsFixture() tb.point = 6 tb.join("a") w := &testWallet{chips: 1000} events := tb.bet("a", tc.spot, tc.amount, w) bets := tb.players.get("a").bets if len(bets) != 1 { t.Fatalf("%s %d was rejected", tc.spot, tc.amount) } if got := bets[0].stake.Total; got != tc.want { t.Fatalf("%s %d rounded to %d, want %d", tc.spot, tc.amount, got, tc.want) } if tc.amount != tc.want { notice := false for _, e := range events { if e.PrivateTo == "a" && strings.Contains(e.PrivateMessage, "Rounded up") { notice = true } } if !notice { t.Fatalf("%s %d rounded without a notice", tc.spot, tc.amount) } } } } func TestCrapsBusyHoldsAcrossPendingRoll(t *testing.T) { tb := crapsFixture() tb.join("alice") tb.bet("alice", "pass", 10, &testWallet{chips: 100}) if tb.busy("alice") || tb.busy("bob") { t.Fatal("table busy before a roll") } tb.action("alice", "roll") if !tb.busy("alice") { t.Fatal("shooter not busy while the dice are in the air") } if tb.busy("bob") { t.Fatal("non-shooter marked busy") } tb.advance(crapsManualRollTicks) if tb.busy("alice") { t.Fatal("still busy after the roll resolved") } } func TestCrapsTakenDownRollDoesNotCarryIntoNewBet(t *testing.T) { tb := crapsFixture() tb.join("alice") w := &testWallet{chips: 100} tb.bet("alice", "field", 10, w) tb.action("alice", "roll") tb.tick() tb.action("alice", "unbet") if tb.pendingRollTicks != 0 { t.Fatal("taking down the last bet did not cancel the pending roll") } tb.bet("alice", "field", 10, w) tb.advance(10) if tb.dice != [2]int{} || !tb.hasLiveBets() { t.Fatal("new wager inherited a canceled roll") } tb.action("alice", "roll") tb.advance(crapsManualRollTicks) if tb.dice == [2]int{} || tb.hasLiveBets() { t.Fatal("fresh manual roll did not settle") } } func TestSimplifiedCrapsDeparturePreservesCountdown(t *testing.T) { for _, departing := range []string{"alice", "bob"} { t.Run(departing, func(t *testing.T) { tb := variantFixture("simplified") tb.join("alice") tb.join("bob") tb.join("carol") tb.bet("carol", "pass", 10, &testWallet{chips: 100}) tb.tick() tb.leave(departing) if tb.countdown != 1 || tb.view().ShootIn != 2 { t.Fatal("departure reset the shared countdown") } events := tb.tick() if len(events) != 1 || tb.countdown != 2 { t.Fatal("countdown repeated after departure", events) } tb.tick() if tb.dice == [2]int{} || tb.hasLiveBets() { t.Fatal("shared roll did not settle on schedule") } }) } }