aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/casino/roulette.go39
-rw-r--r--internal/casino/roulette_test.go14
-rw-r--r--internal/casino/table_ux.go2
-rw-r--r--internal/game/render_roulette.go88
-rw-r--r--internal/game/render_roulette_test.go57
-rw-r--r--internal/game/roulette_test.go2
6 files changed, 167 insertions, 35 deletions
diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go
index ed649e9..5784ac9 100644
--- a/internal/casino/roulette.go
+++ b/internal/casino/roulette.go
@@ -34,9 +34,31 @@ func rouletteRed(n int) bool {
return false
}
+// normalizeRouletteSpot maps legacy bet names onto the labels shown on the
+// board. The old dozen/column spellings stay accepted so existing habits and
+// older help text keep working.
+func normalizeRouletteSpot(spot string) string {
+ switch spot {
+ case "dozen1":
+ return "1to12"
+ case "dozen2":
+ return "13to24"
+ case "dozen3":
+ return "25to36"
+ case "column1":
+ return "row1"
+ case "column2":
+ return "row2"
+ case "column3":
+ return "row3"
+ }
+ return spot
+}
+
// Returns the covered pockets and total-return multiplier (stake included).
// Zero never belongs to an outside bet.
func rouletteBet(spot string, double bool) ([]string, int) {
+ spot = normalizeRouletteSpot(spot)
if spot == "00" {
if double {
return []string{spot}, 36
@@ -62,18 +84,18 @@ func rouletteBet(spot string, double bool) ([]string, int) {
hit = n <= 18
case "high":
hit = n >= 19
- case "dozen1":
+ case "1to12":
hit = n <= 12
- case "dozen2":
+ case "13to24":
hit = n >= 13 && n <= 24
- case "dozen3":
+ case "25to36":
hit = n >= 25
- case "column1":
- hit = n%3 == 1
- case "column2":
- hit = n%3 == 2
- case "column3":
+ case "row1":
hit = n%3 == 0
+ case "row2":
+ hit = n%3 == 2
+ case "row3":
+ hit = n%3 == 1
}
if hit {
pockets = append(pockets, strconv.Itoa(n))
@@ -162,6 +184,7 @@ func (t *rouletteTable) bet(name, spot string, amount int, w Wallet) []Event {
if spot == "" {
spot = p.lastSpot
}
+ spot = normalizeRouletteSpot(spot)
_, mult := rouletteBet(spot, t.Config.Roulette.DoubleZero)
if mult == 0 {
return []Event{t.private(name, "Unknown roulette target. Use table to see all available bets.")}
diff --git a/internal/casino/roulette_test.go b/internal/casino/roulette_test.go
index 3ef3814..a20b17c 100644
--- a/internal/casino/roulette_test.go
+++ b/internal/casino/roulette_test.go
@@ -32,7 +32,7 @@ func TestRouletteRulesAndWheels(t *testing.T) {
if seen["00"] != double {
t.Fatal("wrong double zero")
}
- for _, spot := range strings.Fields("red black even odd low high dozen1 dozen2 dozen3 column1 column2 column3") {
+ for _, spot := range strings.Fields("red black even odd low high dozen1 dozen2 dozen3 column1 column2 column3 1to12 13to24 25to36 row1 row2 row3") {
pockets, mult := rouletteBet(spot, double)
if len(pockets)*mult != 36 {
t.Fatalf("%s coverage/payout %d/%d", spot, len(pockets), mult)
@@ -54,6 +54,18 @@ func TestRouletteRulesAndWheels(t *testing.T) {
}
}
}
+func TestRouletteRowBetMapping(t *testing.T) {
+ // row1 is the top numbered line (3,6,...36) and row3 the bottom (1,4,...34).
+ row1, mult := rouletteBet("row1", false)
+ if mult != 3 || len(row1) != 12 || row1[0] != "3" || row1[len(row1)-1] != "36" {
+ t.Fatalf("row1 = %v (x%d), want 3..36", row1, mult)
+ }
+ row3, mult := rouletteBet("row3", false)
+ if mult != 3 || len(row3) != 12 || row3[0] != "1" || row3[len(row3)-1] != "34" {
+ t.Fatalf("row3 = %v (x%d), want 1..34", row3, mult)
+ }
+}
+
func rouletteFixture() *rouletteTable {
return newRouletteTable(1, TableConfig{ID: "r", Game: GameRoulette, BettingTicks: 2, Roulette: RouletteConfig{SpinTicks: 2}}, rand.New(rand.NewSource(7)))
}
diff --git a/internal/casino/table_ux.go b/internal/casino/table_ux.go
index cec766d..db700d6 100644
--- a/internal/casino/table_ux.go
+++ b/internal/casino/table_ux.go
@@ -13,7 +13,7 @@ func (c TableConfig) RulesText() string {
case GameBaccarat:
return c.BaccaratRulesText()
case GameRoulette:
- return c.Label() + "\nStraight numbers pay 35:1; dozens/columns 2:1; other outside bets 1:1.\nZero pockets lose all outside bets. One wager per spin. Use help roulette."
+ return c.Label() + "\nStraight numbers pay 35:1; 1to12/13to24/25to36 and rows 2:1; other outside bets 1:1.\nZero pockets lose all outside bets. One wager per spin. Use help roulette."
default:
return c.Label()
}
diff --git a/internal/game/render_roulette.go b/internal/game/render_roulette.go
index 95e91ff..4861939 100644
--- a/internal/game/render_roulette.go
+++ b/internal/game/render_roulette.go
@@ -80,26 +80,56 @@ func renderRouletteLayout(double bool, mode string, unicode bool) string {
title = "ROULETTE / DOUBLE ZERO"
}
lines = append(lines, rouletteInk(mode, title))
- // Five columns for the zero lane, twelve four-column pockets. Four
- // adjacent pockets line up exactly over each 19-column dozen.
+ // Five columns for the zero lane, twelve four-column pockets, and a
+ // six-column 2:1 lane on the right holding the row bets. Four adjacent
+ // pockets line up exactly over each 19-column dozen.
+ const rowCell = 6
gridRule := func(left, joint, right string) string {
- return left + strings.Repeat(b.h, 5) + joint + strings.Join(repeatRoulette(strings.Repeat(b.h, 4), 12), joint) + right
+ return left + strings.Repeat(b.h, 5) + joint +
+ strings.Join(repeatRoulette(strings.Repeat(b.h, 4), 12), joint) +
+ joint + strings.Repeat(b.h, rowCell) + right
+ }
+ // The numbered grid's bottom edge closes the row-bet cells and carries the
+ // dozen-row dividers as downward stubs: crosses at the inner dozen
+ // boundaries and where the grid meets the row lane, an upward tee
+ // everywhere else.
+ gridBottom := func() string {
+ var sb strings.Builder
+ sb.WriteString(b.bl)
+ for col := 6; col <= 66; col += 5 {
+ if col == 6 {
+ sb.WriteString(strings.Repeat(b.h, 5))
+ } else {
+ sb.WriteString(strings.Repeat(b.h, 4))
+ }
+ switch col {
+ case 6, 26, 46, 66:
+ sb.WriteString(b.cross)
+ default:
+ sb.WriteString(b.teeBottom)
+ }
+ }
+ sb.WriteString(strings.Repeat(b.h, rowCell))
+ sb.WriteString(b.br)
+ return sb.String()
}
lines = append(lines, rouletteInk(mode, gridRule(b.tl, b.teeTop, b.tr)))
for row := 3; row >= 1; row-- {
- zero, zeroBg := "", -1
+ zero := ""
switch {
case double && row == 3:
- zero, zeroBg = "0", 2
+ zero = "0"
case double && row == 1:
- zero, zeroBg = "00", 2
+ zero = "00"
case !double && row == 2:
- zero, zeroBg = "0", 2
+ zero = "0"
}
- if zeroBg == 2 && (mode == "none" || mode == "") {
+ if mode == "none" || mode == "" {
zero += "G"
}
- line := rouletteInk(mode, b.v) + roulettePaint(mode, rouletteCenter(zero, 5), zeroBg) + rouletteInk(mode, b.v)
+ // The 0/00 lane is one green rectangle, so its blank cells above and
+ // below the zeros are painted to match the pockets beside them.
+ line := rouletteInk(mode, b.v) + roulettePaint(mode, rouletteCenter(zero, 5), 2) + rouletteInk(mode, b.v)
for n := row; n <= 36; n += 3 {
pocket := strconv.Itoa(n)
bg, name := roulettePocketColor(pocket)
@@ -108,22 +138,32 @@ func renderRouletteLayout(double bool, mode string, unicode bool) string {
}
line += roulettePaint(mode, rouletteCenter(pocket, 4), bg) + rouletteInk(mode, b.v)
}
- line += rouletteInk(mode, fmt.Sprintf(" column%d", row))
+ // row1 is the top numbered line (3,6,...36) and row3 the bottom.
+ line += rouletteInk(mode, rouletteCenter(fmt.Sprintf("row%d", 4-row), rowCell)+b.v)
lines = append(lines, line)
if row > 1 {
- // The zero lane spans the grid height; only its labeled cell is green.
- line = rouletteInk(mode, b.v+" "+b.teeLeft+strings.Join(repeatRoulette(strings.Repeat(b.h, 4), 12), b.cross)+b.teeRight)
+ line = rouletteInk(mode, b.v+" "+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)
}
}
- lines = append(lines, rouletteInk(mode, gridRule(b.bl, b.teeBottom, b.br)))
- // Outside bets share the exact 60-column numbered-grid footprint.
+ lines = append(lines, rouletteInk(mode, gridBottom()))
+ // Outside bets share the exact 60-column numbered-grid footprint. The
+ // dozen row gets its own bottom rule so the 1to12/13to24/25to36 boxes are
+ // closed and do not run into the even-money spaces below them.
indent := " "
dozen := indent + rouletteInk(mode, b.v)
- for i := 1; i <= 3; i++ {
- dozen += rouletteInk(mode, rouletteCenter(fmt.Sprintf("dozen%d", i), 19)) + rouletteInk(mode, b.v)
+ for _, label := range []string{"1to12", "13to24", "25to36"} {
+ dozen += rouletteInk(mode, rouletteCenter(label, 19)) + rouletteInk(mode, b.v)
}
lines = append(lines, dozen)
+ dozenBottom := indent + b.teeLeft
+ dozenJoints := []string{b.teeTop, b.cross, b.teeTop, b.cross, b.teeTop, b.teeRight}
+ for i := 0; i < 6; i++ {
+ dozenBottom += strings.Repeat(b.h, 9) + dozenJoints[i]
+ }
+ lines = append(lines, rouletteInk(mode, dozenBottom))
line := indent + rouletteInk(mode, b.v)
for _, bet := range []string{"low", "even", "red", "black", "odd", "high"} {
bg := -1
@@ -135,7 +175,17 @@ func renderRouletteLayout(double bool, mode string, unicode bool) string {
}
line += roulettePaint(mode, rouletteCenter(bet, 9), bg) + rouletteInk(mode, b.v)
}
- lines = append(lines, line, rouletteInk(mode, indent+b.bl+strings.Repeat(b.h, 59)+b.br))
+ // The bottom rule tees upward under each outside-bet divider so the
+ // vertical lines below the dozens meet the table's bottom edge.
+ bottom := indent + b.bl
+ for i := 0; i < 6; i++ {
+ bottom += strings.Repeat(b.h, 9)
+ if i < 5 {
+ bottom += b.teeBottom
+ }
+ }
+ bottom += b.br
+ lines = append(lines, line, rouletteInk(mode, bottom))
if mode == "none" || mode == "" {
lines = append(lines, "R = red, B = black, G = green")
}
@@ -317,9 +367,9 @@ 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 | Dozens / columns 2:1 | Other outside bets 1:1\n" +
+ text := "Numbers 35:1 | 1to12 / 13to24 / 25to36 and rows 2:1 | Other outside bets 1:1\n" +
"Low: 1-18 | High: 19-36 | Zero loses all outside bets\n" +
- "bet 10 on 17 / bet 10 on red / bet 10 on dozen2"
+ "bet 10 on 17 / bet 10 on red / bet 10 on 13to24"
return g.colorize(sess, "casino_action", text)
}
diff --git a/internal/game/render_roulette_test.go b/internal/game/render_roulette_test.go
index 1dde5ad..c1065e5 100644
--- a/internal/game/render_roulette_test.go
+++ b/internal/game/render_roulette_test.go
@@ -74,7 +74,7 @@ func TestRouletteArtModes(t *testing.T) {
} else if !strings.Contains(string([]rune(rows[4])[:7]), "0") {
t.Fatal("zero not left of grid", layout)
}
- for _, target := range []string{"column1", "column2", "column3", "dozen1", "dozen2", "dozen3", "red", "black", "odd", "even", "low", "high"} {
+ for _, target := range []string{"1to12", "13to24", "25to36", "row1", "row2", "row3", "red", "black", "odd", "even", "low", "high"} {
if !strings.Contains(layout, target) {
t.Fatal("missing bet", target)
}
@@ -86,10 +86,9 @@ func TestRouletteArtModes(t *testing.T) {
}
}
if mode != "none" {
- wantGreen := 1
- if double {
- wantGreen = 2
- }
+ // The whole 0/00 lane is green: blank cells above and below
+ // the zeros fill the rectangle.
+ wantGreen := 3
if got := strings.Count(layoutArt, color.BgCode(mode, 2)); got != wantGreen {
t.Fatalf("green cells on board = %d, want %d", got, wantGreen)
}
@@ -135,6 +134,54 @@ func TestRouletteArtModes(t *testing.T) {
}
}
+// TestRouletteLayoutConnections guards the box-drawing joints: the dozen-row
+// boxes must connect to the numbered grid above and be closed under, the
+// row-bet cells must be boxed on the right, and the bottom rule must tee under
+// each outside-bet divider.
+func TestRouletteLayoutConnections(t *testing.T) {
+ for _, double := range []bool{false, true} {
+ art := plainRoulette(renderRouletteLayout(double, "xterm256", true))
+ rows := strings.Split(art, "\n")
+ // rows[7] is the numbered grid's bottom edge, directly above the dozen
+ // labels. The dozen boundaries and the grid/row-lane joint cross; the
+ // row-bet lane closes with a bottom-right corner.
+ gridBottom := []rune(rows[7])
+ for _, col := range []int{6, 26, 46, 66} {
+ if gridBottom[col] != '┼' {
+ t.Fatalf("double=%v: grid bottom col %d = %q, want ┼:\n%s", double, col, gridBottom[col], art)
+ }
+ }
+ if gridBottom[73] != '┘' {
+ t.Fatalf("double=%v: grid bottom row-lane corner = %q, want ┘:\n%s", double, gridBottom[73], art)
+ }
+ // The row-bet lane is boxed: its separators cross at the grid edge and
+ // close on the right.
+ for _, idx := range []int{3, 5} {
+ sep := []rune(rows[idx])
+ if sep[66] != '┼' || sep[73] != '┤' {
+ t.Fatalf("double=%v: row lane separator %q lacks box joints:\n%s", double, rows[idx], art)
+ }
+ }
+ // rows[9] is the dozen boxes' bottom edge: tees point down where the
+ // even-money dividers sit and crosses pass the dozen dividers through.
+ dozenBottom := []rune(rows[9])
+ wantDozen := map[int]rune{6: '├', 16: '┬', 26: '┼', 36: '┬', 46: '┼', 56: '┬', 66: '┤'}
+ for col, want := range wantDozen {
+ if dozenBottom[col] != want {
+ t.Fatalf("double=%v: dozen bottom col %d = %q, want %q:\n%s", double, col, dozenBottom[col], want, art)
+ }
+ }
+ // The last line is the table's bottom rule; it tees upward under each
+ // outside-bet divider.
+ bottom := []rune(rows[len(rows)-1])
+ for _, col := range []int{16, 26, 36, 46, 56} {
+ if bottom[col] != '┴' {
+ t.Fatalf("double=%v: table bottom col %d = %q, want ┴:\n%s", double, col, bottom[col], art)
+ }
+ }
+ }
+}
+
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 9537710..a963913 100644
--- a/internal/game/roulette_test.go
+++ b/internal/game/roulette_test.go
@@ -17,7 +17,7 @@ func TestRouletteNumericTargetsAndRendering(t *testing.T) {
conn := &casinoTestConn{}
sess := &net.Session{Player: player.New("alice"), Conn: conn}
g.loggedInChars = map[string]*net.Session{"alice": sess}
- for _, target := range []string{"0", "17", "00", "red", "column2"} {
+ for _, target := range []string{"0", "17", "00", "red", "row2"} {
conn.data = nil
g.executeBet(sess, []string{"10", "on", target}, "bet 10 on "+target)
expected := "You don't have enough chips and credits"