From 8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 1 Oct 2026 22:47:41 -0400 Subject: feat: consistent wager outcome table across casino games --- internal/game/render_casino_settlement_test.go | 161 +++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 internal/game/render_casino_settlement_test.go (limited to 'internal/game/render_casino_settlement_test.go') diff --git a/internal/game/render_casino_settlement_test.go b/internal/game/render_casino_settlement_test.go new file mode 100644 index 0000000..f26a174 --- /dev/null +++ b/internal/game/render_casino_settlement_test.go @@ -0,0 +1,161 @@ +package game + +import ( + "fmt" + "strings" + "testing" + + "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func plainReceipt(_ string, text string) string { return text } + +func TestCasinoReceiptAccountingAndBorders(t *testing.T) { + lines := []casino.SettlementLine{ + {Label: "Pass line", Stake: 100, Returned: 200, Odds: "1:1", Result: "win"}, + {Label: "Field", Stake: 25, Odds: "Varies", Result: "loss"}, + {Label: "Place 6", Stake: 60, Odds: "7:6", Unresolved: true}, + {Label: "Push", Stake: 10, Returned: 10, Odds: "1:1", Result: "push"}, + } + for _, unicode := range []bool{true, false} { + got := renderCasinoSettlement(lines, unicode, plainReceipt) + for _, want := range []string{"Wager", "Staked", "Odds", "WIN", "LOSS", "PUSH", "UNRESOLVED", "Settled: 135", "Returned: 210", "Active: 60", "NET PROFIT", "+75 credits"} { + if !strings.Contains(got, want) { + t.Fatalf("missing %q:\n%s", want, got) + } + } + if strings.Contains(got, "PAYOUT") || strings.Contains(got, "-60") { + t.Fatal(got) + } + width := color.VisibleLen(strings.Split(got, "\n")[0]) + for _, line := range strings.Split(got, "\n") { + if color.VisibleLen(line) != width || width > 79 { + t.Fatalf("misaligned width=%d:\n%s", width, got) + } + if !unicode { + for _, r := range line { + if r > 127 { + t.Fatalf("non-ASCII %q", line) + } + } + } + } + } +} + +func TestCasinoReceiptColorIsIndependentOfGameResult(t *testing.T) { + var tags []string + got := renderCasinoSettlement([]casino.SettlementLine{{Label: "Bonus hand", Stake: 10, Returned: 5, Odds: "Varies", Result: "win"}}, true, func(tag, text string) string { + tags = append(tags, tag+":"+strings.TrimSpace(text)) + return color.Render("ansi", color.ColorSpec{Fg: 2, Bg: -1}, text) + }) + for _, want := range []string{"casino_result_win:WIN", "casino_result_loss:-5"} { + found := false + for _, tag := range tags { + found = found || tag == want + } + if !found { + t.Fatalf("missing %q: %v", want, tags) + } + } + width := color.VisibleLen(strings.Split(got, "\n")[0]) + for _, line := range strings.Split(got, "\n") { + if color.VisibleLen(line) != width { + t.Fatal(got) + } + } +} + +func TestCasinoReceiptLargeTotalsAndLongLabels(t *testing.T) { + var lines []casino.SettlementLine + for i := 1; i <= 100; i++ { + lines = append(lines, casino.SettlementLine{Label: fmt.Sprintf("Hand %d - Four aces with a long kicker description", i), Stake: 1000000000, Returned: 2000000000, Odds: "19:20", Result: "win"}) + } + lines = append(lines, casino.SettlementLine{Label: "Active", Stake: 1000000000, Odds: "1:1", Unresolved: true}) + got := renderCasinoSettlement(lines, true, plainReceipt) + for _, want := range []string{"Hand 100", "100000000000", "200000000000", "+100000000000 credits"} { + if !strings.Contains(got, want) { + t.Fatalf("missing %s", want) + } + } + width := color.VisibleLen(strings.Split(got, "\n")[0]) + for _, line := range strings.Split(got, "\n") { + if color.VisibleLen(line) != width || width > 79 { + t.Fatalf("width %d; bad line %q", width, line) + } + } +} + +func TestCasinoReceiptRevealThenReceiptThenMenu(t *testing.T) { + p := player.New("alice") + p.Options = map[string]any{"color": "none", "unicode": false} + conn := &casinoTestConn{} + sess := &net.Session{Player: p, Conn: conn} + g := &Game{Deps: Deps{World: world.New(t.TempDir())}, Combat: combat.NewTracker(), loggedInChars: map[string]*net.Session{"alice": sess}} + g.emitCasinoEvents([]casino.Event{{PrivateTo: "alice", Color: "casino_menu", + Snapshot: &casino.VideoPokerSnapshot{Config: (casino.VideoPokerConfig{}).Normalize(), Result: "REVEAL MARKER"}, + Menu: &casino.MenuView{Heading: "NEXT ACTION MARKER"}, + Settlement: []casino.SettlementLine{{Label: "Hand", Stake: 10, Returned: 20, Odds: "1:1", Result: "win"}}, + }}) + got := string(conn.data) + reveal, receipt, menu := strings.Index(got, "REVEAL MARKER"), strings.Index(got, "NET PROFIT"), strings.Index(got, "NEXT ACTION MARKER") + if reveal < 0 || receipt < reveal || menu < receipt { + t.Fatal(got) + } +} + +func TestCasinoReceiptIsPrivateEvenOnPublicResult(t *testing.T) { + hub := net.NewHub() + g := &Game{Hub: hub, Casino: casino.NewManager(1), loggedInChars: map[string]*net.Session{}} + conns := map[string]*casinoTestConn{} + for _, name := range []string{"alice", "bob"} { + p := player.New(name) + p.Options = map[string]any{"color": "none"} + conn := &casinoTestConn{} + sess := &net.Session{Player: p, Conn: conn} + hub.Add(sess) + hub.EnterRoom(sess, 1) + g.loggedInChars[name] = sess + conns[name] = conn + } + g.emitCasinoEvents([]casino.Event{{RoomID: 1, TableID: "test", Public: true, Message: "Alice wins.", PrivateTo: "alice", Settlement: []casino.SettlementLine{{Label: "SECRET WAGER", Stake: 10, Returned: 20, Result: "win"}}}}) + if !strings.Contains(string(conns["alice"].data), "SECRET WAGER") || strings.Contains(string(conns["bob"].data), "SECRET WAGER") || !strings.Contains(string(conns["bob"].data), "Alice wins.") { + t.Fatalf("alice=%s bob=%s", conns["alice"].data, conns["bob"].data) + } +} + +func TestCasinoReceiptUnresolvedIsNotPush(t *testing.T) { + got := renderCasinoSettlement([]casino.SettlementLine{{Label: "Pass line", Stake: 100, Unresolved: true}}, false, plainReceipt) + if !strings.Contains(got, "NET PENDING") || strings.Contains(got, "NET EVEN") || strings.Contains(got, "PUSH") { + t.Fatal(got) + } +} + +func TestCasinoSettlementReplacesFallbackAndPreservesPartialFailure(t *testing.T) { + p := player.New("alice") + p.Options = map[string]any{"color": "none", "unicode": false} + conn := &casinoTestConn{} + sess := &net.Session{Player: p, Conn: conn} + g := &Game{loggedInChars: map[string]*net.Session{"alice": sess}} + g.emitCasinoEvents([]casino.Event{{PrivateTo: "alice", PrivateMessage: "OLD PAYOUT", Settlement: []casino.SettlementLine{{Label: "Blackjack", Odds: "3:2", Stake: 100, Returned: 250, Result: "blackjack"}}, SettlementNote: "Another wager is pending recovery."}}) + got := string(conn.data) + for _, want := range []string{"Blackjack", "3:2", "+150 credits", "pending recovery"} { + if !strings.Contains(got, want) { + t.Fatal(got) + } + } + if strings.Contains(got, "OLD PAYOUT") || strings.Contains(got, "\x1b[") || strings.Contains(got, "╭") { + t.Fatal(got) + } + conn.data = nil + // Structured-only events must survive the empty-message filters. + g.emitCasinoEvents([]casino.Event{{PrivateTo: "alice", Settlement: []casino.SettlementLine{{Label: "Spin", Stake: 10, Result: "loss"}}}}) + if !strings.Contains(string(conn.data), "NET LOSS") { + t.Fatal(string(conn.data)) + } +} -- cgit v1.2.3