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)) } }