diff options
| author | historia <[not public]> | 2026-09-30 03:55:37 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-30 03:55:37 -0400 |
| commit | 735428f0667f67c12fdf3caca872d7dc65e2ce26 (patch) | |
| tree | 92c9931f223bfd50ced4f1954130d879640a435b /internal/casino | |
| parent | ffc7b582e9c14fdc2e579b9239b5ff7ad9a0d90a (diff) | |
| download | thehouseoficarus-735428f0667f67c12fdf3caca872d7dc65e2ce26.tar.gz | |
feat: restyle roulette presentation
Diffstat (limited to 'internal/casino')
| -rw-r--r-- | internal/casino/baccarat.go | 21 | ||||
| -rw-r--r-- | internal/casino/baccarat_test.go | 4 | ||||
| -rw-r--r-- | internal/casino/blackjack.go | 75 | ||||
| -rw-r--r-- | internal/casino/blackjack_test.go | 4 | ||||
| -rw-r--r-- | internal/casino/machine.go | 2 | ||||
| -rw-r--r-- | internal/casino/roulette.go | 13 | ||||
| -rw-r--r-- | internal/casino/table.go | 2 | ||||
| -rw-r--r-- | internal/casino/table_ux.go | 127 | ||||
| -rw-r--r-- | internal/casino/table_ux_test.go | 12 |
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 } } |
