From 39ad552cb5f48c288e1d7a62e80ceb180882eae0 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 30 Sep 2026 17:30:00 -0400 Subject: chore: remove dead casino code and fold duplicated menu/odds logic --- internal/game/casino.go | 70 +++++++++++--------------------- internal/game/casino_commands_test.go | 12 +++++- internal/game/casino_durability_test.go | 1 - internal/game/cmd_look.go | 4 +- internal/game/render_casino_help.go | 17 ++++---- internal/game/render_casino_help_test.go | 2 +- internal/game/render_craps.go | 50 ++++++++++++++++------- internal/game/render_craps_paytable.go | 29 +++++++++++++ 8 files changed, 110 insertions(+), 75 deletions(-) create mode 100644 internal/game/render_craps_paytable.go (limited to 'internal/game') diff --git a/internal/game/casino.go b/internal/game/casino.go index c2c4c4f..517cde0 100644 --- a/internal/game/casino.go +++ b/internal/game/casino.go @@ -121,24 +121,6 @@ func (w casinoWallet) payCredits(amount int, stakeIDs []string) error { return nil } -// ResolveStakes consumes every stake still pending for the player: the round -// resolved as a loss, a forfeit, or a slots outcome whose payout carried no -// stake linkage. The pending ledger and the save are the commit point. -func (w casinoWallet) ResolveStakes() { - if len(w.p.PendingWagers) == 0 { - return - } - before := append([]player.PendingWager(nil), w.p.PendingWagers...) - w.p.PendingWagers = nil - if err := w.save("resolve stakes"); err != nil { - // Keep the in-memory ledger consistent with the durable file: the - // stakes are still pending on disk, so restore them for a later - // retry or startup recovery instead of silently diverging. - w.p.PendingWagers = before - fmt.Fprintf(os.Stderr, "casino resolve stakes: save for %s: %v (ledger restored in memory)\n", w.p.Name, err) - } -} - // ResolveStake consumes ONE pending stake by wallet handle: its hand lost or // was paid by a cover, so it must never be refunded. A settlement that // already removed it (PayCredits) makes this a no-op, so no redundant @@ -614,6 +596,28 @@ func (g *Game) emitCasinoEvents(events []casino.Event) { } } +// casinoMenuParts resolves a structured menu into the shared header/balance +// heading/commands used by both menu render paths: the balance line, the +// heading (falling back to the event message), and a copy of the commands +// annotated with an unavailability note for anything the player can't afford. +func (g *Game) casinoMenuParts(sess *net.Session, menu *casino.MenuView, message string) (balance, heading string, commands []casino.MenuCommand) { + balance = "" + if menu.ShowBalance { + balance = g.casinoBalance(sess) + } + heading = g.casinoTimeText(menu.Heading) + if heading == "" { + heading = message + } + commands = append([]casino.MenuCommand(nil), menu.Commands...) + for i := range commands { + if commands[i].Cost > casinoAffordable(sess.Player) { + commands[i].Desc += " [unavailable: insufficient funds]" + } + } + return balance, heading, commands +} + // casinoBoardReminder splits roulette and craps menus so instructions can be // printed under the betting board, which is the table the reminder refers to. // Every other game, and compact displays, keep their menu in one block. @@ -630,20 +634,7 @@ func (g *Game) casinoBoardReminder(sess *net.Session, category string, menu *cas // The compact reminder is already a short block; leave it intact. return "", "", false } - balance := "" - if menu.ShowBalance { - balance = g.casinoBalance(sess) - } - heading := g.casinoTimeText(menu.Heading) - if heading == "" { - heading = message - } - commands := append([]casino.MenuCommand(nil), menu.Commands...) - for i := range commands { - if commands[i].Cost > casinoAffordable(sess.Player) { - commands[i].Desc += " [unavailable: insufficient funds]" - } - } + balance, heading, commands := g.casinoMenuParts(sess, menu, message) return g.casinoMenuHeader(sess, balance, heading), g.casinoMenuCommands(sess, commands), true } @@ -681,20 +672,7 @@ func (g *Game) casinoPrivateMessage(sess *net.Session, category string, menu *ca if category != "casino_menu" || menu == nil { return message } - balance := "" - if menu.ShowBalance { - balance = g.casinoBalance(sess) - } - heading := g.casinoTimeText(menu.Heading) - if heading == "" { - heading = message - } - commands := append([]casino.MenuCommand(nil), menu.Commands...) - for i := range commands { - if commands[i].Cost > casinoAffordable(sess.Player) { - commands[i].Desc += " [unavailable: insufficient funds]" - } - } + balance, heading, commands := g.casinoMenuParts(sess, menu, message) if sess.Player.OptionString("casino_display") == "compact" && !menu.ShowBalance { var hints []casino.MenuCommand for _, c := range commands { diff --git a/internal/game/casino_commands_test.go b/internal/game/casino_commands_test.go index 7820650..0ae1593 100644 --- a/internal/game/casino_commands_test.go +++ b/internal/game/casino_commands_test.go @@ -102,14 +102,22 @@ func TestCasinoCompactHintsUseOnlyAvailableCommands(t *testing.T) { } func TestSimplifiedFeltSpacingAndNoIdlePrompt(t *testing.T) { text := renderCrapsSnapshot(&casino.CrapsSnapshot{Variant: "simplified"}, "none", true) + for _, want := range []string{"DICE TOTAL", "NET PROFIT", "3 or 4", "1:1 (even money)", "10 or 11", "LOSE STAKE", "ONE ROLL", "stake back"} { + if !strings.Contains(text, want) { + t.Fatalf("missing %q: %s", want, text) + } + } + if strings.Contains(text, "WINS PAY 1:1") || strings.Contains(text, "COME-OUT") { + t.Fatal("stale simplified rules", text) + } if strings.Contains(text, "Place a bet, then roll when ready.") { t.Fatal(text) } rows := strings.Split(text, "\n") for i, row := range rows { if strings.Contains(row, "PASS ONLY") { - if i+2 >= len(rows) || strings.Trim(rows[i+1], "│ ") != "" || !strings.HasPrefix(rows[i+2], "└") { - t.Fatal("expected exactly one blank felt row", text) + if i+1 >= len(rows) || strings.Trim(rows[i+1], "│ ") != "" || !strings.HasPrefix(rows[i+2], "└") { + t.Fatal("expected exactly one blank footer row", text) } return } diff --git a/internal/game/casino_durability_test.go b/internal/game/casino_durability_test.go index 227d391..edb4ac0 100644 --- a/internal/game/casino_durability_test.go +++ b/internal/game/casino_durability_test.go @@ -40,7 +40,6 @@ func TestCasinoWalletSaveFailureConsistency(t *testing.T) { }, true, 68, 0, 4}, {"payout", func(w casinoWallet) error { return w.PayCredits(40, "a", "b") }, true, 110, 2, 1}, {"unlinked payout", func(w casinoWallet) error { return w.PayCredits(20) }, true, 90, 2, 3}, - {"resolve all", func(w casinoWallet) error { w.ResolveStakes(); return nil }, false, 70, 2, 0}, {"resolve one", func(w casinoWallet) error { w.ResolveStake("a"); return nil }, false, 70, 2, 2}, {"resolve batch", func(w casinoWallet) error { w.ResolveStakesByIDs("a", "b"); return nil }, false, 70, 2, 1}, {"refund", func(w casinoWallet) error { diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 1de58f8..6fb80e5 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -146,9 +146,7 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, contentL content = append(content, line) continue } - for _, part := range strings.Split(color.WrapANSI(line, mapWidth), "\r\n") { - content = append(content, part) - } + content = append(content, strings.Split(color.WrapANSI(line, mapWidth), "\r\n")...) } total := len(content) if len(mapLines) > total { diff --git a/internal/game/render_casino_help.go b/internal/game/render_casino_help.go index 5a76ef0..26927ae 100644 --- a/internal/game/render_casino_help.go +++ b/internal/game/render_casino_help.go @@ -67,14 +67,17 @@ func (g *Game) rouletteReminder(sess *net.Session, cfg casino.TableConfig) strin } blocks := []string{g.casinoBetTable(sess, "Roulette payouts", []string{"Bet", "Target / example", "Chance", "Profit"}, rows)} if cfg.Roulette.Racetrack && !cfg.Roulette.DoubleZero { - blocks = append(blocks, g.casinoBetTable(sess, "Racetrack payouts", []string{"Target", "Chance", "Profit on total stake"}, [][]string{ - {"voisins", chance(17), "1:1; 0/2/3 pay 5:3"}, - {"tiers", chance(12), "2:1"}, - {"orphelins", chance(8), "13:5; 1/17 pay 31:5"}, - {"zero / jeu zero", chance(7), "7:2; 26 pays 8:1"}, - {"neighbors ", chance(9), "3:1"}, + blocks = append(blocks, g.casinoBetTable(sess, "Racetrack payouts", []string{"Target", "Winning pockets", "Chance", "Profit"}, [][]string{ + {"voisins", "0, 2, 3", chance(3), "5:3"}, + {"voisins", "Other covered", chance(14), "1:1"}, + {"tiers", "Any covered", chance(12), "2:1"}, + {"orphelins", "1, 17", chance(2), "31:5"}, + {"orphelins", "Other covered", chance(6), "13:5"}, + {"zero / jeu zero", "26", chance(1), "8:1"}, + {"zero / jeu zero", "Other covered", chance(6), "7:2"}, + {"neighbors ", "Any covered", chance(9), "3:1"}, })) - blocks = append(blocks, "Call bets split your total stake; fractional returns round down.\nUse help racetrack for the chip placements.") + blocks = append(blocks, "Profit is on the total stake; each row lists its winning pockets.\nCall bets split your stake; fractional returns round down.\nUse help racetrack for the chip placements.") } blocks = append(blocks, "Profit excludes your returned stake: 1:1 on 10 returns 20.\n"+ diff --git a/internal/game/render_casino_help_test.go b/internal/game/render_casino_help_test.go index 246880f..830186c 100644 --- a/internal/game/render_casino_help_test.go +++ b/internal/game/render_casino_help_test.go @@ -37,7 +37,7 @@ func TestRoulettePayoutTables(t *testing.T) { t.Fatal("American wheel has call bets") } } else { - for _, want := range []string{"1/37", "4/37", "17/37", "12/37", "8/37", "7/37", "9/37", "Racetrack payouts", "5:3", "13:5", "31:5", "7:2", "zero / jeu zero"} { + for _, want := range []string{"1/37", "2/37", "3/37", "4/37", "6/37", "12/37", "9/37", "14/37", "Racetrack payouts", "5:3", "13:5", "31:5", "7:2", "zero / jeu zero"} { if !strings.Contains(plain, want) { t.Fatalf("missing %q:\n%s", want, plain) } diff --git a/internal/game/render_craps.go b/internal/game/render_craps.go index 2270659..d6db43f 100644 --- a/internal/game/render_craps.go +++ b/internal/game/render_craps.go @@ -39,10 +39,13 @@ type crapsCell struct { } type crapsCanvas struct{ cells [][]crapsCell } -func newCrapsCanvas(w, h int) *crapsCanvas { +// crapsCanvasWidth is the fixed felt width every craps render uses. +const crapsCanvasWidth = 77 + +func newCrapsCanvas(h int) *crapsCanvas { c := &crapsCanvas{cells: make([][]crapsCell, h)} for y := range c.cells { - c.cells[y] = make([]crapsCell, w) + c.cells[y] = make([]crapsCell, crapsCanvasWidth) } return c } @@ -136,10 +139,10 @@ func crapsDie(n int) string { return string([]rune{'⚀', '⚁', '⚂', '⚃', '⚄', '⚅'}[n-1]) } func renderCrapsTrackers(s *casino.CrapsSnapshot, mode string, unicode bool) string { - c := newCrapsCanvas(77, 7) - c.box(0, 0, 76, 3) - c.box(0, 3, 38, 6) - c.box(38, 3, 76, 6) + c := newCrapsCanvas(12) + c.box(0, 0, 76, 2) + c.box(0, 5, 38, 8) + c.box(38, 5, 76, 8) c.text(3, 1, "FIRE", crapsGold) for i, n := range []int{4, 5, 6, 8, 9, 10} { ink, mark := crapsMuted, " " @@ -148,9 +151,15 @@ func renderCrapsTrackers(s *casino.CrapsSnapshot, mode string, unicode bool) str } c.text(15+i*9, 1, fmt.Sprintf("[%2d%s]", n, mark), ink) } - c.center(1, 75, 2, "MADE POINTS 4: 24 to 1 5: 249 to 1 6: 999 to 1", crapsInk) - c.text(2, 4, "ALL SMALL", crapsGold) - c.text(40, 4, "TALL", crapsGold) + bounds := []int{0, 25, 50, 76} + for i, payout := range []string{"24:1", "249:1", "999:1"} { + lo, hi := bounds[i], bounds[i+1] + c.box(lo, 2, hi, 5) + c.center(lo+1, hi-1, 3, fmt.Sprintf("%d DISTINCT POINTS", i+4), crapsGold) + c.center(lo+1, hi-1, 4, payout, crapsInk) + } + c.text(2, 6, "ALL SMALL", crapsGold) + c.text(40, 6, "TALL", crapsGold) for i, n := range []int{2, 3, 4, 5, 6, 8, 9, 10, 11, 12} { x := 12 + i*5 if i >= 5 { @@ -160,14 +169,20 @@ func renderCrapsTrackers(s *casino.CrapsSnapshot, mode string, unicode bool) str if crapsHas(s.Seen, n) { ink, mark = crapsMarked, "*" } - c.text(x, 4, fmt.Sprintf("%2d%s ", n, mark), ink) + c.text(x, 6, fmt.Sprintf("%2d%s ", n, mark), ink) + } + c.center(1, 37, 7, "* = rolled", crapsMuted) + c.center(39, 75, 7, "* = rolled", crapsMuted) + for i, row := range [][2]string{{"SMALL", "34:1"}, {"ALL", "175:1"}, {"TALL", "34:1"}} { + lo, hi := bounds[i], bounds[i+1] + c.box(lo, 8, hi, 11) + c.center(lo+1, hi-1, 9, row[0], crapsGold) + c.center(lo+1, hi-1, 10, row[1], crapsInk) } - c.center(1, 37, 5, "SMALL 34:1 | ALL 175:1", crapsInk) - c.center(39, 75, 5, "TALL 34:1 | * = rolled", crapsInk) return c.render(mode, unicode) } func renderCrapsFelt(s *casino.CrapsSnapshot, mode string, unicode bool) string { - c := newCrapsCanvas(77, 26) + c := newCrapsCanvas(26) c.box(0, 0, 14, 6) c.center(1, 13, 4, "DON'T COME", crapsInk) c.center(1, 13, 5, "BAR 12", crapsMuted) @@ -318,6 +333,9 @@ func renderCrapsSnapshot(s *casino.CrapsSnapshot, mode string, unicode bool, sty if s.Point != 0 { point = fmt.Sprintf("POINT %d", s.Point) } + if s.Variant == "simplified" { + point = "ONE ROLL" + } variant := s.Variant if variant == "" { variant = "bank" @@ -327,12 +345,14 @@ func renderCrapsSnapshot(s *casino.CrapsSnapshot, mode string, unicode bool, sty out.WriteString(renderCrapsTrackers(s, mode, unicode)) out.WriteByte('\n') out.WriteString(renderCrapsFelt(s, mode, unicode)) + } else if variant == "simplified" { + out.WriteString(renderSimplifiedCrapsFelt(mode, unicode)) } else { height := 7 - if variant == "simplified" || variant == "high_point" || variant == "die_rich" { + if variant == "high_point" || variant == "die_rich" { height = 6 } - c := newCrapsCanvas(77, height) + c := newCrapsCanvas(height) c.box(0, 0, 76, height-1) c.center(1, 75, 1, "HOUSE EDITION — table rules for payouts", crapsGold) switch variant { diff --git a/internal/game/render_craps_paytable.go b/internal/game/render_craps_paytable.go new file mode 100644 index 0000000..346ed83 --- /dev/null +++ b/internal/game/render_craps_paytable.go @@ -0,0 +1,29 @@ +package game + +// Keep each outcome beside its net profit instead of packing a series of +// total:odds pairs into a sentence. The footer distinguishes profit from the +// returned stake and retains one blank felt row below the command summary. +func renderSimplifiedCrapsFelt(mode string, unicode bool) string { + c := newCrapsCanvas(13) + for _, x := range []int{0, 38} { + c.box(x, 0, x+38, 2) + c.box(x, 2, x+38, 8) + } + c.center(1, 37, 1, "DICE TOTAL", crapsGold) + c.center(39, 75, 1, "NET PROFIT", crapsGold) + rows := [][2]string{ + {"2", "3:1"}, + {"3 or 4", "1:1 (even money)"}, + {"10 or 11", "2:1"}, + {"12", "5:1"}, + {"5, 6, 7, 8 or 9", "LOSE STAKE"}, + } + for i, row := range rows { + c.center(1, 37, i+3, row[0], crapsInk) + c.center(39, 75, i+3, row[1], crapsInk) + } + c.box(0, 8, 76, 12) + c.center(1, 75, 9, "Winners also receive their original stake back.", crapsMuted) + c.center(1, 75, 10, "PASS ONLY | ONE ROLL | NO POINT, ODDS OR SIDE BETS", crapsGold) + return c.render(mode, unicode) +} -- cgit v1.2.3