package game import ( "fmt" "strings" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" ) // A half-table: numbered boxes and the come/field/line rails on the left; // the shared centre-action bets on the right. Every region is a real box, // and shared edges are joined rather than pasted together as broken borders. const crapsFelt = 22 // #005f00: darker than ANSI green / the roulette felt. const ( crapsInk = iota crapsGold crapsRed crapsMuted crapsMarked crapsPuckOn crapsPuckOff ) var crapsPaint = []color.ColorSpec{ {Fg: 15, Bg: crapsFelt}, {Fg: 229, Bg: crapsFelt, Bold: true}, {Fg: 217, Bg: crapsFelt, Bold: true}, {Fg: 108, Bg: crapsFelt}, {Fg: 0, Bg: 220, Bold: true}, {Fg: 0, Bg: 15, Bold: true}, {Fg: 15, Bg: 0, Bold: true}, } type crapsCell struct { glyph rune edges uint8 ink int } type crapsCanvas struct{ cells [][]crapsCell } func newCrapsCanvas(w, h int) *crapsCanvas { c := &crapsCanvas{cells: make([][]crapsCell, h)} for y := range c.cells { c.cells[y] = make([]crapsCell, w) } return c } func (c *crapsCanvas) box(x1, y1, x2, y2 int) { // N=1, E=2, S=4, W=8. Unioning edges makes all T/cross joints exact. for x := x1; x < x2; x++ { c.cells[y1][x].edges |= 2 c.cells[y1][x+1].edges |= 8 c.cells[y2][x].edges |= 2 c.cells[y2][x+1].edges |= 8 } for y := y1; y < y2; y++ { c.cells[y][x1].edges |= 4 c.cells[y+1][x1].edges |= 1 c.cells[y][x2].edges |= 4 c.cells[y+1][x2].edges |= 1 } } func (c *crapsCanvas) text(x, y int, s string, ink int) { for _, r := range s { if x >= 0 && x < len(c.cells[y]) { c.cells[y][x] = crapsCell{glyph: r, ink: ink} } x++ } } func (c *crapsCanvas) center(x1, x2, y int, s string, ink int) { c.text(x1+(x2-x1+1-color.VisibleLen(s))/2, y, s, ink) } func (c *crapsCanvas) render(mode string, unicode bool) string { joints := map[uint8]rune{3: '└', 6: '┌', 9: '┘', 12: '┐', 7: '├', 11: '┴', 13: '┤', 14: '┬', 15: '┼'} var out strings.Builder for _, row := range c.cells { var run strings.Builder ink := row[0].ink flush := func() { if run.Len() > 0 { out.WriteString(color.Render(mode, crapsPaint[ink], run.String())) run.Reset() } } for _, cell := range row { if cell.ink != ink { flush() ink = cell.ink } r := cell.glyph if r == 0 { r = ' ' if cell.edges != 0 { switch { case cell.edges&5 == 0: r = '─' case cell.edges&10 == 0: r = '│' default: r = joints[cell.edges] } if !unicode { switch r { case '─': r = '-' case '│': r = '|' default: r = '+' } } } } run.WriteRune(r) } flush() out.WriteByte('\n') } return out.String() } func crapsHas(ns []int, n int) bool { for _, v := range ns { if v == n { return true } } return false } func renderCrapsTrackers(s *casino.CrapsSnapshot, mode string, unicode bool) string { c := newCrapsCanvas(77, 7) c.box(0, 0, 76, 3) c.box(0, 3, 38, 6) c.box(38, 3, 76, 6) c.text(3, 1, "FIRE", crapsGold) for i, n := range []int{4, 5, 6, 8, 9, 10} { ink, mark := crapsMuted, " " if crapsHas(s.Made, n) { ink, mark = crapsMarked, "*" } c.text(15+i*9, 1, fmt.Sprintf("[%2d%s]", n, mark), ink) } c.center(1, 75, 2, "MADE POINTS 4: 24 to 1 5: 249 to 1 6: 999 to 1", crapsInk) c.text(2, 4, "ALL SMALL", crapsGold) c.text(40, 4, "TALL", crapsGold) for i, n := range []int{2, 3, 4, 5, 6, 8, 9, 10, 11, 12} { x := 12 + i*5 if i >= 5 { x = 48 + (i-5)*5 } ink, mark := crapsMuted, " " if crapsHas(s.Seen, n) { ink, mark = crapsMarked, "*" } c.text(x, 4, fmt.Sprintf("%2d%s ", n, mark), ink) } c.center(1, 37, 5, "SMALL 34:1 | ALL 175:1", crapsInk) c.center(39, 75, 5, "TALL 34:1 | * = rolled", crapsInk) return c.render(mode, unicode) } func renderCrapsFelt(s *casino.CrapsSnapshot, mode string, unicode bool) string { c := newCrapsCanvas(77, 26) c.box(0, 0, 14, 6) c.center(1, 13, 4, "DON'T COME", crapsInk) c.center(1, 13, 5, "BAR 12", crapsMuted) nums := []int{4, 5, 6, 8, 9, 10} labels := []string{"4", "5", "SIX", "8", "NINE", "10"} pays := []string{"9:5", "7:5", "7:6", "7:6", "7:5", "9:5"} puck := func(x int, on bool) { ink, word := crapsPuckOff, "OFF" if on { ink, word = crapsPuckOn, "ON " } a, b, d := "+---+", "|"+word+"|", "+---+" if unicode { a, b, d = "╭───╮", "│"+word+"│", "╰───╯" } c.text(x, 1, a, ink) c.text(x, 2, b, ink) c.text(x, 3, d, ink) } if s.Point == 0 { puck(5, false) } for i, n := range nums { x := 14 + i*6 c.box(x, 0, x+6, 6) c.center(x+1, x+5, 4, labels[i], crapsGold) c.center(x+1, x+5, 5, pays[i], crapsMuted) if s.Point == n { puck(x+1, true) } } c.box(0, 6, 50, 10) c.center(1, 49, 7, "PLACE / BUY / LAY ON THE NUMBERS ABOVE", crapsMuted) c.center(1, 49, 9, "C O M E", crapsRed) c.box(0, 10, 50, 16) c.center(1, 49, 11, "FIELD", crapsGold) c.center(1, 49, 13, "( 2 ) 3 4 9 10 11 ( 12 )", crapsInk) twelve := s.Field12 if twelve == 0 { twelve = 2 } c.center(1, 49, 15, fmt.Sprintf("2 PAYS DOUBLE 12 PAYS %d:1", twelve), crapsGold) c.box(0, 16, 50, 19) c.center(1, 49, 18, "DON'T PASS / BAR 12", crapsInk) c.box(0, 19, 50, 22) c.center(1, 49, 21, "PASS LINE", crapsGold) c.box(0, 22, 50, 25) c.center(1, 49, 24, "FREE ODDS / BEHIND THE LINE", crapsInk) // Centre action: four bonus spots, four hardways, six propositions, horn/hop. bonus := []struct { label, pays string }{{"FIRE", "24x"}, {"SMALL", "34:1"}, {"ALL", "175:1"}, {"TALL", "34:1"}} xs := []int{50, 56, 63, 69, 76} for i, spot := range bonus { c.box(xs[i], 0, xs[i+1], 5) c.center(xs[i]+1, xs[i+1]-1, 2, spot.label, crapsGold) c.center(xs[i]+1, xs[i+1]-1, 4, spot.pays, crapsMuted) } for i, n := range []int{4, 6, 8, 10} { x, y := 50+(i%2)*13, 5+(i/2)*4 c.box(x, y, x+13, y+4) c.center(x+1, x+12, y+1, fmt.Sprintf("HARD %d", n), crapsInk) pay := 7 if n == 6 || n == 8 { pay = 9 } c.center(x+1, x+12, y+3, fmt.Sprintf("%d+%d %d:1", n/2, n/2, pay), crapsGold) } props := []string{"SEVEN", "CRAPS", "ELEVEN", "ACES", "ACE 3", "BOXCARS"} odds := []string{"4:1", "7:1", "15:1", "30:1", "15:1", "30:1"} px := []int{50, 59, 67, 76} for i, label := range props { col, y := i%3, 13+(i/3)*4 x, x2 := px[col], px[col+1] c.box(x, y, x2, y+4) c.center(x+1, x2-1, y+1, label, crapsRed) c.center(x+1, x2-1, y+3, odds[i], crapsInk) } c.box(50, 21, 63, 25) c.center(51, 62, 22, "HORN", crapsGold) c.center(51, 62, 24, "2 3 11 12", crapsMuted) c.box(63, 21, 76, 25) c.center(64, 75, 22, "HOP", crapsGold) c.center(64, 75, 24, "15:1 / 30:1", crapsMuted) return c.render(mode, unicode) } func renderCrapsDice(dice [2]int, mode string, unicode bool, style string) string { if dice[0] < 1 || dice[0] > 6 || dice[1] < 1 || dice[1] > 6 { return "" } patterns := [][3]string{{}, {" ", " o ", " "}, {"o ", " ", " o"}, {"o ", " o ", " o"}, {"o o", " ", "o o"}, {"o o", " o ", "o o"}, {"o o", "o o", "o o"}} top, bottom, v, pip := "+-------+", "+-------+", "|", "o" if unicode { top, bottom, v, pip = "╭───────╮", "╰───────╯", "│", "●" } spec := color.ColorSpec{Fg: 15, Bg: 0, Bold: true} if style == "light" { spec = color.ColorSpec{Fg: 0, Bg: 15, Bold: true} } var out strings.Builder for row := 0; row < 5; row++ { out.WriteString(" ") for i, n := range dice { line := top if row == 4 { line = bottom } else if row > 0 { line = v + " " + strings.ReplaceAll(patterns[n][row-1], "o", pip) + " " + v } out.WriteString(color.Render(mode, spec, line)) if i == 0 { out.WriteString(" ") } } if row == 2 { out.WriteString(color.Render(mode, color.ColorSpec{Fg: 229, Bg: -1, Bold: true}, fmt.Sprintf(" Total: %d", dice[0]+dice[1]))) } out.WriteByte('\n') } return out.String() } func renderCrapsSnapshot(s *casino.CrapsSnapshot, mode string, unicode bool, styles ...string) string { if s == nil { return "" } style := "dark" if len(styles) > 0 { style = styles[0] } var out strings.Builder title := func(text string) string { return color.Render(mode, color.ColorSpec{Fg: 229, Bg: -1, Bold: true}, text) } subtle := func(text string) string { return color.Render(mode, color.ColorSpec{Fg: 250, Bg: -1}, text) } point := "COME-OUT" if s.Point != 0 { point = fmt.Sprintf("POINT %d", s.Point) } out.WriteString("\n" + title(" BANK CRAPS") + subtle(" / "+point) + "\n") out.WriteString(renderCrapsTrackers(s, mode, unicode)) out.WriteByte('\n') out.WriteString(renderCrapsFelt(s, mode, unicode)) out.WriteByte('\n') out.WriteString(renderCrapsDice(s.Dice, mode, unicode, style)) if len(s.Queue) > 0 { out.WriteString(title(" Shooter ") + subtle(s.Queue[0]) + "\n") if len(s.Queue) > 1 { out.WriteString(subtle(" On deck "+strings.Join(s.Queue[1:], " > ")) + "\n") } } hint := "Place a bet, then roll when ready." if s.Phase != "waiting" && s.Phase != "" { hint = "Your pace. Roll when ready." if s.ShootIn > 0 { hint = fmt.Sprintf("Bets open. Shooter ready in %ds.", s.ShootIn) } else if s.DealerIn > 0 { hint = fmt.Sprintf("Shooter ready. Dealer rolls in %ds; bets still open.", s.DealerIn) } } if s.AutoRoll { hint = fmt.Sprintf("AUTOROLL / Next roll in %ds / stop for manual play", s.AutoIn) } out.WriteString(subtle(" "+hint) + "\n") return out.String() }