aboutsummaryrefslogtreecommitdiff
path: root/internal/casino
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-30 03:55:37 -0400
committerhistoria <[not public]>2026-09-30 03:55:37 -0400
commit735428f0667f67c12fdf3caca872d7dc65e2ce26 (patch)
tree92c9931f223bfd50ced4f1954130d879640a435b /internal/casino
parentffc7b582e9c14fdc2e579b9239b5ff7ad9a0d90a (diff)
downloadthehouseoficarus-735428f0667f67c12fdf3caca872d7dc65e2ce26.tar.gz
feat: restyle roulette presentation
Diffstat (limited to 'internal/casino')
-rw-r--r--internal/casino/baccarat.go21
-rw-r--r--internal/casino/baccarat_test.go4
-rw-r--r--internal/casino/blackjack.go75
-rw-r--r--internal/casino/blackjack_test.go4
-rw-r--r--internal/casino/machine.go2
-rw-r--r--internal/casino/roulette.go13
-rw-r--r--internal/casino/table.go2
-rw-r--r--internal/casino/table_ux.go127
-rw-r--r--internal/casino/table_ux_test.go12
9 files changed, 186 insertions, 74 deletions
diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go
index ad56da4..56677b2 100644
--- a/internal/casino/baccarat.go
+++ b/internal/casino/baccarat.go
@@ -186,7 +186,11 @@ func (t *baccaratTable) deal() []Event {
// of a payout that was not durable.
payoutFailed := settleStakeOutcome(p.Wallet, payout, []string{p.stakeID}, settleErr)
publicResult, _ := baccaratResultMessages(p.Name, result, payout)
- privateResult := resultText(result, p.Wager.Total, payout)
+ line := settlementLine{Label: p.lastSpot, Result: result, Stake: p.Wager.Total, Returned: payout}
+ if result == "win" && p.lastSpot == baccaratSpotTie {
+ line.Tag = "casino_jackpot_win"
+ }
+ privateResult := settlementText([]settlementLine{line})
if payoutFailed {
privateResult = payoutFailedMessage()
publicResult = p.Name + "’s settlement is pending recovery."
@@ -195,11 +199,16 @@ func (t *baccaratTable) deal() []Event {
// 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"
- } else {
- settlement.Color = "casino_big_win"
+ if !payoutFailed {
+ switch {
+ case line.Tag != "":
+ settlement.Color = line.Tag
+ case result == "win":
+ settlement.Color = "casino_result_win"
+ case result == "push":
+ settlement.Color = "casino_result_push"
+ default:
+ settlement.Color = "casino_result_loss"
}
}
events = append(events, settlement)
diff --git a/internal/casino/baccarat_test.go b/internal/casino/baccarat_test.go
index f947f5b..dce1629 100644
--- a/internal/casino/baccarat_test.go
+++ b/internal/casino/baccarat_test.go
@@ -173,7 +173,7 @@ func TestBaccaratSettlementShowsBalanceOnlyWithoutNextMenu(t *testing.T) {
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") {
+ if e.PrivateTo == "alice" && strings.Contains(e.PrivateMessage, "{casino_result_win}WIN{/}") {
found = true
if e.ShowBalance != leaving {
t.Fatalf("leaving=%v settlement ShowBalance=%v: %+v", leaving, e.ShowBalance, e)
@@ -379,7 +379,7 @@ func TestBaccaratMenuAndActions(t *testing.T) {
menu = event.Menu
}
}
- if menu == nil || !menu.ShowBalance || !strings.Contains(menu.Heading, "Baccarat / Commission | Next hand | Limits") {
+ if menu == nil || !menu.ShowBalance || !strings.Contains(menu.Heading, "Baccarat / Commission | Limits") {
t.Fatalf("join menu=%+v, want a baccarat betting menu", menu)
}
if len(menu.Commands) != 4 || menu.Commands[0].Command != "bet 1 banker" || menu.Commands[2].Command != "stop" {
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
index 0a5c0d1..5fb8b0b 100644
--- a/internal/casino/blackjack.go
+++ b/internal/casino/blackjack.go
@@ -668,28 +668,22 @@ func (t *blackjackTable) settle() []Event {
if !p.HasBet {
continue
}
- roundNet, roundFailed := int64(0), false
+ needsBalance := p.LeaveAfter || p.SitOutAfter || !p.Connected
+ lines := make([]settlementLine, 0, len(p.hands)+1)
for i, hand := range p.hands {
result, payout, settleErr := settleHand(hand, dealerTotal, dealerBlackjack, t.Rules)
// One shared settlement path for every game: pay before
// announcing, and leave the stakes pending on any failure so a
// refund can never be paid on top of a payout that was not durable.
if settleStakeOutcome(p.Wallet, payout, hand.stakes, settleErr) {
- roundFailed = true
- settlement := t.playerAction(p.Name, p.Name+"’s settlement is pending recovery.", payoutFailedMessage())
- colorizeBlackjackSettlement(&settlement, result)
- events = append(events, settlement)
+ events = append(events, t.playerAction(p.Name, p.Name+"’s settlement is pending recovery.", payoutFailedMessage()))
continue
}
- publicResult, _ := blackjackResultMessages(p.Name, result, payout)
- roundNet += int64(payout) - int64(hand.bet)
- privateResult := resultText(result, hand.bet, payout)
+ label := ""
if len(p.hands) > 1 {
- privateResult = fmt.Sprintf("Hand %d/%d | %s", i+1, len(p.hands), privateResult)
+ label = fmt.Sprintf("Hand %d/%d", i+1, len(p.hands))
}
- settlement := t.playerAction(p.Name, publicResult, privateResult)
- colorizeBlackjackSettlement(&settlement, result)
- events = append(events, settlement)
+ lines = append(lines, settlementLine{Label: label, Result: result, Stake: hand.bet, Returned: payout})
}
if p.insurance > 0 {
insurancePayout := 0
@@ -698,27 +692,19 @@ func (t *blackjackTable) settle() []Event {
insurancePayout, insuranceErr = MulPayout(p.insurance, 1+t.Rules.InsurancePayout)
}
if settleStakeOutcome(p.Wallet, insurancePayout, []string{p.insuranceStakeID}, insuranceErr) {
- roundFailed = true
events = append(events, t.private(p.Name, payoutFailedMessage()))
} else {
- roundNet += int64(insurancePayout) - int64(p.insurance)
- events = append(events, t.private(p.Name, resultText("insurance", p.insurance, insurancePayout)))
+ lines = append(lines, settlementLine{Label: "Insurance", Result: "insurance", Stake: p.insurance, Returned: insurancePayout})
}
}
- if !roundFailed {
- // 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)
- }
+ // One settlement per player reports the round's net once and then
+ // names every hand (and insurance) that produced it. A single hand
+ // keeps the headline only; splits and insurance get the itemized list.
+ if len(lines) > 0 {
+ settlement := t.playerAction(p.Name, blackjackPublicResult(p.Name, lines), settlementText(lines))
+ settlement.ShowBalance = needsBalance
+ settlement.Color = settlementTag(lines)
+ events = append(events, settlement)
}
// Each hand's stakes were already consumed per-handle by
// settleStakeOutcome (a payout removes them in PayCredits; a loss
@@ -780,13 +766,19 @@ func (t *blackjackTable) resetRound() {
}
}
-// colorizeBlackjackSettlement paints a winning settlement with the big-win
-// (or jackpot, for a natural) color.
-func colorizeBlackjackSettlement(e *Event, result string) {
- if result == "blackjack" {
- e.Color = "casino_jackpot_win"
- } else if result == "win" {
- e.Color = "casino_big_win"
+// blackjackPublicResult summarizes a player's round for the room: the net is
+// not named, only the outcome.
+func blackjackPublicResult(name string, lines []settlementLine) string {
+ if len(lines) == 1 && lines[0].Result == "blackjack" {
+ return name + " gets blackjack!"
+ }
+ switch {
+ case settlementNet(lines) > 0:
+ return name + " wins!"
+ case settlementNet(lines) < 0:
+ return name + " loses."
+ default:
+ return name + " pushes."
}
}
@@ -861,17 +853,6 @@ func handScore(hand *blackjackHand) string {
return fmt.Sprintf("Soft %d", total)
}
-func blackjackResultMessages(name, result string, payout int) (string, string) {
- switch result {
- case "win", "blackjack":
- return name + " wins!", fmt.Sprintf("You win! +%d credits", payout)
- case "push":
- return name + " pushes.", fmt.Sprintf("You push. +%d credits", payout)
- default:
- return name + " loses.", "You lose!"
- }
-}
-
// markDisconnected removes the player from the table. A wager is forfeited
// only when a hand is actually in flight (cards are face up, so a free
// refund would hand back information the round carries); a stake placed in
diff --git a/internal/casino/blackjack_test.go b/internal/casino/blackjack_test.go
index 08069ef..aff87c8 100644
--- a/internal/casino/blackjack_test.go
+++ b/internal/casino/blackjack_test.go
@@ -175,7 +175,7 @@ func TestBlackjackInsurancePaysOnDealerBlackjack(t *testing.T) {
if w.paid != 15 {
t.Fatalf("insurance payout=%d, want 15 (stake + 2x)", w.paid)
}
- if !hasMessage(events, "INSURANCE | Net +10 credits | 15 returned") {
+ if !hasMessage(events, "Insurance") || !hasMessage(events, "{casino_result_win}+10 credits{/}") {
t.Fatalf("missing insurance payout message: %+v", events)
}
if table.phase != blackjackWaiting {
@@ -285,7 +285,7 @@ func TestBlackjackDisconnectActivePassesPlay(t *testing.T) {
}
// Bob stands; round settles.
settle := table.action("bob", "stand")
- if !hasMessage(settle, "LOSS | Net -10 credits | 0 returned") {
+ if !hasMessage(settle, "{casino_result_loss}LOSS{/}") || !hasMessage(settle, "{casino_payout}-10 credits{/}") {
t.Fatalf("bob 16 vs dealer 17: expected loss, got %+v", settle)
}
if wb.paid != 0 {
diff --git a/internal/casino/machine.go b/internal/casino/machine.go
index 7821975..f8730be 100644
--- a/internal/casino/machine.go
+++ b/internal/casino/machine.go
@@ -303,7 +303,7 @@ func (m *Machine) finishSpin() []Event {
message = fmt.Sprintf("\nThe free spins pay %d credits.", payNow)
}
winEvents := m.private(message)
- winEvents[0].Color = "casino_payout"
+ winEvents[0].Color = "casino_result_win"
if payNow > m.spinBet {
winEvents[0].Color = "casino_big_win"
}
diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go
index 44c5f30..b4dcdf0 100644
--- a/internal/casino/roulette.go
+++ b/internal/casino/roulette.go
@@ -495,7 +495,6 @@ func (t *rouletteTable) menuEvent(name string) Event {
{Command: "bet / rebet", Desc: "Repeat your previous wager"},
{Command: "table / sitout / stop", Desc: "View layout / sit out / leave after this spin"},
})
- e.Menu.Heading = strings.Replace(e.Menu.Heading, "Next hand", "Next spin", 1)
e.Snapshot = t.view()
return e
}
@@ -622,7 +621,7 @@ func (t *rouletteTable) settle() []Event {
wheel[(index+1)%n], wheel[(index+2)%n],
}
t.lastResult = s
- e := t.event("The ball lands in "+pocket+"!", true)
+ e := t.event("The ball lands in {casino_emphasis}"+pocket+"{/}!", true)
e.Snapshot = s
events := []Event{e}
for _, p := range t.players.ordered() {
@@ -645,7 +644,7 @@ func (t *rouletteTable) settle() []Event {
if !failed {
failed = p.Wallet.PayCredits(payout, p.Wager.ID) != nil
}
- text := resultText(result, p.Wager.Total, payout)
+ text := resultTextLabel(p.lastSpot, result, p.Wager.Total, payout)
public := p.Name + " " + result + "."
if failed {
text = payoutFailedMessage()
@@ -655,8 +654,12 @@ func (t *rouletteTable) settle() []Event {
// Returning players get their balance in the next-spin menu. Only
// departures/sit-outs need a balance attached to the settlement.
event.ShowBalance = !failed && (p.LeaveAfter || p.SitOutAfter || !p.Connected)
- if result == "win" && !failed {
- event.Color = "casino_big_win"
+ if !failed {
+ if result == "win" {
+ event.Color = "casino_result_win"
+ } else {
+ event.Color = "casino_result_loss"
+ }
}
events = append(events, event)
}
diff --git a/internal/casino/table.go b/internal/casino/table.go
index 17732e0..ddf357f 100644
--- a/internal/casino/table.go
+++ b/internal/casino/table.go
@@ -54,7 +54,7 @@ func (b *tableBase) playerAction(name, publicMessage, privateMessage string) Eve
// betMenuEvent builds the shared "place your bet" menu heading for a table
// game; only the command list differs per game.
func (b *tableBase) betMenuEvent(name string, commands []MenuCommand) Event {
- heading := fmt.Sprintf("%s | Next hand | Limits %s", b.Config.Label(), betRangeText(b.Config.MinBet, b.Config.MaxBet))
+ heading := fmt.Sprintf("%s | Limits %s", b.Config.Label(), betRangeText(b.Config.MinBet, b.Config.MaxBet))
e := b.private(name, heading)
e.Color = "casino_menu"
e.Menu = &MenuView{Heading: heading, ShowBalance: true, Commands: commands}
diff --git a/internal/casino/table_ux.go b/internal/casino/table_ux.go
index 002d9f0..b4a13ab 100644
--- a/internal/casino/table_ux.go
+++ b/internal/casino/table_ux.go
@@ -201,13 +201,126 @@ func (t *blackjackTable) insuranceMenuLocked(p *blackjackPlayer) Event {
return e
}
-func resultText(result string, stake, returned int) string {
- label := strings.ToUpper(result)
- switch result {
+// settlementLine is one settled bet within a round. Label names the bet for
+// the itemized line (empty for a lone anonymous hand); Result is the engine's
+// settlement word ("win", "loss", "bust", "push", "blackjack",
+// "surrender", "insurance").
+type settlementLine struct {
+ Label string
+ Result string
+ Stake int
+ Returned int
+ Tag string // optional color-category override (jackpot, for example)
+}
+
+func (l settlementLine) net() int { return l.Returned - l.Stake }
+
+// resultTextLabel renders a settled wager as a colored outcome plus its net,
+// naming the bet on its own line.
+func resultTextLabel(label, result string, stake, returned int) string {
+ return settlementText([]settlementLine{{Label: label, Result: result, Stake: stake, Returned: returned}})
+}
+
+// settlementText renders a settlement as a colored headline followed by one
+// line per named bet. The net is the only figure shown: the old
+// "Net ... | ... returned (stake included)" split is gone. Wins are green,
+// losses red and pushes white, with the headline taking the round's overall
+// result and each line its own.
+func settlementText(lines []settlementLine) string {
+ net := 0
+ for _, line := range lines {
+ net += line.net()
+ }
+ var sb strings.Builder
+ fmt.Fprintf(&sb, "{%s}%s{/} {casino_payout}%s{/}", settlementTag(lines), settlementLabel(lines), creditsText(net))
+ for _, line := range lines {
+ if line.Label == "" {
+ continue
+ }
+ fmt.Fprintf(&sb, "\n {casino_action}%s:{/} {%s}%s{/}", line.Label, lineTag(line), creditsText(line.net()))
+ }
+ return sb.String()
+}
+
+// creditsText formats a signed credit total with the right plural.
+func creditsText(n int) string {
+ word := "credits"
+ if n == 1 || n == -1 {
+ word = "credit"
+ }
+ return fmt.Sprintf("%+d %s", n, word)
+}
+
+// settlementLabel returns the headline word for a round.
+func settlementLabel(lines []settlementLine) string {
+ if len(lines) == 1 {
+ switch lines[0].Result {
+ case "blackjack":
+ return "BLACKJACK"
+ case "surrender":
+ return "SURRENDER"
+ }
+ }
+ switch {
+ case settlementNet(lines) > 0:
+ return "WIN"
+ case settlementNet(lines) < 0:
+ return "LOSS"
+ default:
+ return "PUSH"
+ }
+}
+
+// settlementTag returns the headline's color tag. A lone natural keeps the
+// jackpot gradient; otherwise the net decides green, red or white.
+func settlementTag(lines []settlementLine) string {
+ if len(lines) == 1 && lines[0].Tag != "" {
+ return lines[0].Tag
+ }
+ if len(lines) == 1 && lines[0].Result == "blackjack" {
+ return "casino_jackpot_win"
+ }
+ switch {
+ case settlementNet(lines) > 0:
+ return "casino_result_win"
+ case settlementNet(lines) < 0:
+ return "casino_result_loss"
+ default:
+ return "casino_result_push"
+ }
+}
+
+// lineTag colors one bet's outcome. Insurance is judged by its net because the
+// same result word covers a paid and a lost premium.
+func lineTag(line settlementLine) string {
+ if line.Tag != "" {
+ return line.Tag
+ }
+ switch line.Result {
case "blackjack":
- label = "BLACKJACK"
- case "bust", "loss", "dealer blackjack":
- label = "LOSS"
+ return "casino_jackpot_win"
+ case "push":
+ return "casino_result_push"
+ case "win":
+ return "casino_result_win"
+ case "insurance":
+ switch {
+ case line.net() > 0:
+ return "casino_result_win"
+ case line.net() < 0:
+ return "casino_result_loss"
+ default:
+ return "casino_result_push"
+ }
+ default:
+ return "casino_result_loss"
+ }
+}
+
+func settlementNet(lines []settlementLine) int {
+ net := 0
+ for _, line := range lines {
+ net += line.net()
}
- return fmt.Sprintf("%s | Net %+d credits | %d returned (stake included)", label, returned-stake, returned)
+ return net
}
diff --git a/internal/casino/table_ux_test.go b/internal/casino/table_ux_test.go
index 812bed0..cb13300 100644
--- a/internal/casino/table_ux_test.go
+++ b/internal/casino/table_ux_test.go
@@ -136,7 +136,7 @@ func TestQueuedLeaveAndSitoutPreserveCommittedBlackjackHands(t *testing.T) {
t.Fatalf("%s altered committed hand", action)
}
events := table.action("alice", "stand")
- if !hasMessage(events, "Net -10 credits") {
+ if !hasMessage(events, "{casino_payout}-10 credits{/}") {
t.Fatalf("missing settlement: %+v", events)
}
if action == "leave_after" && table.hasParticipant("alice") {
@@ -154,7 +154,13 @@ func TestSplitAndInsuranceRoundNetIncludesEveryStake(t *testing.T) {
p.hands = []*blackjackHand{{bet: 20, stood: true, cards: []card{{rank: "10"}, {rank: "10"}}}, {bet: 10, bust: true, cards: []card{{rank: "10"}, {rank: "10"}, {rank: "5"}}}}
p.insurance = 5
events := table.settle()
- for _, text := range []string{"Hand 1/2 | WIN | Net +20 credits | 40 returned", "Hand 2/2 | LOSS | Net -10 credits", "INSURANCE | Net -5 credits", "Round complete | Net +5 credits"} {
+ for _, text := range []string{
+ "{casino_result_win}WIN{/}",
+ "{casino_payout}+5 credits{/}",
+ "Hand 1/2:{/} {casino_result_win}+20 credits{/}",
+ "Hand 2/2:{/} {casino_result_loss}-10 credits{/}",
+ "Insurance:{/} {casino_result_loss}-5 credits{/}",
+ } {
if !hasMessage(events, text) {
t.Fatalf("missing %q in %+v", text, events)
}
@@ -184,7 +190,7 @@ func TestDepartingSettlementCarriesBalanceSummary(t *testing.T) {
events := table.settle()
found := false
for _, e := range events {
- if strings.Contains(e.Message, "Round complete") && e.ShowBalance {
+ if strings.Contains(e.PrivateMessage, "LOSS") && e.ShowBalance {
found = true
}
}