aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_casino_settlement_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-01 22:47:41 -0400
committerhistoria <[not public]>2026-10-01 22:47:41 -0400
commit8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (patch)
tree63698c0618f382a553d4558a33e3d2087039c378 /internal/game/render_casino_settlement_test.go
parent6d9908b29c8187196d23b126b1165c7e0bd0fc76 (diff)
downloadthehouseoficarus-8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0.tar.gz
feat: consistent wager outcome table across casino games
Diffstat (limited to 'internal/game/render_casino_settlement_test.go')
-rw-r--r--internal/game/render_casino_settlement_test.go161
1 files changed, 161 insertions, 0 deletions
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))
+ }
+}