clone

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

feat/app-decomposition
puzzletea / games / rippleeffect / style.go
style.go 7.2K
1package rippleeffect23import (4 "image/color"5 "maps"6 "strconv"78 "charm.land/lipgloss/v2"9 "github.com/FelineStateMachine/puzzletea/game"10 "github.com/FelineStateMachine/puzzletea/theme"11)1213const cellWidth = game.DynamicGridCellWidth1415type visualKind int1617const (18 visualNormal visualKind = iota19 visualCage20 visualCompleted21 visualConflictCursor22 visualCursor23 visualSolved24 visualConflict25)2627func chooseVisualKind(cursor, solved, conflict, completed, cageHighlight bool) visualKind {28 switch {29 case cursor && conflict:30 return visualConflictCursor31 case conflict:32 return visualConflict33 case solved:34 return visualSolved35 case cursor:36 return visualCursor37 case completed:38 return visualCompleted39 case cageHighlight:40 return visualCage41 default:42 return visualNormal43 }44}4546func gridView(m Model) string {47 activeCage := m.geo.cageGrid[m.cursor.Y][m.cursor.X]48 completed := completedCageBackgrounds(m)49 bridgeBG := bridgeBackgrounds(m, completed)50 return game.RenderDynamicGrid(game.DynamicGridSpec{51 Width: m.width,52 Height: m.height,53 Solved: m.solved,54 Cell: func(x, y int) string {55 return cellView(m, x, y, activeCage, completed[m.geo.cageGrid[y][x]])56 },57 ZoneAt: func(x, y int) int {58 return m.geo.cageGrid[y][x]59 },60 ZoneFill: func(zone int) color.Color {61 return bridgeBG[zone]62 },63 BridgeFill: func(bridge game.DynamicGridBridge) color.Color {64 return bridgeFill(m, bridgeBG, bridge)65 },66 })67}6869func cellView(m Model, x, y, activeCage int, completedBG color.Color) string {70 p := theme.Current()71 kind := chooseVisualKind(72 x == m.cursor.X && y == m.cursor.Y,73 m.solved,74 m.conflicts[y][x],75 completedBG != nil,76 m.geo.cageGrid[y][x] == activeCage,77 )7879 style := lipgloss.NewStyle().80 Width(cellWidth).81 AlignHorizontal(lipgloss.Center)8283 fg := p.FG84 bg := p.BG85 text := " · "86 if value := m.grid[y][x]; value != 0 {87 text = " " + strconv.Itoa(value) + " "88 }8990 switch kind {91 case visualConflictCursor:92 fg = game.CursorFG()93 bg = game.ConflictBG()94 text = cursorText(m.grid[y][x])95 case visualConflict:96 fg = game.ConflictFG()97 bg = game.ConflictBG()98 case visualSolved:99 fg = p.SolvedFG100 bg = p.SuccessBG101 case visualCursor:102 fg = p.AccentText103 bg = p.AccentBG104 text = cursorText(m.grid[y][x])105 case visualCompleted:106 bg = completedBG107 fg = theme.TextOnBG(bg)108 case visualCage:109 bg = p.SelectionBG110 fg = theme.TextOnBG(bg)111 }112113 if m.givens[y][x] != 0 {114 style = style.Bold(true)115 switch kind {116 case visualNormal, visualCage, visualCompleted:117 bg = theme.GivenTint(bg)118 }119 }120121 return style.Foreground(fg).Background(bg).Render(text)122}123124func cursorText(value int) string {125 if value == 0 {126 return game.CursorLeft + "·" + game.CursorRight127 }128 return game.CursorLeft + strconv.Itoa(value) + game.CursorRight129}130131func completedCageBackgrounds(m Model) map[int]color.Color {132 palette := theme.Current()133 colors := palette.ThemeColors()134 if len(colors) == 0 {135 return nil136 }137138 backgrounds := make(map[int]color.Color)139 for cageIdx, cells := range m.geo.cageCells {140 if !cageCompleted(m, cageIdx, cells) {141 continue142 }143 backgrounds[cageIdx] = completedCageColor(m.geo.cages[cageIdx], colors, palette.Surface)144 }145 return backgrounds146}147148func solvedBridgeBackgrounds(m Model) map[int]color.Color {149 p := theme.Current()150 backgrounds := make(map[int]color.Color, len(m.geo.cages))151 for cageIdx := range m.geo.cages {152 backgrounds[cageIdx] = p.SuccessBG153 }154 return backgrounds155}156157func conflictBridgeBackgrounds(m Model) map[int]color.Color {158 backgrounds := make(map[int]color.Color)159 for cageIdx, cells := range m.geo.cageCells {160 for _, cell := range cells {161 if !m.conflicts[cell.y][cell.x] {162 continue163 }164 backgrounds[cageIdx] = game.ConflictBG()165 break166 }167 }168 return backgrounds169}170171func bridgeBackgrounds(m Model, completed map[int]color.Color) map[int]color.Color {172 if m.solved {173 return solvedBridgeBackgrounds(m)174 }175176 backgrounds := activeCageBridgeBackgrounds(m)177 maps.Copy(backgrounds, completed)178 maps.Copy(backgrounds, conflictBridgeBackgrounds(m))179 return backgrounds180}181182func bridgeFill(m Model, bridgeBG map[int]color.Color, bridge game.DynamicGridBridge) color.Color {183 if m.solved {184 return nil185 }186 if bridge.Uniform && bridgeBG[bridge.Zone] != nil {187 return nil188 }189 if bridge.Count > 0 && !bridgeTouchesBorder(m.geo, bridge) {190 return nil191 }192 return nil193}194195func bridgeTouchesBorder(geo *geometry, bridge game.DynamicGridBridge) bool {196 if geo == nil {197 return false198 }199200 switch bridge.Kind {201 case game.DynamicGridBridgeVertical:202 return cageJunctionRune(geo.cageGrid, geo.width, geo.height, bridge.X, bridge.Y) != ' ' ||203 cageJunctionRune(geo.cageGrid, geo.width, geo.height, bridge.X, bridge.Y+1) != ' '204 case game.DynamicGridBridgeHorizontal:205 return cageJunctionRune(geo.cageGrid, geo.width, geo.height, bridge.X, bridge.Y) != ' ' ||206 cageJunctionRune(geo.cageGrid, geo.width, geo.height, bridge.X+1, bridge.Y) != ' '207 default:208 return false209 }210}211212func cageHorizontalEdge(cageGrid [][]int, height, x, y int) bool {213 switch {214 case y <= 0, y >= height:215 return true216 default:217 return cageGrid[y-1][x] != cageGrid[y][x]218 }219}220221func cageVerticalEdge(cageGrid [][]int, width, x, y int) bool {222 switch {223 case x <= 0, x >= width:224 return true225 default:226 return cageGrid[y][x-1] != cageGrid[y][x]227 }228}229230func cageJunctionRune(cageGrid [][]int, width, height, x, y int) rune {231 north := y > 0 && cageVerticalEdge(cageGrid, width, x, y-1)232 south := y < height && cageVerticalEdge(cageGrid, width, x, y)233 west := x > 0 && cageHorizontalEdge(cageGrid, height, x-1, y)234 east := x < width && cageHorizontalEdge(cageGrid, height, x, y)235236 switch {237 case north && south && west && east:238 return '┼'239 case north && south && west:240 return '┤'241 case north && south && east:242 return '├'243 case west && east && north:244 return '┴'245 case west && east && south:246 return '┬'247 case south && east:248 return '┌'249 case south && west:250 return '┐'251 case north && east:252 return '└'253 case north && west:254 return '┘'255 case north || south:256 return '│'257 case west || east:258 return '─'259 default:260 return ' '261 }262}263264func activeCageBridgeBackgrounds(m Model) map[int]color.Color {265 backgrounds := make(map[int]color.Color, 1)266 if m.geo == nil || m.width == 0 || m.height == 0 {267 return backgrounds268 }269270 activeCage := m.geo.cageGrid[m.cursor.Y][m.cursor.X]271 backgrounds[activeCage] = theme.Current().SelectionBG272 return backgrounds273}274275func cageCompleted(m Model, cageIdx int, cells []point) bool {276 size := m.geo.cageSizes[cageIdx]277 seen := make(map[int]struct{}, size)278 for _, cell := range cells {279 if m.conflicts[cell.y][cell.x] {280 return false281 }282 value := m.grid[cell.y][cell.x]283 if value < 1 || value > size {284 return false285 }286 if _, exists := seen[value]; exists {287 return false288 }289 seen[value] = struct{}{}290 }291 return len(seen) == size292}293294func completedCageColor(cage Cage, colors []color.Color, base color.Color) color.Color {295 if len(colors) == 0 {296 return nil297 }298 first := cage.Cells[0]299 index := (first.Y*37 + first.X*17 + cage.Size*13 + cage.ID*7) % len(colors)300 return theme.Blend(base, colors[index], 0.52)301}302303func statusBarView(showFullHelp bool) string {304 if showFullHelp {305 return game.StatusBarStyle().Render("1-9: place bkspc: clear arrows/wasd: move esc: menu ctrl+r: reset ctrl+h: help")306 }307 return game.StatusBarStyle().Render("1-9: place bkspc: clear")308}