aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/casino.go17
-rw-r--r--internal/game/look_entities.go4
-rw-r--r--internal/game/render_roulette.go25
-rw-r--r--internal/game/render_roulette_test.go33
-rw-r--r--internal/game/roulette_test.go52
5 files changed, 114 insertions, 17 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 8c10dfd..9282653 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -431,6 +431,10 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
// sets the wager and any other token is the bet target; AFTER "on", the
// next token is always the target. That makes numeric targets reachable
// ("bet 5 on 17"), which roulette, craps, and keno need.
+ // Roulette combination bets need a multi-token target ("split 1 2",
+ // "corner 1 2 4 5"), so a roulette table joins every extra target token.
+ // Every other game keeps the stricter one-token target and its usage error.
+ joinTarget := table.Config.Game == casino.GameRoulette
amount, spot, hasAmount, sawOn := 0, "", false, false
for i, arg := range args {
if strings.EqualFold(arg, "on") {
@@ -472,11 +476,16 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
continue
}
}
- if spot != "" {
- sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
- return
+ if spot == "" {
+ spot = strings.ToLower(arg)
+ continue
}
- spot = strings.ToLower(arg)
+ if joinTarget {
+ spot += " " + strings.ToLower(arg)
+ continue
+ }
+ sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
+ return
}
if table.Config.Game == casino.GameBaccarat {
switch spot {
diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go
index 2507073..e120c9c 100644
--- a/internal/game/look_entities.go
+++ b/internal/game/look_entities.go
@@ -231,7 +231,7 @@ func (g *Game) showRoomCasinos(sess *net.Session, room *world.Room) []string {
var lines []string
for _, table := range room.CasinoTables {
cfg := table.Normalize()
- label := fmt.Sprintf("%s | Limits %d-%d | 0/%d seated | Betting open", cfg.Label(), cfg.MinBet, cfg.MaxBet, cfg.MaxPlayers)
+ label := fmt.Sprintf("%s | Limits %d-%d | 0/%d seated", cfg.Label(), cfg.MinBet, cfg.MaxBet, cfg.MaxPlayers)
if g.Casino != nil {
if live := g.Casino.Table(room.ID, table.ID); live != nil {
label = g.casinoTimeText(live.Overview())
@@ -244,7 +244,7 @@ func (g *Game) showRoomCasinos(sess *net.Session, room *world.Room) []string {
if table.Game == casino.GameBaccarat {
alias = "bac"
}
- lines = append(lines, g.colorize(sess, "casino_action", label), " "+g.colorize(sess, "command", "play "+alias)+" Join | "+g.colorize(sess, "command", "watch "+alias)+" Spectate")
+ lines = append(lines, g.colorize(sess, "casino_action", label), " "+g.colorize(sess, "command", "play "+alias)+" | "+g.colorize(sess, "command", "watch "+alias))
}
for _, machine := range room.CasinoMachines {
label := casinoGameLabel(machine.Game) + " machine"
diff --git a/internal/game/render_roulette.go b/internal/game/render_roulette.go
index cfded31..47ba5c6 100644
--- a/internal/game/render_roulette.go
+++ b/internal/game/render_roulette.go
@@ -53,13 +53,19 @@ func rouletteCenter(text string, width int) string {
return strings.Repeat(" ", pad/2) + text + strings.Repeat(" ", pad-pad/2)
}
-type rouletteLines struct{ h, v, tl, tr, bl, br, teeTop, teeBottom, teeLeft, teeRight, cross string }
+type rouletteLines struct {
+ h, v, tl, tr, bl, br, teeTop, teeBottom, teeLeft, teeRight, cross string
+ // zeroTL/zeroBL are the zero lane's outer corners. They are rounded while
+ // the rest of the board keeps square joints, so the tall green zero panel
+ // reads as a soft-edged block without disturbing the grid's connections.
+ zeroTL, zeroBL string
+}
func rouletteBorders(unicode bool) rouletteLines {
if unicode {
- return rouletteLines{"─", "│", "┌", "┐", "└", "┘", "┬", "┴", "├", "┤", "┼"}
+ return rouletteLines{"─", "│", "┌", "┐", "└", "┘", "┬", "┴", "├", "┤", "┼", "╭", "╰"}
}
- return rouletteLines{"-", "|", "+", "+", "+", "+", "+", "+", "+", "+", "+"}
+ return rouletteLines{"-", "|", "+", "+", "+", "+", "+", "+", "+", "+", "+", "+", "+"}
}
func repeatRoulette(s string, n int) []string {
@@ -97,7 +103,7 @@ func renderRouletteLayout(double bool, mode string, unicode bool) string {
// everywhere else.
gridBottom := func() string {
var sb strings.Builder
- sb.WriteString(b.bl)
+ sb.WriteString(b.zeroBL)
for col := 6; col <= 66; col += 5 {
if col == 6 {
sb.WriteString(strings.Repeat(b.h, 5))
@@ -115,7 +121,7 @@ func renderRouletteLayout(double bool, mode string, unicode bool) string {
sb.WriteString(b.br)
return sb.String()
}
- lines = append(lines, rouletteInk(mode, gridRule(b.tl, b.teeTop, b.tr)))
+ lines = append(lines, rouletteInk(mode, gridRule(b.zeroTL, b.teeTop, b.tr)))
for row := 3; row >= 1; row-- {
zero := ""
switch {
@@ -144,7 +150,9 @@ func renderRouletteLayout(double bool, mode string, unicode bool) string {
line += rouletteInk(mode, rouletteCenter(fmt.Sprintf("row%d", 4-row), rowCell)+b.v)
lines = append(lines, line)
if row > 1 {
- line = rouletteInk(mode, b.v+" "+b.teeLeft+
+ // The zero lane is one green rectangle, so the two separator rows
+ // are painted to match the cells above and below them.
+ line = rouletteInk(mode, b.v) + roulettePaint(mode, " ", 2) + rouletteInk(mode, b.teeLeft+
strings.Join(repeatRoulette(strings.Repeat(b.h, 4), 12), b.cross)+
b.cross+strings.Repeat(b.h, rowCell)+b.teeRight)
lines = append(lines, line)
@@ -428,9 +436,10 @@ func renderRouletteResult(pockets [3]string, mode string, unicode bool, size int
// rouletteReminder is the odds/cheat-sheet block, shown only by "table" (and
// "table rules"), never with the ambient betting board.
func (g *Game) rouletteReminder(sess *net.Session) string {
- text := "Numbers 35:1 | 1to12 / 13to24 / 25to36 and rows 2:1 | Other outside bets 1:1\n" +
+ text := "Numbers 35:1 | Split 17:1 | Street/trio 11:1 | Corner 8:1 | Line 5:1 | Dozens & rows 2:1 | Outside 1:1\n" +
+ "Combos: split 1 2 | street 1 2 3 | corner 1 2 4 5 | line 1 2 3 4 5 6 | trio 0 1 2 | basket (double zero)\n" +
"Low: 1-18 | High: 19-36 | Zero loses all outside bets\n" +
- "bet 10 on 17 / bet 10 on red / bet 10 on 13to24"
+ "bet 10 on 17 / bet 10 on red / bet 10 on split 1 2"
return g.colorize(sess, "casino_action", text)
}
diff --git a/internal/game/render_roulette_test.go b/internal/game/render_roulette_test.go
index b1a6005..1cb75ce 100644
--- a/internal/game/render_roulette_test.go
+++ b/internal/game/render_roulette_test.go
@@ -86,9 +86,9 @@ func TestRouletteArtModes(t *testing.T) {
}
}
if mode != "none" {
- // The whole 0/00 lane is green: blank cells above and below
- // the zeros fill the rectangle.
- wantGreen := 3
+ // The whole 0/00 lane is green: the three numbered rows plus
+ // the two separator rows that run through the lane.
+ wantGreen := 5
if got := strings.Count(layoutArt, color.BgCode(mode, 2)); got != wantGreen {
t.Fatalf("green cells on board = %d, want %d", got, wantGreen)
}
@@ -199,6 +199,33 @@ func TestRouletteLayoutConnections(t *testing.T) {
}
}
+// TestRouletteZeroPanel checks the two zero-panel fixes: the whole five-row
+// lane carries the green background, and its outer corners are rounded in
+// Unicode mode while ASCII mode stays square.
+func TestRouletteZeroPanel(t *testing.T) {
+ for _, double := range []bool{false, true} {
+ colored := renderRouletteLayout(double, "xterm256", true)
+ rows := strings.Split(colored, "\n")
+ // Interior rows 2-6: top numbered row, separator, middle, separator,
+ // bottom numbered row. Each must contain the green background.
+ for row := 2; row <= 6; row++ {
+ if !strings.Contains(rows[row], color.BgCode("xterm256", 2)) {
+ t.Fatalf("double=%v: zero lane row %d is not green:\n%s", double, row, plainRoulette(colored))
+ }
+ }
+ plainRows := strings.Split(plainRoulette(colored), "\n")
+ if !strings.HasPrefix(plainRows[1], "╭") || !strings.HasPrefix(plainRows[7], "╰") {
+ t.Fatalf("double=%v: zero panel corners are not rounded:\n%s", double, plainRoulette(colored))
+ }
+ ascii := plainRoulette(renderRouletteLayout(double, "none", false))
+ for _, r := range ascii {
+ if r > 127 {
+ t.Fatalf("double=%v: non-ASCII rune %q in ASCII mode", double, r)
+ }
+ }
+ }
+}
+
func TestRouletteResultSizesAndNeighbors(t *testing.T) {
for _, unicode := range []bool{false, true} {
for _, size := range []int{1, 5, 7, 9} {
diff --git a/internal/game/roulette_test.go b/internal/game/roulette_test.go
index a963913..7a22d9b 100644
--- a/internal/game/roulette_test.go
+++ b/internal/game/roulette_test.go
@@ -43,6 +43,58 @@ func TestRouletteNumericTargetsAndRendering(t *testing.T) {
}
}
+func TestRouletteCombinationTargetParsing(t *testing.T) {
+ for _, double := range []bool{false, true} {
+ g := &Game{Casino: casino.NewManager(1)}
+ cfg := casino.TableConfig{ID: "roulette", Game: casino.GameRoulette, Roulette: casino.RouletteConfig{DoubleZero: double}}
+ g.Casino.Join(1, cfg, "alice")
+ conn := &casinoTestConn{}
+ sess := &net.Session{Player: player.New("alice"), Conn: conn}
+ g.loggedInChars = map[string]*net.Session{"alice": sess}
+ // Multi-token targets are joined for roulette, with or without "on".
+ valid := [][]string{
+ {"10", "on", "split", "1", "2"},
+ {"10", "on", "split", "1-2"},
+ {"10", "split", "1-2"},
+ {"10", "on", "street", "1", "2", "3"},
+ {"10", "on", "street", "3", "1", "2"},
+ {"10", "on", "corner", "1", "2", "4", "5"},
+ {"10", "on", "corner", "5", "1", "4", "2"},
+ {"10", "on", "line", "1", "2", "3", "4", "5", "6"},
+ {"10", "on", "trio", "0", "1", "2"},
+ {"10", "on", "basket"},
+ }
+ if double {
+ valid = append(valid, []string{"10", "on", "split", "0", "00"})
+ }
+ for _, args := range valid {
+ conn.data = nil
+ g.executeBet(sess, args, "bet "+strings.Join(args, " "))
+ if !strings.Contains(string(conn.data), "You don't have enough chips and credits") {
+ t.Fatalf("double=%v args=%v: expected wallet failure, got %q", double, args, conn.data)
+ }
+ }
+ // Non-adjacent or wrong-shaped combinations must not reach the wallet.
+ invalid := [][]string{
+ {"10", "on", "split", "1", "5"},
+ {"10", "on", "street", "2", "3", "4"},
+ {"10", "on", "corner", "1", "2", "3", "4"},
+ {"10", "on", "line", "1", "2", "3", "4", "5", "7"},
+ {"10", "on", "trio", "0", "1", "5"},
+ }
+ if !double {
+ invalid = append(invalid, []string{"10", "on", "split", "0", "00"})
+ }
+ for _, args := range invalid {
+ conn.data = nil
+ g.executeBet(sess, args, "bet "+strings.Join(args, " "))
+ if !strings.Contains(string(conn.data), "Unknown roulette target") {
+ t.Fatalf("double=%v args=%v: expected target rejection, got %q", double, args, conn.data)
+ }
+ }
+ }
+}
+
func TestCasinoTestPortalAuthoredContract(t *testing.T) {
g, rooms, _ := loadVesta(t)
found := false