aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/casino.go47
-rw-r--r--internal/game/casino_look_test.go2
-rw-r--r--internal/game/casino_ux_test.go4
-rw-r--r--internal/game/pai_gow_test.go86
-rw-r--r--internal/game/render_pai_gow.go17
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 {