From ced77d7c89129da6ba4001ab8e3dad265aa41396 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 9 Oct 2026 17:05:54 -0400 Subject: feat: new casino game: coin flip --- internal/game/coin_flip_test.go | 162 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 internal/game/coin_flip_test.go (limited to 'internal/game/coin_flip_test.go') diff --git a/internal/game/coin_flip_test.go b/internal/game/coin_flip_test.go new file mode 100644 index 0000000..c8a5372 --- /dev/null +++ b/internal/game/coin_flip_test.go @@ -0,0 +1,162 @@ +package game + +import ( + "strings" + "testing" + + "thehouseoficarus/internal/casino" +) + +func coinFlipRoom() string { + return "name: Two Moons Flip\ncasino_tables:\n - id: coin_flip\n game: coin_flip\n min_bet: 1\n max_bet: 100\n" +} + +func TestCoinFlipPlayAndFlip(t *testing.T) { + g, s, conn := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none", "casino_hints": "on"} + g.executePlay(s, []string{"coin", "flip"}, "play coin flip") + table, ok := g.Casino.Participation(s.Player.Name) + if !ok || table == nil || table.Config.Game != casino.GameCoinFlip { + t.Fatal("Coin Flip alias did not seat the player") + } + if table.StakeMultiple() != 100 { + t.Fatalf("stake multiple %d, want 100", table.StakeMultiple()) + } + // Sitting down must not print a coin or a multiplier. + if text := string(conn.data); strings.Contains(text, "▐") || strings.Contains(text, "BLUE") { + t.Fatalf("join printed a coin: %s", text) + } + // The player only holds 100 credits, so a stake of 1 reserves 100. + conn.data = nil + g.executeBet(s, []string{"1"}, "bet 1") + if len(s.Player.PendingWagers) != 1 || s.Player.PendingWagers[0].Credits != 100 { + t.Fatalf("bet did not reserve 100x: %+v", s.Player.PendingWagers) + } + if !strings.Contains(string(conn.data), "Stake 1") { + t.Fatalf("bet did not print the stake line: %s", conn.data) + } + // The first spin frame is held back one tick. + conn.data = nil + g.CasinoTick() + if !strings.Contains(string(conn.data), "▼") { + t.Fatalf("first tick did not start the red spin: %s", conn.data) + } + // Advance until the round lands (the spin + flip plan is at most ~40 frames). + for i := 0; i < 200 && len(s.Player.PendingWagers) > 0; i++ { + conn.data = nil + g.CasinoTick() + } + if len(s.Player.PendingWagers) != 0 { + t.Fatalf("round never settled: %+v", s.Player.PendingWagers) + } + if !strings.Contains(stripANSI(string(conn.data)), "Returned:") { + t.Fatalf("settlement receipt missing: %s", conn.data) + } +} + +func TestCoinFlipStakeLimit(t *testing.T) { + g, s, _ := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none"} + g.executePlay(s, []string{"coin flip"}, "play coin flip") + // 100 credits cannot cover a stake of 2 (200 reserved). + g.executeBet(s, []string{"2"}, "bet 2") + if len(s.Player.PendingWagers) != 0 { + t.Fatalf("accepted a stake the reserve cannot cover: %+v", s.Player.PendingWagers) + } +} + +func TestCoinFlipBetMaxUsesReserve(t *testing.T) { + g, s, _ := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none"} + g.executePlay(s, []string{"coin flip"}, "play coin flip") + // 100 credits with a 100x reserve means the largest stake is 1. + g.executeBet(s, []string{"max"}, "bet max") + if len(s.Player.PendingWagers) != 1 || s.Player.PendingWagers[0].Credits != 100 { + t.Fatalf("bet max did not reserve 100x the affordable stake: %+v", s.Player.PendingWagers) + } +} + +func TestCoinFlipSettleReachesGame(t *testing.T) { + g, s, conn := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none"} + g.executePlay(s, []string{"coin flip"}, "play coin flip") + conn.data = nil + g.handleGameCommand(s, "settle") + if strings.Contains(string(conn.data), "Unknown command") { + t.Fatalf("settle was rejected before reaching Coin Flip: %s", conn.data) + } +} + +func TestCoinFlipWheelInk(t *testing.T) { + if ink, bold := coinFlipWheelInk(30, casino.CoinFlipSideRed); ink != -1 || bold { + t.Fatalf("30x = %d,%v; want plain", ink, bold) + } + if ink, bold := coinFlipWheelInk(60, casino.CoinFlipSideRed); ink != coinFlipRedInk || bold { + t.Fatalf("60x = %d,%v; want red not bold", ink, bold) + } + if ink, bold := coinFlipWheelInk(80, casino.CoinFlipSideBlue); ink != coinFlipBlueInk || !bold { + t.Fatalf("80x = %d,%v; want blue bold", ink, bold) + } + if ink, bold := coinFlipWheelInk(-90, casino.CoinFlipSideRed); ink != coinFlipRedInk || !bold { + t.Fatalf("-90x = %d,%v; want red bold", ink, bold) + } +} + +func TestCoinFlipRendering(t *testing.T) { + for _, mode := range []string{"none", "ansi", "xterm256"} { + for _, unicode := range []bool{false, true} { + spin := &casino.CoinFlipSnapshot{ + Phase: casino.CoinFlipPhaseRedSpin, + SpinSide: casino.CoinFlipSideRed, + SpinValues: []int{-10, -20, -30, -40, -50, -60, -70}, + } + rendered := renderCoinFlip(spin, mode, unicode) + if mode == "none" && strings.Contains(rendered, "\x1b") { + t.Fatalf("plain spin has ANSI escapes: %q", rendered) + } + if !strings.Contains(rendered, "▼") && unicode { + t.Fatalf("spin strip missing its pointer: %q", rendered) + } + + set := &casino.CoinFlipSnapshot{ + Phase: casino.CoinFlipPhaseRedSpin, + SpinSide: casino.CoinFlipSideRed, + SpinValues: []int{-10, -20, -30, -40, -50, -60, -70}, + Set: true, + } + if !strings.Contains(renderCoinFlip(set, mode, unicode), "SET") { + t.Fatalf("landed spin missing SET: %q", renderCoinFlip(set, mode, unicode)) + } + + results := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseResults, Red: -61, Blue: 85} + rendered = renderCoinFlip(results, mode, unicode) + if plain := stripANSI(rendered); !strings.Contains(plain, "-61x") || !strings.Contains(plain, "+85x") { + t.Fatalf("results panel missing a face: %q", plain) + } + + coin := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseFlipping, Side: casino.CoinFlipSideBlue, Multiplier: 42} + rendered = renderCoinFlip(coin, mode, unicode) + if plain := stripANSI(rendered); !strings.Contains(plain, "+42x") { + t.Fatalf("coin missing its multiplier: %q", plain) + } + if strings.Contains(rendered, "COIN FLIP") || strings.Contains(rendered, "stake") { + t.Fatalf("flip printed chrome instead of just the coin: %q", rendered) + } + + settled := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseResult, Side: casino.CoinFlipSideRed, Multiplier: -50, Result: true} + rendered = renderCoinFlip(settled, mode, unicode) + if plain := stripANSI(rendered); !strings.Contains(plain, "-50x") { + t.Fatalf("settled coin missing its multiplier: %q", plain) + } + if mode == "xterm256" && !strings.Contains(rendered, "48;5;196") { + t.Fatalf("settled red coin has no red fill: %q", rendered) + } + if mode == "ansi" && !strings.Contains(rendered, "\x1b[41m") { + t.Fatalf("settled red coin has no red fill: %q", rendered) + } + } + } + if got := renderCoinFlip(&casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseIdle}, "none", true); got != "" { + t.Fatalf("idle coin rendered %q", got) + } +} -- cgit v1.2.3