aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/casino/blackjack.go2
-rw-r--r--internal/casino/core.go4
-rw-r--r--internal/casino/craps.go35
-rw-r--r--internal/casino/craps_projection.go19
-rw-r--r--internal/casino/craps_projection_test.go4
-rw-r--r--internal/casino/craps_simplified_test.go35
-rw-r--r--internal/casino/craps_variants.go14
-rw-r--r--internal/casino/craps_variants_test.go5
-rw-r--r--internal/casino/manager_test.go4
-rw-r--r--internal/casino/types.go3
-rw-r--r--internal/game/casino.go70
-rw-r--r--internal/game/casino_commands_test.go12
-rw-r--r--internal/game/casino_durability_test.go1
-rw-r--r--internal/game/cmd_look.go4
-rw-r--r--internal/game/render_casino_help.go17
-rw-r--r--internal/game/render_casino_help_test.go2
-rw-r--r--internal/game/render_craps.go50
-rw-r--r--internal/game/render_craps_paytable.go29
18 files changed, 186 insertions, 124 deletions
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
index 925b7ef..53bee6c 100644
--- a/internal/casino/blackjack.go
+++ b/internal/casino/blackjack.go
@@ -535,7 +535,7 @@ func (t *blackjackTable) prompt() []Event {
// The absolute turn ceiling belongs to the turn, not the prompt: restart
// it only when the active (player, hand) changes. A seat event that
// re-prompts the same turn must not reset a hung bettor's deadline.
- if name != t.turnName || t.activeHand != t.turnHand {
+ if changed {
t.ceiling.start(t.Rules.ActionTicks * loneTurnGrace)
t.turnName = name
t.turnHand = t.activeHand
diff --git a/internal/casino/core.go b/internal/casino/core.go
index a927ab1..fdb7877 100644
--- a/internal/casino/core.go
+++ b/internal/casino/core.go
@@ -181,9 +181,7 @@ func settleStakeOutcome(w Wallet, payout int, stakeIDs []string, payoutErr error
}
// A zero payout is a loss: consume the stakes in one save so recovery can
// never refund a round that resolved.
- if w != nil {
- w.ResolveStakesByIDs(stakeIDs...)
- }
+ w.ResolveStakesByIDs(stakeIDs...)
return false
}
diff --git a/internal/casino/craps.go b/internal/casino/craps.go
index 9e48667..410a372 100644
--- a/internal/casino/craps.go
+++ b/internal/casino/craps.go
@@ -207,17 +207,22 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event {
continue
}
against := strings.HasPrefix(b.kind, "dont")
- x, y := crapsTrueOdds(b.number)
+ // The odds payout is a whole-ratio return on the stake, so the wager
+ // must divide evenly by the ratio's denominator. Don't odds invert
+ // the pass ratio, so against the same number the other side of the
+ // true odds is the denominator. (crapsReturn applies the full
+ // ratio at payout time.)
+ num, den := crapsTrueOdds(b.number)
+ payoutDen := den
if against {
- x, y = y, x
+ payoutDen = num
}
- _ = x
limit := t.Config.Craps.oddsLimit(b.number, against)
if b.odds.Total > 0 {
continue
}
// Compare without multiplying a potentially very large stake by Nx.
- if (amount-1)/limit >= b.stake.Total || amount%y != 0 {
+ if (amount-1)/limit >= b.stake.Total || amount%payoutDen != 0 {
return reject("Odds already placed, above the free-odds limit, or not in exact payout units.")
}
stake, err := w.Wager(amount)
@@ -575,14 +580,26 @@ func (t *crapsTable) markDisconnected(name string) []Event {
var events []Event
// Refunding after observing intermediate dice would turn contract bets into
// free options. Disconnect consumes live bets; shutdown alone refunds them.
+ // Every live bet resolves as a loss covered by ONE wallet save: PayCredits
+ // with a zero return consumes every listed stake in that one save the same
+ // way a losing round does, instead of one save per bet.
+ var ids []string
+ var lines []settlementLine
for _, b := range p.bets {
- line, failed := t.pay(p, b, -1, 1, -1, false)
- var lines []settlementLine
- if !failed {
- lines = append(lines, line)
+ ids = append(ids, b.stake.ID)
+ if b.odds.Total > 0 {
+ ids = append(ids, b.odds.ID)
}
- events = append(events, t.settlementEvent(p, lines, failed))
+ lines = append(lines, settlementLine{
+ Label: crapsBetLabel(b), Stake: b.stake.Total + b.odds.Total,
+ Returned: 0, Result: "loss",
+ })
+ }
+ failed := false
+ if p.Wallet != nil {
+ failed = p.Wallet.PayCredits(0, ids...) != nil
}
+ events = append(events, t.settlementEvent(p, lines, failed))
t.remove(name)
events = append(events, t.syncRoundClock()...)
return append(events, t.event(name+" leaves craps; live wagers are forfeited.", true))
diff --git a/internal/casino/craps_projection.go b/internal/casino/craps_projection.go
index 96d64b7..c36f69f 100644
--- a/internal/casino/craps_projection.go
+++ b/internal/casino/craps_projection.go
@@ -95,25 +95,6 @@ func crapsBonusComplete(lo, hi int, seen map[int]bool, next int) bool {
}
return true
}
-func crapsRollLabel(bet *crapsBet, a, b int) string {
- s := a + b
- switch bet.kind {
- case "hard":
- if a == b {
- return fmt.Sprintf("%d+%d", a, b)
- }
- if s == 7 {
- return "7"
- }
- return fmt.Sprintf("easy %d", s)
- case "hop":
- if bet.number == a*10+b || bet.number == b*10+a {
- return fmt.Sprintf("%d+%d", a, b)
- }
- return "any other"
- }
- return fmt.Sprint(s)
-}
func (t *crapsTable) rollTable(p *crapsPlayer) string {
agg := map[string]int{}
pairsMatter := false
diff --git a/internal/casino/craps_projection_test.go b/internal/casino/craps_projection_test.go
index be48691..3745249 100644
--- a/internal/casino/craps_projection_test.go
+++ b/internal/casino/craps_projection_test.go
@@ -48,9 +48,7 @@ func (t *crapsTable) simulateNet(name string, a, b int) int {
wallets := map[string]*testWallet{}
for _, seat := range t.players.ordered() {
wallet := &testWallet{}
- for _, wager := range seat.Wallet.(*testWallet).open {
- wallet.open = append(wallet.open, wager)
- }
+ wallet.open = append(wallet.open, seat.Wallet.(*testWallet).open...)
wallets[seat.Name] = wallet
cp := &crapsPlayer{Participant: Participant{Name: seat.Name, Connected: true, HasBet: seat.HasBet, Wallet: wallet}}
for _, bet := range seat.bets {
diff --git a/internal/casino/craps_simplified_test.go b/internal/casino/craps_simplified_test.go
new file mode 100644
index 0000000..e36bc2d
--- /dev/null
+++ b/internal/casino/craps_simplified_test.go
@@ -0,0 +1,35 @@
+package casino
+
+import "testing"
+
+func TestSimplifiedCrapsAll36SettlementsAndHouseEdge(t *testing.T) {
+ // Total returns for a one-credit wager, including the winning stake.
+ returns := [13]int{2: 4, 3: 2, 4: 2, 10: 3, 11: 3, 12: 6}
+ totalReturned, wins := 0, 0
+ for a := 1; a <= 6; a++ {
+ for b := 1; b <= 6; b++ {
+ tb := variantFixture("simplified")
+ tb.join("a")
+ w := &testWallet{chips: 100}
+ tb.bet("a", "pass", 1, w)
+ want := returns[a+b]
+ for _, row := range tb.projectionFor(tb.players.get("a")) {
+ if row.Total == a+b && row.Net != want-1 {
+ t.Fatalf("preview for %d+%d: got %d, want %d", a, b, row.Net, want-1)
+ }
+ }
+ tb.resolveRoll(a, b)
+ if w.paid != want || len(w.open) != 0 || tb.players.get("a").HasBet || tb.point != 0 {
+ t.Fatalf("roll %d+%d: paid %d, want %d; open %v, point %d", a, b, w.paid, want, w.open, tb.point)
+ }
+ totalReturned += w.paid
+ if w.paid > 0 {
+ wins++
+ }
+ }
+ }
+ // 36 credits wagered, 35 returned: house edge exactly 1/36 = 2.777...%.
+ if totalReturned != 35 || wins != 12 {
+ t.Fatalf("all 36 rolls: returned %d, wins %d", totalReturned, wins)
+ }
+}
diff --git a/internal/casino/craps_variants.go b/internal/casino/craps_variants.go
index b3be51b..e64fd0f 100644
--- a/internal/casino/craps_variants.go
+++ b/internal/casino/craps_variants.go
@@ -75,10 +75,18 @@ func (c CrapsConfig) outcome(kind string, n, a, b, point int) (bool, int, int) {
s := a + b
switch c.variant() {
case "simplified":
- if s == 2 || s == 3 || s == 4 || s == 10 || s == 11 || s == 12 {
+ switch s {
+ case 2:
+ return true, 3, 1
+ case 3, 4:
return true, 1, 1
+ case 10, 11:
+ return true, 2, 1
+ case 12:
+ return true, 5, 1
+ default:
+ return true, -1, 1
}
- return true, -1, 1
case "high_point":
if n == 0 {
if s == 2 || s == 3 {
@@ -161,7 +169,7 @@ func (c CrapsConfig) variantRulesText() string {
rules := ""
switch c.variant() {
case "simplified":
- rules = "Pass only, one roll: 2/3/4/10/11/12 win 1:1; 5/6/7/8/9 lose. No point. A loss passes the dice."
+ rules = "Pass only, one roll of two dice. Net payouts: 2 pays 3:1; 3/4 pay 1:1; 10/11 pay 2:1; 12 pays 5:1. Totals 5/6/7/8/9 lose. Winning bets also return their stake. No point, odds, come, place, side bets or multi-roll wagers. House edge: 1/36 (approximately 2.78%). A loss passes the dice."
case "high_point":
rules = "Pass only, 1:1. Come-out 2/3 roll again, 11/12 win; 4-10 establish a point. The very next roll wins only if HIGHER than the point; equal/lower loses. Loss passes the dice."
case "die_rich":
diff --git a/internal/casino/craps_variants_test.go b/internal/casino/craps_variants_test.go
index 687a496..284a3df 100644
--- a/internal/casino/craps_variants_test.go
+++ b/internal/casino/craps_variants_test.go
@@ -51,10 +51,7 @@ func TestVariantAllOutcomes(t *testing.T) {
switch v {
case "simplified":
done = true
- profit = -1
- if s <= 4 || s >= 10 {
- profit = 1
- }
+ profit = [13]int{0, 0, 3, 1, 1, -1, -1, -1, -1, -1, 2, 2, 5}[s]
case "high_point":
if n == 0 {
done = s >= 11
diff --git a/internal/casino/manager_test.go b/internal/casino/manager_test.go
index 43bd67f..889618c 100644
--- a/internal/casino/manager_test.go
+++ b/internal/casino/manager_test.go
@@ -58,10 +58,6 @@ func (w *testWallet) ResolveStakesByIDs(stakeIDs ...string) {
}
}
-// ResolveStakes mirrors the production wallet: every outstanding stake is
-// settled (no longer refundable).
-func (w *testWallet) ResolveStakes() { w.open = nil }
-
// RefundOutstanding returns every outstanding stake to the balances and
// resolves the stakes, mirroring the production refund path: credits are
// paid through PayCredits (recorded in paid), chips return as chips.
diff --git a/internal/casino/types.go b/internal/casino/types.go
index 56a09d8..62a8f34 100644
--- a/internal/casino/types.go
+++ b/internal/casino/types.go
@@ -217,9 +217,6 @@ type Wallet interface {
// startup, never paid on top of the refund — and the game must not
// announce the win.
PayCredits(amount int, stakeIDs ...string) error
- // ResolveStakes consumes every pending stake for this player: the round
- // resolved as a loss or a forfeit, so the stakes must not be refunded.
- ResolveStakes()
// ResolveStake consumes ONE pending stake by wallet handle (a single
// hand paid or lost while a sibling hand's round is still in flight).
ResolveStake(stakeID string)
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 <n>", 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 <n>", "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)
+}