diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/casino.go | 17 | ||||
| -rw-r--r-- | internal/game/look_entities.go | 4 | ||||
| -rw-r--r-- | internal/game/render_roulette.go | 25 | ||||
| -rw-r--r-- | internal/game/render_roulette_test.go | 33 | ||||
| -rw-r--r-- | internal/game/roulette_test.go | 52 |
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 |
