aboutsummaryrefslogtreecommitdiff
path: root/internal/game/coin_flip_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-09 17:05:54 -0400
committerhistoria <[not public]>2026-10-09 17:05:54 -0400
commitced77d7c89129da6ba4001ab8e3dad265aa41396 (patch)
tree2acfd429e5b4a83121565bf5f1bbc122228e9d8a /internal/game/coin_flip_test.go
parent6321c0a6b63061ac57a982c9ec5f93c1f731ae65 (diff)
downloadthehouseoficarus-ced77d7c89129da6ba4001ab8e3dad265aa41396.tar.gz
feat: new casino game: coin flip
Diffstat (limited to 'internal/game/coin_flip_test.go')
-rw-r--r--internal/game/coin_flip_test.go162
1 files changed, 162 insertions, 0 deletions
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)
+ }
+}