clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

main
puzzletea / games / sudoku / style.go
style.go 5.7K
1package sudoku23import (4 "image/color"5 "strconv"67 "charm.land/lipgloss/v2"8 "github.com/FelineStateMachine/puzzletea/game"9 "github.com/FelineStateMachine/puzzletea/theme"10)1112const cellWidth = game.DynamicGridCellWidth1314func emptyCellStyle() lipgloss.Style {15 p := theme.Current()16 return lipgloss.NewStyle().17 Foreground(p.TextDim).18 Background(p.BG)19}2021func providedCellStyle(v int, bg color.Color) lipgloss.Style {22 return lipgloss.NewStyle().23 Bold(true).24 Foreground(digitColor(v)).25 Background(theme.GivenTint(bg))26}2728func userCellStyle(v int) lipgloss.Style {29 p := theme.Current()30 return lipgloss.NewStyle().31 Foreground(digitColor(v)).32 Background(p.BG)33}3435func conflictCellStyle() lipgloss.Style {36 p := theme.Current()37 return lipgloss.NewStyle().38 Foreground(p.Error).39 Background(p.ErrorBG)40}4142func sameNumberStyle(v int) lipgloss.Style {43 return lipgloss.NewStyle().44 Bold(true).45 Foreground(digitColor(v))46}4748func digitCursorStyle(value int) lipgloss.Style {49 bg := digitColor(value)50 return lipgloss.NewStyle().51 Bold(true).52 Foreground(theme.TextOnBG(bg)).53 Background(bg)54}5556func renderGrid(m Model, solved bool) string {57 return game.RenderDynamicGrid(game.DynamicGridSpec{58 Width: gridSize,59 Height: gridSize,60 Solved: solved,61 Cell: func(x, y int) string {62 return cellView(m, x, y, solved)63 },64 ZoneAt: func(x, y int) int {65 return sudokuBoxIndex(x, y)66 },67 ZoneFill: func(zone int) color.Color {68 return activeBoxZoneFill(m.cursor, solved, zone)69 },70 BridgeFill: func(bridge game.DynamicGridBridge) color.Color {71 return bridgeFill(m, solved, bridge)72 },73 })74}7576func cellView(m Model, x, y int, solved bool) string {77 c := m.grid[y][x]78 style := cellStyle(m, c, x, y, m.conflicts[y][x], solved)79 text := cellContent(c, m.conflicts[y][x])8081 if x == m.cursor.X && y == m.cursor.Y {82 if c.v == 0 {83 text = game.CursorLeft + "·" + game.CursorRight84 } else {85 text = game.CursorLeft + strconv.Itoa(c.v) + game.CursorRight86 }87 }8889 return style.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(text)90}9192func cellStyle(m Model, c cell, x, y int, conflict, solved bool) lipgloss.Style {93 p := theme.Current()94 isCursor := m.cursor.X == x && m.cursor.Y == y95 cursorVal := m.grid[m.cursor.Y][m.cursor.X].v9697 switch {98 case isCursor && solved:99 if c.v != 0 {100 return digitCursorStyle(c.v)101 }102 return game.CursorSolvedStyle()103 case isCursor:104 if c.v != 0 {105 return digitCursorStyle(c.v)106 }107 return game.CursorStyle()108 case solved:109 return lipgloss.NewStyle().110 Foreground(p.SolvedFG).111 Background(p.SuccessBG)112 case conflict:113 return conflictCellStyle()114 case cursorVal != 0 && c.v == cursorVal:115 return sameNumberStyle(c.v)116 }117118 isProvided := m.providedGrid[y][x]119 if isProvided {120 bg := p.BG121 if inCursorContext(m.cursor, x, y) {122 bg = p.Surface123 }124 return providedCellStyle(c.v, bg)125 }126127 if c.v != 0 {128 style := userCellStyle(c.v)129 if inCursorContext(m.cursor, x, y) {130 style = style.Background(p.Surface)131 }132 return style133 }134135 style := emptyCellStyle()136 if inCursorContext(m.cursor, x, y) {137 style = style.Background(p.Surface)138 }139 return style140}141142func bridgeFill(m Model, solved bool, bridge game.DynamicGridBridge) color.Color {143 if solved {144 return nil145 }146147 if game.DynamicGridBridgeOnCrosshairAxis(m.cursor, bridge) {148 return theme.Current().Surface149 }150 return nil151}152153func activeBoxZoneFill(cursor game.Cursor, solved bool, zone int) color.Color {154 if solved || zone != sudokuBoxIndex(cursor.X, cursor.Y) {155 return nil156 }157 return theme.Current().Surface158}159160func inCursorContext(cursor game.Cursor, x, y int) bool {161 return cursor.X == x ||162 cursor.Y == y ||163 sudokuBoxIndex(cursor.X, cursor.Y) == sudokuBoxIndex(x, y)164}165166func sudokuBoxIndex(x, y int) int {167 return (y / 3 * 3) + (x / 3)168}169170func digitColor(value int) color.Color {171 if value <= 0 {172 return theme.Current().TextDim173 }174175 colors := theme.Current().ThemeColors()176 if len(colors) == 0 {177 return theme.Current().FG178 }179180 return colors[(value-1)%len(colors)]181}182183func cellContent(c cell, conflict bool) string {184 if c.v == 0 {185 return "·"186 }187 if conflict {188 return "!" + strconv.Itoa(c.v) + "!"189 }190 return strconv.Itoa(c.v)191}192193// computeConflicts returns a grid of booleans indicating which cells have conflicts.194// A cell has a conflict if its value appears more than once in its row, column, or 3x3 box.195func computeConflicts(g grid) [gridSize][gridSize]bool {196 var conflicts [gridSize][gridSize]bool197198 for y := range gridSize {199 var seen [10][]int // value -> list of x positions200 for x := range gridSize {201 v := g[y][x].v202 if v != 0 {203 seen[v] = append(seen[v], x)204 }205 }206 for v := 1; v <= 9; v++ {207 if len(seen[v]) > 1 {208 for _, x := range seen[v] {209 conflicts[y][x] = true210 }211 }212 }213 }214215 for x := range gridSize {216 var seen [10][]int217 for y := range gridSize {218 v := g[y][x].v219 if v != 0 {220 seen[v] = append(seen[v], y)221 }222 }223 for v := 1; v <= 9; v++ {224 if len(seen[v]) > 1 {225 for _, y := range seen[v] {226 conflicts[y][x] = true227 }228 }229 }230 }231232 for boxY := range 3 {233 for boxX := range 3 {234 type pos struct{ y, x int }235 var seen [10][]pos236 for dy := range 3 {237 for dx := range 3 {238 y, x := boxY*3+dy, boxX*3+dx239 v := g[y][x].v240 if v != 0 {241 seen[v] = append(seen[v], pos{y, x})242 }243 }244 }245 for v := 1; v <= 9; v++ {246 if len(seen[v]) > 1 {247 for _, p := range seen[v] {248 conflicts[p.y][p.x] = true249 }250 }251 }252 }253 }254255 return conflicts256}257258func statusBarView(showFullHelp bool) string {259 if showFullHelp {260 return game.StatusBarStyle().Render("mouse: click focus arrows/wasd: move 1-9: fill bkspc: clear esc: menu ctrl+r: reset ctrl+h: help")261 }262 return game.StatusBarStyle().Render("mouse: click focus 1-9: fill bkspc: clear")263}