package ui import ( "fmt" "strings" "time" "termdoku/internal/game" "github.com/charmbracelet/lipgloss" ) func boardString(m Model) string { var b strings.Builder var dup [9][9]bool if m.autoCheck { dup = game.DuplicateMap(m.board.Values, m.cursorRow, m.cursorCol) } var conf [9][9]bool if m.autoCheck { conf = game.ConflictMap(m.board.Values, m.board.Given) } cellWidth := lipgloss.Width(m.styles.Cell.Render("0")) buildLine := func(left, mid, right string) string { seg := strings.Repeat("─", cellWidth) var sb strings.Builder sb.WriteString(left) for c := range 9 { sb.WriteString(seg) switch c { case 8: sb.WriteString(right) case 2, 5: sb.WriteString(mid) } } return sb.String() } topBorder := m.styles.RowSep.Render(buildLine("╭", "┬", "╮")) midBorder := m.styles.RowSep.Render(buildLine("├", "┼", "┤")) botBorder := m.styles.RowSep.Render(buildLine("╰", "┴", "╯")) b.WriteString(topBorder) b.WriteString("\n") for r := range 9 { b.WriteString(m.styles.ColSep.Render("│")) for c := range 9 { if c > 0 && c%3 == 0 { b.WriteString(m.styles.ColSep.Render("│")) } cell := m.cellView(r, c, dup[r][c], conf[r][c]) b.WriteString(cell) } b.WriteString(m.styles.ColSep.Render("│")) b.WriteString("\n") if r == 2 || r == 5 { b.WriteString(midBorder) b.WriteString("\n") } } b.WriteString(botBorder) return b.String() } func Render(m Model) string { if m.showHelp { return renderHelpOverlay(m) } if m.paused { return renderPauseScreen(m) } board := boardString(m) status := lipgloss.PlaceHorizontal(46, lipgloss.Center, m.StatusLine()) // Win animation if m.showWinAnim && time.Since(m.winAnimStart) < 2*time.Second { winMsg := renderWinAnimation(m) return board + "\n\n" + winMsg + "\n" + status } return board + "\n\n\n" + status } func (m Model) cellView(r, c int, isDup, isConf bool) string { v := m.board.Values[r][c] str := "·" // Show notes if cell is empty and has notes if v == 0 { if notes, ok := m.notes[[2]int{r, c}]; ok && len(notes) > 0 { // Show first note as indicator str = string('₀' + rune(notes[0])) // subscript numbers } } else { str = string('0' + v) } style := m.styles.Cell if !m.showHelp && !m.paused && !m.completed { inSameRow := r == m.cursorRow inSameCol := c == m.cursorCol inSameBox := (r/3 == m.cursorRow/3) && (c/3 == m.cursorCol/3) if !inSameRow && !inSameCol && !inSameBox { dimColor := "#555555" if m.theme.Name == "light" { dimColor = "#d1d5db" } style = style.Foreground(lipgloss.Color(dimColor)) } else if inSameRow || inSameCol || inSameBox { highlightColor := "#6b7280" if m.theme.Name == "light" { highlightColor = "#9ca3af" } if r != m.cursorRow || c != m.cursorCol { style = style.Foreground(lipgloss.Color(highlightColor)) } } } if m.board.Given[r][c] { style = m.styles.CellFixed } if isDup { style = m.styles.CellDuplicate } if isConf { style = m.styles.CellConflict } if r == m.cursorRow && c == m.cursorCol { style = m.styles.CellSelected } if deadline, ok := m.flashes[[2]int{r, c}]; ok { if time.Now().Before(deadline) { style = style.Bold(true) } } return style.Render(str) } func renderPauseScreen(m Model) string { pauseMsg := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#fbbf24")). Render("⏸ PAUSED") hint := m.styles.Status.Render("Press 'p' to resume") content := pauseMsg + "\n\n" + hint box := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#fbbf24")). Padding(2, 4). Render(content) return lipgloss.Place(80, 24, lipgloss.Center, lipgloss.Center, box) } func renderHelpOverlay(m Model) string { title := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color(m.theme.Palette.Accent)). Render("KEYBOARD SHORTCUTS") helpItems := []struct { keys string desc string }{ {"↑↓←→ / hjkl", "Move cursor"}, {"1-9", "Enter number"}, {"0 / Space", "Clear cell"}, {"n", "Toggle note mode"}, {"u / Ctrl+Z", "Undo"}, {"Ctrl+Y / Ctrl+R", "Redo"}, {"Ctrl+H", "Get hint"}, {"a", "Toggle auto-check"}, {"t", "Toggle timer"}, {"p", "Pause game"}, {"m", "Main menu"}, {"?", "Toggle this help"}, {"q / Esc", "Quit"}, } var helpText strings.Builder for _, item := range helpItems { key := lipgloss.NewStyle().Foreground(lipgloss.Color("#60a5fa")).Bold(true).Render(item.keys) desc := m.styles.Status.Render(item.desc) helpText.WriteString(fmt.Sprintf(" %-25s %s\n", key, desc)) } content := title + "\n\n" + helpText.String() + "\n" + m.styles.Status.Render("Press '?' again to close") box := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color(m.theme.Palette.Accent)). Padding(1, 2). Render(content) return lipgloss.Place(80, 30, lipgloss.Center, lipgloss.Center, box) } func renderWinAnimation(m Model) string { elapsed := time.Since(m.winAnimStart).Milliseconds() confetti := []string{"✨", "🎉", "🎊", "⭐", "💫", "🌟", "✦", "★"} animPhase := int(elapsed / 150) var confettiLine strings.Builder for i := 0; i < 20; i++ { if (i+animPhase)%3 == 0 { symbol := confetti[(i+animPhase)%len(confetti)] confettiLine.WriteString(symbol) } else { confettiLine.WriteString(" ") } } colors := []string{"#10b981", "#3b82f6", "#8b5cf6", "#f59e0b", "#ef4444"} colorIdx := (animPhase / 2) % len(colors) mainMsg := "🏆 PUZZLE COMPLETE! 🏆" styledMsg := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color(colors[colorIdx])). Render(mainMsg) return confettiLine.String() + "\n" + styledMsg + "\n" + confettiLine.String() }