clone

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

chore/regenerate-vhs-gifs
puzzletea / takuzu / style.go
style.go 7.4K
1package takuzu23import (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 cellWidth = game.DynamicGridCellWidth1415var renderRuneMap = map[rune]string{16 zeroCell: " ● ",17 oneCell: " ○ ",18 emptyCell: " · ",19}2021func zeroStyle() lipgloss.Style {22 p := theme.Current()23 return lipgloss.NewStyle().Foreground(p.Accent).Background(p.BG)24}2526func oneStyle() lipgloss.Style {27 p := theme.Current()28 return lipgloss.NewStyle().Foreground(p.Secondary).Background(p.BG)29}3031func emptyStyle() lipgloss.Style {32 p := theme.Current()33 return lipgloss.NewStyle().Foreground(p.TextDim).Background(p.BG)34}3536func renderStyleMap() map[rune]lipgloss.Style {37 return map[rune]lipgloss.Style{38 zeroCell: zeroStyle(),39 oneCell: oneStyle(),40 emptyCell: emptyStyle(),41 }42}4344type countPair struct {45 zeros int46 ones int47}4849type countContext struct {50 row countPair51 col countPair52 target int53}5455func cellView(val rune, isProvided, isCursor, inCursorRow, inCursorCol, solved, inDuplicateRow, inDuplicateCol bool) string {56 p := theme.Current()57 styles := renderStyleMap()58 style, ok := styles[val]59 if !ok {60 style = emptyStyle()61 }6263 text, ok := renderRuneMap[val]64 if !ok {65 text = renderRuneMap[emptyCell]66 }6768 switch {69 case solved:70 style = style.Foreground(p.SolvedFG).Background(p.SuccessBG)71 }7273 if isProvided && val != emptyCell && !solved {74 style = style.Bold(true).Background(theme.GivenTint(p.BG))75 }76 if (inDuplicateRow || inDuplicateCol) && !solved {77 style = style.Foreground(game.ConflictFG()).Background(game.ConflictBG())78 text = conflictText(text)79 }80 if isCursor {81 text = cursorText(text)82 }8384 return style.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(text)85}8687func cursorText(text string) string {88 runes := []rune(text)89 if len(runes) != cellWidth {90 return text91 }9293 switch {94 case runes[0] == ' ' && runes[cellWidth-1] == ' ':95 return game.CursorLeft + string(runes[1]) + game.CursorRight96 case runes[cellWidth-1] == ' ':97 return game.CursorLeft + string(runes[:cellWidth-1])98 case runes[0] == ' ':99 return string(runes[1:]) + game.CursorRight100 default:101 return text102 }103}104105func conflictText(text string) string {106 runes := []rune(text)107 if len(runes) != cellWidth {108 return text109 }110 return "!" + string(runes[1]) + "!"111}112113func lineComplete(row []rune) bool {114 for _, r := range row {115 if r == emptyCell {116 return false117 }118 }119 return true120}121122func colComplete(g grid, size, col int) bool {123 for y := range size {124 if col >= len(g[y]) || g[y][col] == emptyCell {125 return false126 }127 }128 return true129}130131func duplicateRowSet(g grid, size int) map[int]bool {132 dup := map[int]bool{}133 for i := range size {134 if !lineComplete(g[i]) {135 continue136 }137 for j := i + 1; j < size; j++ {138 if lineComplete(g[j]) && rowEqual(g[i], g[j]) {139 dup[i] = true140 dup[j] = true141 }142 }143 }144 return dup145}146147func duplicateColSet(g grid, size int) map[int]bool {148 dup := map[int]bool{}149 for i := range size {150 if !colComplete(g, size, i) {151 continue152 }153 for j := i + 1; j < size; j++ {154 if colComplete(g, size, j) && colEqual(g, size, i, j) {155 dup[i] = true156 dup[j] = true157 }158 }159 }160 return dup161}162163func gridView(m Model) string {164 dupRows := duplicateRowSet(m.grid, m.size)165 dupCols := duplicateColSet(m.grid, m.size)166 return game.RenderDynamicGrid(game.DynamicGridSpec{167 Width: m.size,168 Height: m.size,169 Solved: m.solved,170 Cell: func(x, y int) string {171 return cellView(172 m.grid[y][x],173 m.provided[y][x],174 x == m.cursor.X && y == m.cursor.Y,175 y == m.cursor.Y,176 x == m.cursor.X,177 m.solved,178 dupRows[y],179 dupCols[x],180 )181 },182 ZoneAt: func(_, _ int) int {183 return 0184 },185 BridgeFill: func(bridge game.DynamicGridBridge) color.Color {186 return bridgeFill(m, bridge)187 },188 })189}190191func bridgeFill(_ Model, _ game.DynamicGridBridge) color.Color {192 return nil193}194195func countContextView(m Model) string {196 ctx := buildCountContext(m.grid, m.cursor, m.size)197 width := countValueWidth(ctx.target)198 label := lipgloss.NewStyle().Foreground(theme.Current().Info)199200 var b strings.Builder201 b.WriteString(label.Render("row "))202 b.WriteString(countLabelStyle(zeroCell).Render(string([]rune(renderRuneMap[zeroCell])[1]) + ":"))203 b.WriteString(renderCountValue(ctx.row.zeros, ctx.target, zeroCell, width))204 b.WriteString(label.Render(" "))205 b.WriteString(countLabelStyle(oneCell).Render(string([]rune(renderRuneMap[oneCell])[1]) + ":"))206 b.WriteString(renderCountValue(ctx.row.ones, ctx.target, oneCell, width))207 b.WriteString(label.Render(" col "))208 b.WriteString(countLabelStyle(zeroCell).Render(string([]rune(renderRuneMap[zeroCell])[1]) + ":"))209 b.WriteString(renderCountValue(ctx.col.zeros, ctx.target, zeroCell, width))210 b.WriteString(label.Render(" "))211 b.WriteString(countLabelStyle(oneCell).Render(string([]rune(renderRuneMap[oneCell])[1]) + ":"))212 b.WriteString(renderCountValue(ctx.col.ones, ctx.target, oneCell, width))213 return b.String()214}215216func buildCountContext(g grid, cursor game.Cursor, size int) countContext {217 if size <= 0 || len(g) == 0 {218 return countContext{}219 }220 row := 0221 if cursor.Y >= 0 && cursor.Y < len(g) {222 row = cursor.Y223 }224 col := 0225 if cursor.X >= 0 && len(g[row]) > 0 && cursor.X < len(g[row]) {226 col = cursor.X227 }228 return countContext{229 row: countLine(g[row]),230 col: countColumn(g, col, size),231 target: size / 2,232 }233}234235func countLine(row []rune) countPair {236 var counts countPair237 for _, value := range row {238 switch value {239 case zeroCell:240 counts.zeros++241 case oneCell:242 counts.ones++243 }244 }245 return counts246}247248func countColumn(g grid, col, size int) countPair {249 var counts countPair250 for y := 0; y < size && y < len(g); y++ {251 if col < 0 || col >= len(g[y]) {252 continue253 }254 switch g[y][col] {255 case zeroCell:256 counts.zeros++257 case oneCell:258 counts.ones++259 }260 }261 return counts262}263264func countPairString(counts countPair) string {265 return strconv.Itoa(counts.zeros) + "/" + strconv.Itoa(counts.ones)266}267268func renderCountValue(count, target int, value rune, width int) string {269 text := strconv.Itoa(count) + "/" + strconv.Itoa(target)270 style := countValueStyle(value)271 if count == target {272 style = metGoalCountStyle(value)273 }274 if count > target {275 style = overGoalCountStyle()276 }277 return style.Width(width).AlignHorizontal(lipgloss.Right).Render(text)278}279280func countValueWidth(target int) int {281 return len(strconv.Itoa(target))*2 + 1282}283284func countValueStyle(value rune) lipgloss.Style {285 return lipgloss.NewStyle().Foreground(binaryValueColor(value))286}287288func countLabelStyle(value rune) lipgloss.Style {289 return lipgloss.NewStyle().290 Bold(true).291 Foreground(binaryValueColor(value))292}293294func metGoalCountStyle(value rune) lipgloss.Style {295 bg := binaryValueColor(value)296 return lipgloss.NewStyle().297 Bold(true).298 Foreground(theme.TextOnBG(bg)).299 Background(bg)300}301302func overGoalCountStyle() lipgloss.Style {303 bg := game.ConflictBG()304 return lipgloss.NewStyle().305 Bold(true).306 Foreground(theme.TextOnBG(bg)).307 Background(bg)308}309310func binaryValueColor(value rune) color.Color {311 p := theme.Current()312 switch value {313 case zeroCell:314 return p.Accent315 case oneCell:316 return p.Secondary317 default:318 return p.TextDim319 }320}321322func statusBarView(showFullHelp bool) string {323 if showFullHelp {324 return game.StatusBarStyle().Render("arrows/wasd: move mouse: click/cycle z/0: ● x/1: ○ bkspc: clear esc: menu ctrl+r: reset ctrl+h: help")325 }326 return game.StatusBarStyle().Render("mouse: click/cycle z/0: ● x/1: ○")327}