clone

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

bug/architecture-test-trim-path
puzzletea / games / sudokurgb / style.go
style.go 6.8K
1package sudokurgb23import (4 "image/color"5 "strconv"6 "strings"78 "charm.land/lipgloss/v2"9 "github.com/FelineStateMachine/puzzletea/game"10 "github.com/FelineStateMachine/puzzletea/theme"11)1213const (14 cellWidth = game.DynamicGridCellWidth15 rowHintWidth = valueCount*2 - 116 colHintHeight = valueCount17)1819func emptyCellStyle() lipgloss.Style {20 p := theme.Current()21 return lipgloss.NewStyle().22 Foreground(p.TextDim).23 Background(game.DefaultBorderColors().BackgroundBG)24}2526func providedCellStyle(value int, bg color.Color) lipgloss.Style {27 return lipgloss.NewStyle().28 Bold(true).29 Foreground(symbolColor(value)).30 Background(theme.GivenTint(bg))31}3233func userCellStyle(value int) lipgloss.Style {34 return lipgloss.NewStyle().35 Foreground(symbolColor(value)).36 Background(game.DefaultBorderColors().BackgroundBG)37}3839func boxConflictCellStyle() lipgloss.Style {40 return lipgloss.NewStyle().41 Foreground(theme.TextOnBG(game.ConflictBG())).42 Background(game.ConflictBG())43}4445func valueCursorStyle(value int) lipgloss.Style {46 if value == 0 {47 return game.CursorStyle()48 }4950 bg := symbolColor(value)51 return lipgloss.NewStyle().52 Bold(true).53 Foreground(theme.TextOnBG(bg)).54 Background(bg)55}5657func hintStyle(value int) lipgloss.Style {58 return lipgloss.NewStyle().Foreground(symbolColor(value))59}6061func hintSolvedStyle(value int) lipgloss.Style {62 bg := symbolColor(value)63 return lipgloss.NewStyle().64 Bold(true).65 Foreground(theme.TextOnBG(bg)).66 Background(bg)67}6869func hintErrorStyle() lipgloss.Style {70 return lipgloss.NewStyle().71 Bold(true).72 Foreground(theme.TextOnBG(game.ConflictBG())).73 Background(game.ConflictBG())74}7576func solvedBoardStyle() lipgloss.Style {77 p := theme.Current()78 return lipgloss.NewStyle().79 Foreground(p.SolvedFG).80 Background(p.SuccessBG)81}8283type boardBlockLayout struct {84 Block string85 Grid string86 HintWidth int87 HintHeight int88}8990func buildBoardBlock(m Model, solved bool) boardBlockLayout {91 grid := renderGrid(m, solved)92 rowHints := rowHintView(m.analysis, solved)93 colHints := colHintView(m.analysis, solved)94 spacerStyle := lipgloss.NewStyle()95 if solved {96 spacerStyle = solvedBoardStyle()97 }98 spacer := spacerStyle.Width(lipgloss.Width(rowHints)).Height(colHintHeight).Render("")99 topBand := lipgloss.JoinHorizontal(lipgloss.Top, spacer, colHints)100 block := lipgloss.JoinVertical(101 lipgloss.Left,102 topBand,103 lipgloss.JoinHorizontal(lipgloss.Top, rowHints, grid),104 )105106 return boardBlockLayout{107 Block: block,108 Grid: grid,109 HintWidth: lipgloss.Width(rowHints),110 HintHeight: lipgloss.Height(topBand),111 }112}113114func renderGrid(m Model, solved bool) string {115 return game.RenderDynamicGrid(game.DynamicGridSpec{116 Width: gridSize,117 Height: gridSize,118 Solved: solved,119 Cell: func(x, y int) string {120 return cellView(m, x, y, solved)121 },122 ZoneAt: func(x, y int) int {123 return sudokuBoxIndex(x, y)124 },125 })126}127128func cellView(m Model, x, y int, solved bool) string {129 c := m.grid[y][x]130 style := cellStyle(m, c, x, y, solved)131 text := cellContent(c)132 if x == m.cursor.X && y == m.cursor.Y {133 if c.v == 0 {134 text = game.CursorLeft + "·" + game.CursorRight135 } else {136 text = game.CursorLeft + cellContentValue(c.v) + game.CursorRight137 }138 }139140 return style.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(text)141}142143func cellStyle(m Model, c cell, x, y int, solved bool) lipgloss.Style {144 isCursor := m.cursor.X == x && m.cursor.Y == y145 var base lipgloss.Style146147 switch c.v {148 case 0:149 base = emptyCellStyle()150 default:151 base = userCellStyle(c.v)152 }153154 if m.providedGrid[y][x] {155 base = providedCellStyle(c.v, game.DefaultBorderColors().BackgroundBG)156 }157158 switch {159 case isCursor:160 return valueCursorStyle(c.v)161 case m.analysis.boxConflictCells[y][x]:162 base = base.163 Foreground(theme.TextOnBG(game.ConflictBG())).164 Background(game.ConflictBG())165 case solved:166 base = base.167 Bold(true).168 Foreground(theme.Current().SolvedFG).169 Background(theme.Current().SuccessBG)170 }171172 return base173}174175func sudokuBoxIndex(x, y int) int {176 return (y / 3 * 3) + (x / 3)177}178179func cellContent(c cell) string {180 if c.v == 0 {181 return "·"182 }183 return cellContentValue(c.v)184}185186func cellContentValue(value int) string {187 switch value {188 case 1:189 return "▲"190 case 2:191 return "■"192 case 3:193 return "●"194 default:195 return "·"196 }197}198199func symbolColor(value int) color.Color {200 p := theme.Current()201 switch value {202 case 1:203 return p.Error204 case 2:205 return p.Success206 case 3:207 return p.Secondary208 default:209 return p.TextDim210 }211}212213func rowHintView(analysis boardAnalysis, solved bool) string {214 lines := make([]string, 0, gridSize*2+1)215 blankStyle := lipgloss.NewStyle()216 if solved {217 blankStyle = solvedBoardStyle()218 }219 blank := blankStyle.Width(rowHintWidth).Render("")220 lines = append(lines, blank)221 for row := range gridSize {222 lines = append(lines, renderRowHintCounts(analysis, row, solved))223 lines = append(lines, blank)224 }225 return strings.Join(lines, "\n")226}227228func renderRowHintCounts(analysis boardAnalysis, row int, solved bool) string {229 var b strings.Builder230 spacerStyle := lipgloss.NewStyle()231 if solved {232 spacerStyle = solvedBoardStyle()233 }234235 for value := 1; value <= valueCount; value++ {236 if value > 1 {237 b.WriteString(spacerStyle.Render(" "))238 }239 b.WriteString(renderHintCount(analysis.rowCounts[row][value], value, analysis.rowOverQuota[row][value], solved))240 }241242 rowStyle := lipgloss.NewStyle()243 if solved {244 rowStyle = solvedBoardStyle()245 }246 return rowStyle.Width(rowHintWidth).Render(b.String())247}248249func colHintView(analysis boardAnalysis, solved bool) string {250 lines := make([]string, 0, colHintHeight)251 for value := 1; value <= valueCount; value++ {252 var b strings.Builder253 lineStyle := lipgloss.NewStyle()254 if solved {255 lineStyle = solvedBoardStyle()256 }257 b.WriteString(lineStyle.Render(" "))258 for col := range gridSize {259 cellStyle := lipgloss.NewStyle().260 Width(cellWidth).261 AlignHorizontal(lipgloss.Center)262 if solved {263 cellStyle = cellStyle.Inherit(solvedBoardStyle())264 }265 b.WriteString(266 cellStyle.Render(renderHintCount(analysis.colCounts[col][value], value, analysis.colOverQuota[col][value], solved)),267 )268 if col < gridSize-1 {269 b.WriteString(lineStyle.Render(" "))270 }271 }272 b.WriteString(lineStyle.Render(" "))273 lines = append(lines, b.String())274 }275 return strings.Join(lines, "\n")276}277278func renderHintCount(count, value int, overQuota, solved bool) string {279 if solved {280 return solvedBoardStyle().Render(strconv.Itoa(count))281 }282283 style := hintStyle(value)284 if count == houseQuota {285 style = hintSolvedStyle(value)286 }287 if overQuota {288 style = hintErrorStyle()289 }290 return style.Render(strconv.Itoa(count))291}292293func statusBarView(showFullHelp bool) string {294 if showFullHelp {295 return game.StatusBarStyle().Render("mouse: click focus arrows/wasd: move 1/2/3: ▲■● bkspc: clear esc: menu ctrl+r: reset ctrl+h: help")296 }297 return game.StatusBarStyle().Render("mouse: click focus 1/2/3: ▲■● bkspc: clear")298}