aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-29 14:35:57 -0400
committerhistoria <[not public]>2026-09-29 14:35:57 -0400
commit61fb8e950e0b30ef0ee65aa5882545e5292d9112 (patch)
tree06319b38231d1b86236b8d3944a1accf9824eb08 /internal/game
parent004dcd3826856870b267a55dacf84aa7742b7e3f (diff)
downloadthehouseoficarus-61fb8e950e0b30ef0ee65aa5882545e5292d9112.tar.gz
fix: roulette table box drawing characters
Diffstat (limited to 'internal/game')
-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
3 files changed, 122 insertions, 25 deletions
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"