aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-29 14:23:52 -0400
committerhistoria <[not public]>2026-09-29 14:23:52 -0400
commit004dcd3826856870b267a55dacf84aa7742b7e3f (patch)
tree3789558833d8486153271e818e7579e86721855e
parent3db3071c444abb2133e1fcaf7e685095415c1adf (diff)
downloadthehouseoficarus-004dcd3826856870b267a55dacf84aa7742b7e3f.tar.gz
fix: detailed casino output by default, compact output overrides card size
-rw-r--r--building_guide/rooms.md4
-rw-r--r--internal/casino/baccarat.go4
-rw-r--r--internal/casino/baccarat_test.go24
-rw-r--r--internal/casino/blackjack.go36
-rw-r--r--internal/casino/blackjack_test.go8
-rw-r--r--internal/casino/table_ux_test.go32
-rw-r--r--internal/game/casino.go5
-rw-r--r--internal/game/casino_test.go27
-rw-r--r--internal/game/casino_ux.go2
-rw-r--r--internal/player/player.go4
10 files changed, 118 insertions, 28 deletions
diff --git a/building_guide/rooms.md b/building_guide/rooms.md
index af19119..6f9e957 100644
--- a/building_guide/rooms.md
+++ b/building_guide/rooms.md
@@ -342,7 +342,9 @@ keys may define Unicode art for sizes 5, 7, and 9. Missing art uses the
generated renderer. Players can choose `option card_size 1`, `5`, `7`, or `9`;
size 1 uses Unicode playing-card symbols. ASCII mode always uses generated
5x5 cards. `option card_style light` adds a white card background; `dark` is
-the default style. The same art renders baccarat hands.
+the default style. The same art renders baccarat hands. `option casino_display`
+only controls menu verbosity (`detailed` is the default; `compact` shortens
+menus and action hints) and never changes card size.
Baccarat tables play punto banco: every participant bets on the player, the
banker, or a tie, then the dealer plays out both hands by fixed drawing rules
diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go
index e34518f..ad56da4 100644
--- a/internal/casino/baccarat.go
+++ b/internal/casino/baccarat.go
@@ -192,7 +192,9 @@ func (t *baccaratTable) deal() []Event {
publicResult = p.Name + "’s settlement is pending recovery."
}
settlement := t.playerAction(p.Name, publicResult, privateResult)
- settlement.ShowBalance = !payoutFailed
+ // Returning players get their balance in the next bet menu; only
+ // departures/sit-outs need it attached to the settlement.
+ settlement.ShowBalance = !payoutFailed && (p.LeaveAfter || p.SitOutAfter || !p.Connected)
if result == "win" {
if p.lastSpot == baccaratSpotTie {
settlement.Color = "casino_jackpot_win"
diff --git a/internal/casino/baccarat_test.go b/internal/casino/baccarat_test.go
index 12dc07c..f947f5b 100644
--- a/internal/casino/baccarat_test.go
+++ b/internal/casino/baccarat_test.go
@@ -162,6 +162,30 @@ func TestBaccaratSoloBetDealsImmediately(t *testing.T) {
}
}
+func TestBaccaratSettlementShowsBalanceOnlyWithoutNextMenu(t *testing.T) {
+ for _, leaving := range []bool{false, true} {
+ table := newBaccaratTestTable(TableConfig{ID: "baccarat", Game: GameBaccarat})
+ table.join("alice")
+ forceShoe(table, card{rank: "5"}, card{rank: "4"}, card{rank: "2"}, card{rank: "3"})
+ if leaving {
+ table.players.get("alice").LeaveAfter = true
+ }
+ events := table.bet("alice", baccaratSpotPlayer, 10, &testWallet{credits: 100})
+ found := false
+ for _, e := range events {
+ if e.PrivateTo == "alice" && strings.Contains(e.PrivateMessage, "WIN | Net") {
+ found = true
+ if e.ShowBalance != leaving {
+ t.Fatalf("leaving=%v settlement ShowBalance=%v: %+v", leaving, e.ShowBalance, e)
+ }
+ }
+ }
+ if !found {
+ t.Fatalf("leaving=%v missing settlement: %+v", leaving, events)
+ }
+ }
+}
+
func TestBaccaratWaitsForAllPlayersToBet(t *testing.T) {
table := newBaccaratTestTable(TableConfig{ID: "baccarat", Game: GameBaccarat, MaxPlayers: 2})
table.join("alice")
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
index 2e7d7e9..0a5c0d1 100644
--- a/internal/casino/blackjack.go
+++ b/internal/casino/blackjack.go
@@ -5,7 +5,6 @@ package casino
import (
"fmt"
"math/rand"
- "strings"
)
// Blackjack uses the shared table phases; these aliases keep the rule
@@ -550,24 +549,22 @@ func (t *blackjackTable) prompt() []Event {
func (t *blackjackTable) turnMenuLocked(name string) Event {
p := t.players.get(name)
- h := p.hands[t.activeHand]
- ranks := make([]string, len(h.cards))
- for i, card := range h.cards {
- ranks[i] = card.rank
- }
- actions := t.availableActions(p, h)
- e := t.private(name, fmt.Sprintf("Your turn (%s).\nActions: %s", strings.Join(ranks, ","), strings.Join(actions, ", ")))
+ hand := p.hands[t.activeHand]
+ actions := t.availableActions(p, hand)
+ // The table snapshot already shows the active hand's cards, score, wager,
+ // and "Your turn" status; the menu only needs the deadline and actions.
+ e := t.private(name, "Your turn.")
e.Color = "casino_menu"
deadline := t.timer.remaining()
if p.Connected && t.bettorCount() <= 1 {
deadline = t.ceiling.remaining()
}
- e.Menu = &MenuView{Heading: fmt.Sprintf("Your turn (%s) | Hand %d/%d | %s | Wager %d | %d ticks remaining", strings.Join(ranks, ","), t.activeHand+1, len(p.hands), handScore(h), h.bet, deadline), Commands: blackjackActionCommands(actions)}
+ e.Menu = &MenuView{Heading: fmt.Sprintf("Your turn | %d ticks remaining", deadline), Commands: blackjackActionCommands(actions)}
for i := range e.Menu.Commands {
c := &e.Menu.Commands[i]
if c.Command == "double" || c.Command == "split" {
- c.Cost = h.bet
- c.Desc += fmt.Sprintf(" (+%d credits)", h.bet)
+ c.Cost = hand.bet
+ c.Desc += fmt.Sprintf(" (+%d credits)", hand.bet)
}
}
return e
@@ -709,12 +706,19 @@ func (t *blackjackTable) settle() []Event {
}
}
if !roundFailed {
- e := t.private(p.Name, fmt.Sprintf("Round complete | Net %+d credits (insurance included)", roundNet))
- e.ShowBalance = true
- if roundNet > 0 {
- e.Color = "casino_big_win"
+ // A single settled hand with no insurance already reported its net
+ // in the per-hand result line, so a summary is only noise. Keep it
+ // for split hands and insurance, and whenever the player will not
+ // receive the next-round menu that carries their balance.
+ needsBalance := p.LeaveAfter || p.SitOutAfter || !p.Connected
+ if len(p.hands) > 1 || p.insurance > 0 || needsBalance {
+ e := t.private(p.Name, fmt.Sprintf("Round complete | Net %+d credits (insurance included)", roundNet))
+ e.ShowBalance = needsBalance
+ if roundNet > 0 {
+ e.Color = "casino_big_win"
+ }
+ events = append(events, e)
}
- events = append(events, e)
}
// Each hand's stakes were already consumed per-handle by
// settleStakeOutcome (a payout removes them in PayCredits; a loss
diff --git a/internal/casino/blackjack_test.go b/internal/casino/blackjack_test.go
index dd3d83a..08069ef 100644
--- a/internal/casino/blackjack_test.go
+++ b/internal/casino/blackjack_test.go
@@ -127,10 +127,10 @@ func TestBlackjackPromptNamesActiveHandAndOnlyOffersAvailableActions(t *testing.
table.activeHand = 0
table.dealer.cards = []card{{rank: "10"}, {rank: "7"}}
event := table.prompt()[0]
- if event.Message != "Your turn (K,6).\nActions: hit, stand, double" {
+ if event.Message != "Your turn." {
t.Fatalf("prompt=%q", event.Message)
}
- if event.Menu == nil || !strings.Contains(event.Menu.Heading, "Hand 1/1 | 16 | Wager 10") || event.Menu.ShowBalance ||
+ if event.Menu == nil || !strings.Contains(event.Menu.Heading, "Your turn |") || event.Menu.ShowBalance ||
len(event.Menu.Commands) != 3 || event.Menu.Commands[0].Command != "hit" || event.Menu.Commands[1].Command != "stand" || event.Menu.Commands[2].Command != "double" {
t.Fatalf("prompt menu=%+v", event.Menu)
}
@@ -199,7 +199,7 @@ func TestBlackjackInsuranceDeclineContinuesRound(t *testing.T) {
if table.phase != blackjackPlaying {
t.Fatalf("phase=%s, want playing after insurance declined", table.phase)
}
- if !hasMessage(events, "Your turn (10,8).") {
+ if !hasMessage(events, "Your turn.") {
t.Fatalf("round did not continue after insurance: %+v", events)
}
}
@@ -280,7 +280,7 @@ func TestBlackjackDisconnectActivePassesPlay(t *testing.T) {
t.Fatalf("alice's forfeited wager was paid: %d", wa.paid)
}
// Bob is now prompted for his turn (10,6 against dealer 10-up).
- if !hasMessage(disEvt, "Your turn (10,6)") {
+ if !hasMessage(disEvt, "Your turn.") {
t.Fatalf("play did not pass to bob after alice left: %+v", disEvt)
}
// Bob stands; round settles.
diff --git a/internal/casino/table_ux_test.go b/internal/casino/table_ux_test.go
index 7f93951..812bed0 100644
--- a/internal/casino/table_ux_test.go
+++ b/internal/casino/table_ux_test.go
@@ -161,6 +161,38 @@ func TestSplitAndInsuranceRoundNetIncludesEveryStake(t *testing.T) {
}
}
+func TestSingleHandSettlementKeepsSummaryQuiet(t *testing.T) {
+ // A connected player with one hand and no insurance already saw the net in
+ // the per-hand result; the next bet menu carries the balance.
+ table := uxBlackjack()
+ table.dealer.cards = []card{{rank: "10", suit: "spades"}, {rank: "7", suit: "hearts"}}
+ events := table.settle()
+ if hasMessage(events, "Round complete") {
+ t.Fatalf("single-hand settlement repeated the net: %+v", events)
+ }
+ for _, e := range events {
+ if e.ShowBalance {
+ t.Fatalf("balance attached to a settlement whose next menu carries it: %+v", e)
+ }
+ }
+}
+
+func TestDepartingSettlementCarriesBalanceSummary(t *testing.T) {
+ table := uxBlackjack()
+ table.dealer.cards = []card{{rank: "10", suit: "spades"}, {rank: "7", suit: "hearts"}}
+ table.players.get("alice").LeaveAfter = true
+ events := table.settle()
+ found := false
+ for _, e := range events {
+ if strings.Contains(e.Message, "Round complete") && e.ShowBalance {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatalf("departing player lost the balance summary: %+v", events)
+ }
+}
+
func TestRebetMenusAndMidRoundJoin(t *testing.T) {
table := uxBlackjack()
events := table.join("bob")
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 230108f..8c10dfd 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -672,10 +672,9 @@ func (g *Game) renderCasinoSnapshot(sess *net.Session, snapshot casino.Snapshot)
}
mode := g.colorMode(sess)
unicode := sess.Player.OptionBool("unicode")
+ // Card art is governed by card_size alone; casino_display only controls
+ // how verbose the command menus are.
size := sess.Player.OptionInt("card_size")
- if sess.Player.OptionString("casino_display") == "compact" {
- size = 1
- }
switch s := snapshot.(type) {
case *casino.RouletteSnapshot:
return renderRouletteSnapshot(s, mode, unicode, size)
diff --git a/internal/game/casino_test.go b/internal/game/casino_test.go
index b3bfb9b..3a1fc41 100644
--- a/internal/game/casino_test.go
+++ b/internal/game/casino_test.go
@@ -26,6 +26,33 @@ func (c *casinoTestConn) SetWriteDeadline(time.Time) error { return nil }
func (c *casinoTestConn) Close() error { return nil }
func (c *casinoTestConn) SetEcho(bool) error { return nil }
+func TestCasinoDisplayDefaultsToDetailedAndDoesNotOverrideCardSize(t *testing.T) {
+ snapshot := &casino.BlackjackSnapshot{
+ Dealer: []casino.CardView{{Rank: "K", Suit: "hearts"}},
+ Players: []casino.BlackjackPlayerView{{Name: "alice", Score: "10", Cards: []casino.CardView{{Rank: "10", Suit: "clubs"}}}},
+ }
+ if got := player.New("alice").OptionString("casino_display"); got != "detailed" {
+ t.Fatalf("casino_display default=%q, want detailed", got)
+ }
+ for _, display := range []string{"", "detailed", "compact"} {
+ p := player.New("alice")
+ p.Options = map[string]any{"color": "none"}
+ if display != "" {
+ p.Options["casino_display"] = display
+ }
+ sess := &net.Session{Player: p}
+ g := &Game{}
+ got := g.renderCasinoSnapshot(sess, snapshot)
+ want := renderBlackjackSnapshotFor(snapshot, g.colorMode(sess), p.OptionBool("unicode"), p.OptionInt("card_size"), p.OptionString("card_style"), nil, p.Name)
+ if got != want {
+ t.Fatalf("display=%q: card_size ignored\n got %q\nwant %q", display, got, want)
+ }
+ if !strings.Contains(got, "┌") {
+ t.Fatalf("display=%q: expected card art, got %q", display, got)
+ }
+ }
+}
+
func TestExecuteBetRejectsInvalidMachineArgs(t *testing.T) {
g := &Game{Casino: casino.NewManager(1)}
cfg := casino.MachineConfig{ID: "slots", Game: casino.GameSlots}
diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go
index d885665..e218653 100644
--- a/internal/game/casino_ux.go
+++ b/internal/game/casino_ux.go
@@ -122,7 +122,7 @@ func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) {
{Command: "insurance / decline", Desc: "Answer the insurance offer explicitly"},
{Command: "sitout", Desc: "Skip future rounds; bet to rejoin"},
{Command: "stop", Desc: "Leave now, or after your committed round"},
- {Command: "option casino_display compact", Desc: "Short cards and action hints (or detailed for card art)"},
+ {Command: "option casino_display compact", Desc: "Compact action hints (detailed for full command menus)"},
{Command: "table rules", Desc: "Review this table’s rules"},
}))
}
diff --git a/internal/player/player.go b/internal/player/player.go
index b296dee..0c92af3 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -140,8 +140,8 @@ var OptionDefs = []OptionDef{
{"show_queued_cmds", OptBool, false, nil, "Show confirmation messages for queued tick actions"},
{"wrap_width", OptInt, 80, nil, "Wrap all output to this many columns (minimum 80)"},
{"unicode", OptBool, true, nil, "Unicode box-drawing characters"},
- {"casino_display", OptString, "compact", []string{"compact", "detailed"}, "Table display: compact cards and hints, or detailed card art and menus"},
- {"card_size", OptInt, 9, nil, "Detailed casino card size (1, 5, 7, or 9)"},
+ {"casino_display", OptString, "detailed", []string{"compact", "detailed"}, "Menu style: compact action hints, or detailed command menus"},
+ {"card_size", OptInt, 9, nil, "Casino card art size (1, 5, 7, or 9; applies at every table)"},
{"card_style", OptString, "dark", []string{"dark", "light"}, "Casino card style (dark or light)"},
{"visual_ticks", OptBool, false, nil, "Display a tick marker every game tick"},
{"visual_tick_count", OptInt, 0, nil, "Cycle length for tick counter (0 = no counter)"},