1package wordsearch23import (4 "fmt"5 "strings"67 "charm.land/lipgloss/v2"8 "github.com/FelineStateMachine/puzzletea/game"9 "github.com/FelineStateMachine/puzzletea/theme"10)1112func normalStyle() lipgloss.Style {13 p := theme.Current()14 return lipgloss.NewStyle().15 Foreground(p.FG).16 Background(p.BG)17}1819func foundStyle() lipgloss.Style {20 p := theme.Current()21 return lipgloss.NewStyle().22 Foreground(p.SolvedFG).23 Background(p.SuccessBG)24}2526func wordListHeaderStyle() lipgloss.Style {27 return lipgloss.NewStyle().28 Foreground(theme.Current().Accent).29 Bold(true).30 Underline(true)31}3233func foundWordStyle() lipgloss.Style {34 return lipgloss.NewStyle().35 Foreground(theme.Current().SuccessBorder).36 Strikethrough(true)37}3839func unfoundWordStyle() lipgloss.Style {40 return lipgloss.NewStyle().41 Foreground(theme.Current().Info)42}4344func previewIdleStyle() lipgloss.Style {45 return game.StatusBarStyle().Foreground(theme.Current().TextDim)46}4748func previewInfoStyle() lipgloss.Style {49 return game.StatusBarStyle().Foreground(theme.Current().Info)50}5152func previewSuccessStyle() lipgloss.Style {53 return game.StatusBarStyle().Foreground(theme.Current().SuccessBorder)54}5556func gridBorderStyle() lipgloss.Style {57 p := theme.Current()58 return lipgloss.NewStyle().59 Border(lipgloss.RoundedBorder()).60 BorderForeground(p.Border).61 BorderBackground(p.BG)62}6364func gridBorderSolvedStyle() lipgloss.Style {65 p := theme.Current()66 return lipgloss.NewStyle().67 Border(lipgloss.RoundedBorder()).68 BorderForeground(p.SuccessBorder).69 BorderBackground(p.BG)70}7172func wordListBorderStyle() lipgloss.Style {73 return lipgloss.NewStyle().74 Border(lipgloss.RoundedBorder()).75 BorderForeground(theme.Current().Border).76 Padding(0, 1)77}7879func wordListBorderSolvedStyle() lipgloss.Style {80 return lipgloss.NewStyle().81 Border(lipgloss.RoundedBorder()).82 BorderForeground(theme.Current().SuccessBorder).83 Padding(0, 1)84}8586// selectionStyle returns the style for cells in the active drag selection,87// using the current theme's SelectionBG color for the background.88func selectionStyle() lipgloss.Style {89 bg := theme.Current().SelectionBG90 return lipgloss.NewStyle().91 Foreground(theme.TextOnBG(bg)).92 Background(bg)93}9495func renderView(m Model) string {96 title := game.TitleBarView("Word Search", m.modeTitle, m.solved)97 gridContent := renderGrid(m)98 wordListContent := renderWordList(m)99100 gBorder := gridBorderStyle()101 wBorder := wordListBorderStyle()102 if m.solved {103 gBorder = gridBorderSolvedStyle()104 wBorder = wordListBorderSolvedStyle()105 }106107 gridView := gBorder.Render(gridContent)108 gridHeight := lipgloss.Height(gridView)109 // Match word list height to grid height, subtracting border lines (top+bottom).110 wBorder = wBorder.Height(gridHeight - 2)111 wordListView := wBorder.Render(wordListContent)112113 spacer := strings.Repeat(" ", 2)114 mainView := lipgloss.JoinHorizontal(lipgloss.Top, gridView, spacer, wordListView)115 if m.solved {116 return game.ComposeGameView(title, mainView)117 }118119 preview := selectionPreviewView(m)120 status := statusBarView(m.showFullHelp)121122 return game.ComposeGameViewRows(123 title,124 mainView,125 game.StableRow(preview, selectionPreviewView(Model{}), selectionPreviewSolvedView()),126 game.StableRow(status, statusBarView(false), statusBarView(true)),127 )128}129130func statusBarView(showFullHelp bool) string {131 if showFullHelp {132 return game.StatusBarStyle().Render("arrows/wasd: move enter/space: select bkspc: cancel mouse: click & drag esc: menu ctrl+r: reset ctrl+h: help")133 }134 return game.StatusBarStyle().Render("enter/space: select bkspc: cancel mouse: click & drag")135}136137func selectionPreviewView(m Model) string {138 preview := m.currentSelectionPreview()139140 switch {141 case m.selection != startSelected:142 return previewIdleStyle().Render("selection: choose a start cell")143 case !preview.Valid:144 return previewIdleStyle().Render("selection: start locked, move to choose an endpoint")145 default:146 parts := []string{147 fmt.Sprintf("trace: %s", preview.Letters),148 fmt.Sprintf("dir: %s", preview.Direction),149 fmt.Sprintf("len: %d", preview.Length),150 }151152 switch {153 case preview.ExactWord != "":154 parts = append(parts, fmt.Sprintf("match: %s", preview.ExactWord))155 return previewSuccessStyle().Render(strings.Join(parts, " "))156 case preview.NearWord != "":157 parts = append(parts, fmt.Sprintf("near: %s", preview.NearWord))158 return previewInfoStyle().Render(strings.Join(parts, " "))159 default:160 parts = append(parts, "line: ready")161 return previewIdleStyle().Render(strings.Join(parts, " "))162 }163 }164}165166func selectionPreviewSolvedView() string {167 return previewSuccessStyle().Render("trace: PUZZLETEA dir: down-right len: 10 match: PUZZLETEA")168}169170func renderGrid(m Model) string {171 var rows []string172173 for y := 0; y < m.height; y++ {174 var cells []string175 for x := 0; x < m.width; x++ {176 letter := m.grid.Get(x, y)177 style := getCellStyle(m, x, y)178 display := fmt.Sprintf(" %c ", letter)179 if m.cursor.X == x && m.cursor.Y == y {180 display = game.CursorLeft + fmt.Sprintf("%c", letter) + game.CursorRight181 }182 cells = append(cells, style.Render(display))183 }184 rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, cells...))185 }186187 return lipgloss.JoinVertical(lipgloss.Left, rows...)188}189190func getCellStyle(m Model, x, y int) lipgloss.Style {191 // Priority order: cursor > selection > found word > normal192193 if m.cursor.X == x && m.cursor.Y == y {194 return game.CursorStyle()195 }196197 if m.selection == startSelected && isInSelection(m, x, y) {198 return selectionStyle()199 }200201 if m.foundCells[y][x] {202 return foundStyle()203 }204205 return normalStyle()206}207208func isInSelection(m Model, x, y int) bool {209 found := false210 walkLine(m.selectionStart, m.cursor, func(cx, cy int) {211 if cx == x && cy == y {212 found = true213 }214 })215 return found216}217218func renderWordList(m Model) string {219 var sb strings.Builder220221 sb.WriteString(wordListHeaderStyle().Render("Words to Find:"))222 sb.WriteString("\n\n")223224 for _, word := range m.words {225 if word.Found {226 sb.WriteString(foundWordStyle().Render(fmt.Sprintf("\u2713 %s", word.Text)))227 } else {228 sb.WriteString(unfoundWordStyle().Render(fmt.Sprintf("\u25cb %s", word.Text)))229 }230 sb.WriteString("\n")231 }232233 sb.WriteString("\n")234 fmt.Fprintf(&sb, "Found: %d/%d", m.countFoundWords(), len(m.words))235236 return sb.String()237}