clone

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

chore/regenerate-vhs-gifs
puzzletea / spellpuzzle / style.go
style.go 5.7K
1package spellpuzzle23import (4 "fmt"5 "image/color"6 "strings"78 "charm.land/lipgloss/v2"9 "github.com/FelineStateMachine/puzzletea/game"10 "github.com/FelineStateMachine/puzzletea/theme"11)1213const boardCellWidth = game.DynamicGridCellWidth1415func renderView(m Model) string {16 title := game.TitleBarView("Spell Puzzle", m.modeTitle, m.solved)17 main := renderMainView(m)18 bank := renderBankRow(m)19 if m.solved {20 content := lipgloss.JoinVertical(lipgloss.Center, main, bank)21 return game.ComposeGameView(title, content)22 }23 trace := renderTraceRow(m)24 status := renderStatusRow(m)2526 return game.ComposeGameViewRows(27 title,28 main,29 game.StaticRow(bank),30 game.StaticRow(trace),31 game.StaticRow(status),32 )33}3435func renderMainView(m Model) string {36 boardBorder := lipgloss.NewStyle().37 Border(lipgloss.RoundedBorder()).38 BorderForeground(theme.Current().Border).39 Padding(0, 1)40 sidebarBorder := lipgloss.NewStyle().41 Border(lipgloss.RoundedBorder()).42 BorderForeground(theme.Current().Border).43 Padding(0, 1)4445 boardView := boardBorder.Render(renderBoard(m))46 sidebarView := sidebarBorder.Render(renderSidebar(m))47 return lipgloss.JoinHorizontal(lipgloss.Top, boardView, " ", sidebarView)48}4950func renderBoard(m Model) string {51 if m.board.Width == 0 || m.board.Height == 0 {52 return ""53 }5455 gridColors := game.DefaultBorderColors()56 gridColors.BackgroundBG = nil57 gridColors.SolvedBG = nil5859 return game.RenderDynamicGrid(game.DynamicGridSpec{60 Width: m.board.Width,61 Height: m.board.Height,62 CellWidth: boardCellWidth,63 Solved: m.solved,64 Colors: &gridColors,65 Cell: func(x, y int) string {66 return renderBoardCell(m.board.Cells[y][x])67 },68 ZoneAt: func(x, y int) int {69 cell := m.board.Cells[y][x]70 if !cell.Occupied {71 return 072 }73 return 174 },75 HasVerticalEdge: func(x, y int) bool {76 return m.board.hasVerticalEdge(x, y)77 },78 HasHorizontalEdge: func(x, y int) bool {79 return m.board.hasHorizontalEdge(x, y)80 },81 BridgeFill: func(bridge game.DynamicGridBridge) color.Color {82 fill, solved := m.board.bridgeFillState(bridge)83 if !fill {84 return nil85 }86 if solved {87 return theme.Current().SuccessBG88 }89 return theme.Current().BG90 },91 })92}9394func renderBoardCell(cell boardCell) string {95 if !cell.Occupied {96 return lipgloss.NewStyle().97 Width(boardCellWidth).98 Align(lipgloss.Center).99 Render(" ")100 }101102 style := lipgloss.NewStyle().Width(boardCellWidth).Align(lipgloss.Center)103 display := " ยท "104 switch {105 case cell.Revealed:106 style = style.Foreground(theme.Current().SolvedFG).Background(theme.Current().SuccessBG)107 display = fmt.Sprintf(" %c ", cell.Letter)108 default:109 style = style.Foreground(theme.Current().TextDim).Background(theme.Current().BG)110 }111 return style.Render(display)112}113114func renderSidebar(m Model) string {115 var builder strings.Builder116 header := lipgloss.NewStyle().Foreground(theme.Current().Accent).Bold(true)117 value := lipgloss.NewStyle().Foreground(theme.Current().Info)118 builder.WriteString(header.Render("Allowed Letters"))119 builder.WriteString("\n")120 builder.WriteString(value.Render(strings.Join(strings.Fields(fmt.Sprint(string(m.bank))), "")))121 builder.WriteString("\n\n")122 builder.WriteString(header.Render("Words Left"))123 builder.WriteString("\n")124 builder.WriteString(value.Render(fmt.Sprintf("%d", m.countWordsLeft())))125 builder.WriteString("\n\n")126 builder.WriteString(header.Render("Bonus Words"))127 builder.WriteString("\n")128 builder.WriteString(value.Render(fmt.Sprintf("%d/%d", len(m.bonusWords), m.countBonusWordsTotal())))129 builder.WriteString("\n\n")130 builder.WriteString(header.Render("Recent Bonus"))131 builder.WriteString("\n")132 recent := m.recentBonusWords(5)133 if len(recent) == 0 {134 builder.WriteString(lipgloss.NewStyle().Foreground(theme.Current().TextDim).Render("none yet"))135 } else {136 for _, word := range recent {137 builder.WriteString(value.Render(word))138 builder.WriteString("\n")139 }140 }141 return strings.TrimRight(builder.String(), "\n")142}143144func renderBankRow(m Model) string {145 paletteColors := theme.Current().ThemeColors()146 tiles := make([]string, 0, len(m.bank))147 for i, letter := range m.bank {148 style := lipgloss.NewStyle().Width(3).Align(lipgloss.Center)149 display := fmt.Sprintf(" %c ", letter)150 switch {151 case m.tileUsage(i) > 0:152 style = style.Foreground(theme.Current().SolvedFG).Background(theme.Current().SuccessBG)153 case m.bankCursor == i:154 style = style.Foreground(game.CursorFG())155 display = game.CursorLeft + string(letter) + game.CursorRight156 case m.hoverTile == i:157 style = style.Foreground(theme.Current().FG).Background(theme.Current().HighlightBG)158 default:159 bg := bankTileBackground(paletteColors, m.bankColorSlots, i)160 style = style.Foreground(theme.TextOnBG(bg)).Background(bg)161 }162 tiles = append(tiles, style.Render(display))163 }164 return strings.Join(tiles, " ")165}166167func bankTileBackground(colors []color.Color, slots []int, index int) color.Color {168 if len(colors) == 0 {169 return theme.Current().Surface170 }171 slot := index172 if index >= 0 && index < len(slots) {173 slot = slots[index]174 }175 return colors[slot%len(colors)]176}177178func renderTraceRow(m Model) string {179 trace := m.traceWord()180 if trace == "" {181 trace = "build a word from the letter bank"182 }183 return game.StatusBarStyle().Render("trace: " + trace)184}185186func renderStatusRow(m Model) string {187 if m.feedback.Message == "" {188 if m.showFullHelp {189 return game.StatusBarStyle().Render("type letters from the bank 1: shuffle enter: submit bkspc: delete mouse: drag across letters")190 }191 return game.StatusBarStyle().Render("type: add letters 1: shuffle enter: submit bkspc: delete")192 }193194 style := game.StatusBarStyle()195 if m.feedback.Kind == feedbackError {196 style = style.Foreground(theme.Current().Error)197 } else {198 style = style.Foreground(theme.Current().SuccessBorder)199 }200 return style.Render(m.feedback.Message)201}