235 lines
5.6 KiB
Go
235 lines
5.6 KiB
Go
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()
|
|
}
|