diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/casino.go | 47 | ||||
| -rw-r--r-- | internal/game/casino_look_test.go | 2 | ||||
| -rw-r--r-- | internal/game/casino_ux_test.go | 4 | ||||
| -rw-r--r-- | internal/game/pai_gow_test.go | 86 | ||||
| -rw-r--r-- | internal/game/render_pai_gow.go | 17 |
5 files changed, 107 insertions, 49 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go index db5bfad..66c90a1 100644 --- a/internal/game/casino.go +++ b/internal/game/casino.go @@ -595,8 +595,8 @@ func (g *Game) emitCasinoEvents(events []casino.Event) { privateMessage := g.casinoPrivateMessage(sess, category, event.Menu, g.casinoTimeText(text)) privateMessage = g.renderCasinoMessage(sess, category, event.Menu, privateMessage) rendered := g.renderCasinoSnapshot(sess, event.Snapshot) - if header, reminder, ok := g.casinoBoardReminder(sess, category, event.Menu, event.Snapshot, g.casinoTimeText(text)); ok { - privateMessage = composeTableThenReminder(header, rendered, reminder) + if reminder, ok := g.casinoBoardReminder(sess, category, event.Menu, event.Snapshot, g.casinoTimeText(text)); ok { + privateMessage = composeTableThenReminder(rendered, reminder) } else if rendered != "" { privateMessage += "\n" + rendered } @@ -635,35 +635,37 @@ func (g *Game) casinoMenuParts(sess *net.Session, menu *casino.MenuView, message // casinoBoardReminder splits a persistent-board menu (craps, roulette, video // poker, Pai Gow) so its instructions print under the board they describe. // Every other game, and compact displays, keep their menu in one block. -func (g *Game) casinoBoardReminder(sess *net.Session, category string, menu *casino.MenuView, snapshot casino.Snapshot, message string) (header, reminder string, ok bool) { +func (g *Game) casinoBoardReminder(sess *net.Session, category string, menu *casino.MenuView, snapshot casino.Snapshot, message string) (reminder string, ok bool) { if category != "casino_menu" || menu == nil || sess.Player == nil { - return "", "", false + return "", false } _, isCraps := snapshot.(*casino.CrapsSnapshot) _, isPoker := snapshot.(*casino.VideoPokerSnapshot) _, isPaiGow := snapshot.(*casino.PaiGowSnapshot) s, isRoulette := snapshot.(*casino.RouletteSnapshot) if !isPoker && !isCraps && !isPaiGow && (!isRoulette || s.Phase == "playing" || s.Phase == "result") { - return "", "", false + return "", false } if !isPoker && !isCraps && !isPaiGow && sess.Player.OptionString("casino_display") == "compact" && (menu.Repeat || !menu.ShowBalance) { // The compact reminder is already a short block; leave it intact. - return "", "", false + return "", false } balance, heading, commands := g.casinoMenuParts(sess, menu, message) - return g.casinoMenuHeader(sess, balance, heading), g.casinoMenuCommands(sess, commands), true + // The board replaces the game-name heading, and the balance sits with the + // commands underneath it rather than above the graphic. + return g.renderCasinoCommands(sess, balance, heading, commands), true } -// composeTableThenReminder joins a menu header, the board graphic, and the -// wager reminder. The board snapshot already leads with a blank line, and the -// reminder keeps exactly one blank line below the board's last visible row. -func composeTableThenReminder(header, table, reminder string) string { - out := header - if table != "" { - out += "\n" + strings.TrimRight(table, "\n") - } +// composeTableThenReminder joins a board graphic and the wager reminder. The +// board snapshot already leads with a blank line, and the reminder keeps +// exactly one blank line below the board's last visible row. +func composeTableThenReminder(table, reminder string) string { + out := strings.TrimRight(table, "\n") if reminder != "" { - out += "\n\n" + reminder + if out != "" { + out += "\n\n" + } + out += reminder } return out } @@ -766,18 +768,19 @@ func (g *Game) renderCasinoCommands(sess *net.Session, balance, heading string, } // casinoMenuHeader formats the balance and heading that introduce a casino -// menu. It is split from the command list so the roulette board can be drawn -// between them. +// menu. The heading (if any) comes first so the balance always sits directly +// above the command list. It is split from the command list so the roulette +// board can be drawn between them. func (g *Game) casinoMenuHeader(sess *net.Session, balance, heading string) string { var sb strings.Builder - if balance != "" { - sb.WriteString(balance) - sb.WriteString("\n\n") - } if heading != "" { sb.WriteString(g.colorize(sess, "casino_action", heading)) sb.WriteString("\n\n") } + if balance != "" { + sb.WriteString(balance) + sb.WriteString("\n\n") + } return strings.TrimRight(sb.String(), "\n") } diff --git a/internal/game/casino_look_test.go b/internal/game/casino_look_test.go index 71db40a..4c464f1 100644 --- a/internal/game/casino_look_test.go +++ b/internal/game/casino_look_test.go @@ -56,7 +56,7 @@ func TestLookAtTableShowsGameStateAndHints(t *testing.T) { if !strings.Contains(text, "Simplified Salon") { t.Fatalf("look lost the room: %q", text) } - if !strings.Contains(strings.ToUpper(text), "SIMPLIFIED CRAPS") || !strings.Contains(text, "Limits") { + if !strings.Contains(strings.ToUpper(text), "SIMPLIFIED CRAPS") || !strings.Contains(text, "seated") { t.Fatalf("look missing the table state: %q", text) } if !strings.Contains(text, "rules") { diff --git a/internal/game/casino_ux_test.go b/internal/game/casino_ux_test.go index 89363b0..5313484 100644 --- a/internal/game/casino_ux_test.go +++ b/internal/game/casino_ux_test.go @@ -129,7 +129,7 @@ func TestPlayAliasesJoinGames(t *testing.T) { if !ok || table == nil || string(table.Config.Game) != game { t.Fatalf("play %s: participation=%v game=%q", input, ok, game) } - if !strings.Contains(stripANSI(string(conn.data)), "Limits between") { + if !strings.Contains(stripANSI(string(conn.data)), "Available ") { t.Fatalf("play %s: missing betting menu, got %q", input, stripANSI(string(conn.data))) } } @@ -165,7 +165,7 @@ func TestTableStatusShowsStateAndRules(t *testing.T) { g.executePlay(sess, []string{"bj"}, "play bj") g.executeTableStatus(sess, nil, "table") text := stripANSI(string(conn.data)) - if !strings.Contains(text, "Limits") || !strings.Contains(text, "seated") { + if !strings.Contains(text, "LIMITS") || !strings.Contains(text, "Waiting for bets") { t.Fatalf("status missing overview: %q", text) } conn.data = nil diff --git a/internal/game/pai_gow_test.go b/internal/game/pai_gow_test.go index f13655b..af6b749 100644 --- a/internal/game/pai_gow_test.go +++ b/internal/game/pai_gow_test.go @@ -25,9 +25,8 @@ func TestPaiGowCommandsLookAndSettlement(t *testing.T) { t.Fatalf("missing %s: %s", want, text) } } - g.executeBet(sess, []string{"20"}, "bet 20") g.executeBet(sess, []string{"5", "fortune"}, "bet 5 fortune") - g.executeTableAction(sess, "deal") + g.executeBet(sess, []string{"20"}, "bet 20") snap := table.Status(sess.Player.Name)[0].Snapshot.(*casino.PaiGowSnapshot) if len(snap.Cards) != 7 || len(snap.DealerHigh) != 0 || len(sess.Player.PendingWagers) != 2 { t.Fatal("deal/side bet") @@ -132,34 +131,24 @@ func TestPaiGowContextHintsFollowState(t *testing.T) { g, sess, conn := casinoCrapsGame(t, "name: Pai Gow\ncasino_tables:\n - id: fortune\n game: paigow\n min_bet: 5\n max_bet: 1000\n pai_gow:\n envy: true\n") g.executePlay(sess, []string{"pai"}, "play pai") - // A fresh bettor is told how to place the main wager. + // A fresh bettor is told about the side bet and the main wager, plus the + // shared tail. conn.data = nil g.executeTableStatus(sess, nil, "table") text := stripANSI(string(conn.data)) - if !strings.Contains(text, "bet <amount>") { - t.Fatalf("missing main-bet hint: %s", text) - } - - // After the main wager the live menu offers Fortune and deal, not a second - // main bet, and the shared tail is present. - conn.data = nil - g.executeBet(sess, []string{"20"}, "bet 20") - text = stripANSI(string(conn.data)) - for _, want := range []string{"bet <amount> fortune", "deal", "table rules", "rules", "stop"} { + for _, want := range []string{"bet <amount> fortune", "bet <amount>", "table rules", "rules", "stop"} { if !strings.Contains(text, want) { - t.Fatalf("missing %q after main bet: %s", want, text) + t.Fatalf("missing %q at a fresh table: %s", want, text) } } - // Setting offers the split selectors and the house-way shortcut. - g.executeBet(sess, []string{"5", "fortune"}, "bet 5 fortune") - g.executeTableAction(sess, "deal") + // A lone main wager deals immediately; the live menu moves to setting. conn.data = nil - g.executeTableStatus(sess, nil, "table") + g.executeBet(sess, []string{"20"}, "bet 20") text = stripANSI(string(conn.data)) for _, want := range []string{"24 / 13456", "house"} { if !strings.Contains(text, want) { - t.Fatalf("missing %q while setting: %s", want, text) + t.Fatalf("missing %q after the main wager: %s", want, text) } } } @@ -186,3 +175,62 @@ func TestJokerEmptySuitDoesNotPanic(t *testing.T) { } } } + +func pgBoolPtr(b bool) *bool { return &b } + +func TestPaiGowFeltEndsAtBottomBorderAndNotesA2345(t *testing.T) { + on := renderPaiGowFelt(casino.TableConfig{Game: casino.GamePaiGow}, "none", false) + if !strings.Contains(on, "A2345 is second-highest straight.") { + t.Fatalf("enabled felt is missing the A2345 rule:\n%s", on) + } + lines := strings.Split(strings.TrimRight(on, "\n"), "\n") + // The bottom border is the final row and the joker note sits directly + // above it: the old blank interior row is gone. + if last := lines[len(lines)-1]; !strings.HasPrefix(last, "+") { + t.Fatalf("felt does not end on its bottom border: %q", last) + } + if !strings.Contains(lines[len(lines)-2], "Joker:") { + t.Fatalf("blank line below the joker note: %q", lines[len(lines)-2]) + } + offFelt := renderPaiGowFelt(casino.TableConfig{Game: casino.GamePaiGow, PaiGow: casino.PaiGowConfig{A2345SecondHighest: pgBoolPtr(false)}}, "none", false) + if strings.Contains(offFelt, "A2345 is second-highest straight.") { + t.Fatal("disabled felt still advertises the A2345 rule") + } +} + +func TestPaiGowEnvyColumnCentersUnderLabel(t *testing.T) { + cfg := casino.TableConfig{Game: casino.GamePaiGow, PaiGow: casino.PaiGowConfig{Envy: true}} + lines := strings.Split(renderPaiGowFelt(cfg, "none", false), "\n") + header := lines[5] + row := lines[6] + hi := strings.Index(header, "ENVY") + vi := strings.Index(row, "500") + if hi < 0 || vi < 0 { + t.Fatalf("missing header/value:\n%s\n%s", header, row) + } + // A value must sit within the label's four-column span. + if vi < hi || vi+len("500") > hi+len("ENVY") { + t.Fatalf("ENVY header and value are not aligned: header=%d value=%d\n%s\n%s", hi, vi, header, row) + } +} + +func TestPaiGowStopMidRoundQueuesDeparture(t *testing.T) { + g, sess, conn := casinoCrapsGame(t, "name: Pai Gow\ncasino_tables:\n - id: fortune\n game: paigow\n min_bet: 5\n max_bet: 1000\n") + g.executePlay(sess, []string{"pai"}, "play pai") + // A lone main wager deals at once, so stop reaches the committed-round path. + g.executeBet(sess, []string{"20"}, "bet 20") + conn.data = nil + g.executeCommand(sess, "stop", nil, "stop") + text := stripANSI(string(conn.data)) + if !strings.Contains(text, "leave after this round") { + t.Fatalf("stop mid-round did not queue a departure: %s", text) + } + if _, playing := g.Casino.Participation(sess.Player.Name); !playing { + t.Fatal("stop mid-round removed the seat too early") + } + // Finishing the hand applies the queued departure. + g.executeCommand(sess, "house", nil, "house") + if _, playing := g.Casino.Participation(sess.Player.Name); playing { + t.Fatal("queued stop did not leave after the round") + } +} diff --git a/internal/game/render_pai_gow.go b/internal/game/render_pai_gow.go index 239ddb4..d364c61 100644 --- a/internal/game/render_pai_gow.go +++ b/internal/game/render_pai_gow.go @@ -9,8 +9,8 @@ import ( ) func renderPaiGowFelt(cfg casino.TableConfig, mode string, unicode bool) string { - c := newCrapsCanvas(25) - c.box(0, 0, 76, 24) + c := newCrapsCanvas(24) + c.box(0, 0, 76, 23) c.center(1, 75, 1, strings.ToUpper(cfg.PaiGow.Label()), crapsGold) c.center(1, 75, 2, "LIMITS "+betLimitsText(cfg.MinBet, cfg.MaxBet), crapsInk) note := "5% COMMISSION" @@ -30,12 +30,15 @@ func renderPaiGowFelt(cfg casino.TableConfig, mode string, unicode bool) string if cfg.PaiGow.Envy { envy = "ENVY" } - c.text(62, 5, envy, crapsGold) + // Center the Envy header and its values over the same narrow field so the + // numbers sit under the label instead of hanging off its right edge. + const envyLeft, envyRight = 62, 66 + c.center(envyLeft, envyRight, 5, envy, crapsGold) for i, row := range casino.FortunePayTable(cfg.PaiGow.Envy) { c.text(3, 6+i, row.Hand, crapsInk) c.text(50, 6+i, fmt.Sprintf("%d:1", row.Odds), crapsInk) if cfg.PaiGow.Envy { - c.text(65, 6+i, fmt.Sprintf("%d", row.Envy), crapsInk) + c.center(envyLeft, envyRight, 6+i, fmt.Sprintf("%d", row.Envy), crapsInk) } } c.center(1, 75, 18, "Highest prize only. Winning Fortune wagers also return their stake.", crapsMuted) @@ -45,7 +48,11 @@ func renderPaiGowFelt(cfg casino.TableConfig, mode string, unicode bool) string } c.center(1, 75, 20, note, crapsInk) c.center(1, 75, 21, "High must outrank low. Copies lose.", crapsMuted) - c.center(1, 75, 22, "Joker: ace or completes straight/flush.", crapsMuted) + joker := "Joker: ace or completes straight/flush." + if cfg.PaiGow.A2345High() { + joker = "Joker: ace or completes straight/flush. A2345 is second-highest straight." + } + c.center(1, 75, 22, joker, crapsMuted) return c.render(mode, unicode) } func renderPaiGow(s *casino.PaiGowSnapshot, mode string, unicode bool, size int, style string, store *cardart.Store) string { |
