clone

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

restylize nonogram and sudoku

Dami committed 7915414
commit
7915414b15131915fe27634e11e95d6a17042fee
parent
4d5a3a5fcfb62a74b0bac8e1ca195048f9bfdbd2
author
Dami <dami@Damis-MacBook-Air.local>
7 files changed, +487 -99
Mnonogram/Model.go107++++++++++++----
Mnonogram/style.go144++++++++++++++++++----
Msudoku/Model.go96++++++++++-----
Msudoku/cell.go15++
Msudoku/keys.go5+-
Asudoku/style.go197++++++++++++++++++++++++++++++
Mwordsearch/generator.go22++--
M nonogram/Model.go +79 -28
@@ -26,6 +26,7 @@ type Model struct {
2626 cursor cursor2727 grid grid2828 keys KeyMap29+ modeName string2930 }3031 3132 func New(mode NonogramMode, hints Hints, save ...string) (game.Gamer, error) {
@@ -45,6 +46,7 @@ func New(mode NonogramMode, hints Hints, save ...string) (game.Gamer, error) {
4546 colHints: c,4647 grid: newGrid(s),4748 keys: DefaultKeyMap,49+ modeName: mode.title,4850 }4951 5052 return m, nil
@@ -88,52 +90,101 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
8890 }8991 9092 func (m Model) View() string {93+ curr := generateTomography(m.grid)94+ solved := curr.rows.equal(m.rowHints) && curr.cols.equal(m.colHints)95+9196 maxWidth, maxHeight := m.rowHints.RequiredLen()*cellWidth, m.colHints.RequiredLen()92- g := gridView(m.grid, m.cursor)93- r := rowHintView(m.rowHints, maxWidth)94- c := colHintView(m.colHints, maxHeight)97+98+ title := nonoTitleBarView(m.modeName, solved)99+ g := gridView(m.grid, m.cursor, solved)100+ r := rowHintView(m.rowHints, maxWidth, curr.rows)101+ c := colHintView(m.colHints, maxHeight, curr.cols)95102 spacer := baseStyle.Width(maxWidth).Height(maxHeight).Render("")103+ status := nonoStatusBarView(m.keys)96104 97105 s1 := lipgloss.JoinHorizontal(lipgloss.Bottom, spacer, c)98106 s2 := lipgloss.JoinHorizontal(lipgloss.Top, r, g)99107 100- s := lipgloss.JoinVertical(lipgloss.Left, s1, s2)108+ grid := lipgloss.JoinVertical(lipgloss.Left, s1, s2)101109 102- return s110+ return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)103111 }104112 105113 func (m Model) GetDebugInfo() string {106114 curr := generateTomography(m.grid)107115 solved := curr.rows.equal(m.rowHints) && curr.cols.equal(m.colHints)116+117+ status := "In Progress"118+ if solved {119+ status = "Solved"120+ }121+108122 s := fmt.Sprintf(109- "# Nonogram - Dev Info\n"+110- "| Property | Value |\n"+111- "| :----- | :---- |\n"+112- "| Solved | %v |\n"+113- "| Cursor | (%d,%d) |\n"+114- "| Dimensions | [%dx%d] |\n"+115- "| Hint Render Width | %dr %dc |\n"+116- "\n"+117- "Row Tomography\n\t%v\n"+118- "Row Hints\n\t\t%v\n\n"+119- "Col Tomography\n\t%v\n"+120- "Col Hints\n\t\t%v\n\n",121- solved,122- m.cursor.x,123- m.cursor.y,124- m.width,125- m.height,126- m.rowHints.RequiredLen()*cellWidth,127- m.colHints.RequiredLen(),128- curr.rows,129- m.rowHints,130- curr.cols,131- m.colHints,123+ "# Nonogram\n\n"+124+ "## Game State\n\n"+125+ "| Property | Value |\n"+126+ "| :--- | :--- |\n"+127+ "| Status | %s |\n"+128+ "| Cursor | (%d, %d) |\n"+129+ "| Grid Size | %d x %d |\n"+130+ "| Hint Widths | row: %d, col: %d |\n",131+ status,132+ m.cursor.x, m.cursor.y,133+ m.width, m.height,134+ m.rowHints.RequiredLen()*cellWidth, m.colHints.RequiredLen(),132135 )133136 137+ s += "\n## Row Tomography\n\n"138+ s += "| Row | Hint | Current | Match |\n"139+ s += "| :--- | :--- | :--- | :--- |\n"140+ for i, hint := range m.rowHints {141+ var currStr string142+ match := false143+ if i < len(curr.rows) {144+ currStr = intSliceStr(curr.rows[i])145+ match = intSliceEqual(hint, curr.rows[i])146+ }147+ matchStr := "No"148+ if match {149+ matchStr = "Yes"150+ }151+ s += fmt.Sprintf("| %d | %s | %s | %s |\n", i, intSliceStr(hint), currStr, matchStr)152+ }153+154+ s += "\n## Column Tomography\n\n"155+ s += "| Col | Hint | Current | Match |\n"156+ s += "| :--- | :--- | :--- | :--- |\n"157+ for i, hint := range m.colHints {158+ var currStr string159+ match := false160+ if i < len(curr.cols) {161+ currStr = intSliceStr(curr.cols[i])162+ match = intSliceEqual(hint, curr.cols[i])163+ }164+ matchStr := "No"165+ if match {166+ matchStr = "Yes"167+ }168+ s += fmt.Sprintf("| %d | %s | %s | %s |\n", i, intSliceStr(hint), currStr, matchStr)169+ }170+134171 return s135172 }136173 174+func intSliceStr(s []int) string {175+ if len(s) == 0 {176+ return "[]"177+ }178+ result := ""179+ for i, v := range s {180+ if i > 0 {181+ result += ", "182+ }183+ result += fmt.Sprintf("%d", v)184+ }185+ return result186+}187+137188 func (m *Model) updateTile(r rune) {138189 m.grid[m.cursor.y][m.cursor.x] = r139190 }
M nonogram/style.go +118 -26
@@ -11,18 +11,43 @@ var (
1111 baseStyle = lipgloss.NewStyle()1212 1313 filledStyle = baseStyle.14- Background(lipgloss.Color("255")).15- Foreground(lipgloss.Color("255"))14+ Foreground(lipgloss.Color("#ffffff")).15+ Background(lipgloss.Color("#4a4a4a"))1616 1717 markedStyle = baseStyle.18- Foreground(lipgloss.Color("245"))18+ Foreground(lipgloss.Color("#ff6b6b")).19+ Background(lipgloss.Color("#2a1a1a"))1920 2021 emptyStyle = baseStyle.21- Foreground(lipgloss.Color("250"))22+ Foreground(lipgloss.Color("#333333")).23+ Background(lipgloss.Color("#1a1a1a"))2224 23- highlightStyle = baseStyle.24- Background(lipgloss.Color("205")).25- Foreground(lipgloss.Color("255"))25+ cursorStyle = baseStyle.26+ Bold(true).27+ Foreground(lipgloss.Color("#ffffff")).28+ Background(lipgloss.Color("#ff00ff"))29+30+ crosshairBG = lipgloss.Color("#252525")31+32+ hintStyle = baseStyle.33+ Foreground(lipgloss.Color("#888888"))34+35+ hintSatisfiedStyle = baseStyle.36+ Foreground(lipgloss.Color("#00ff00"))37+38+ nonoTitleStyle = lipgloss.NewStyle().39+ Bold(true).40+ Foreground(lipgloss.Color("#ffffff")).41+ Background(lipgloss.Color("#7B2FBE")).42+ Padding(0, 1)43+44+ nonoSolvedBadgeStyle = lipgloss.NewStyle().45+ Bold(true).46+ Foreground(lipgloss.Color("#00ff00"))47+48+ nonoStatusBarStyle = lipgloss.NewStyle().49+ Foreground(lipgloss.Color("#888888")).50+ MarginTop(1)2651 2752 cellWidth = 32853
@@ -35,22 +60,36 @@ var (
3560 }3661 3762 renderRuneMap = map[rune]string{38- filledTile: "⬤",39- markedTile: "⊗",40- emptyTile: "◯",63+ filledTile: "███",64+ markedTile: "✕",65+ emptyTile: " ",4166 }4267 )4368 44-func colHintView(c TomographyDefinition, height int) string {69+func colHintView(c TomographyDefinition, height int, current ...TomographyDefinition) string {70+ var hasCurrent bool71+ var curr TomographyDefinition72+ if len(current) > 0 {73+ hasCurrent = true74+ curr = current[0]75+ }76+4577 var renderedCols []string46- for _, hints := range c {78+ for i, hints := range c {4779 var colHints []string4880 for range height - len(hints) {4981 spacer := hintCellStyle.Render(" ")5082 colHints = append(colHints, spacer)5183 }84+85+ satisfied := hasCurrent && i < len(curr) && intSliceEqual(hints, curr[i])86+5287 for _, hint := range hints {53- hintCell := hintCellStyle.88+ style := hintStyle89+ if satisfied {90+ style = hintSatisfiedStyle91+ }92+ hintCell := style.Width(cellWidth).5493 Align(lipgloss.Center).5594 Render(fmt.Sprintf("%d", hint))5695 colHints = append(colHints, hintCell)
@@ -60,15 +99,30 @@ func colHintView(c TomographyDefinition, height int) string {
6099 }61100 return lipgloss.JoinHorizontal(lipgloss.Top, renderedCols...)62101 }63-func rowHintView(r TomographyDefinition, width int) string {102+103+func rowHintView(r TomographyDefinition, width int, current ...TomographyDefinition) string {104+ var hasCurrent bool105+ var curr TomographyDefinition106+ if len(current) > 0 {107+ hasCurrent = true108+ curr = current[0]109+ }110+64111 var renderedRows []string65- for _, hints := range r {112+ for i, hints := range r {113+ satisfied := hasCurrent && i < len(curr) && intSliceEqual(hints, curr[i])114+115+ style := hintStyle116+ if satisfied {117+ style = hintSatisfiedStyle118+ }119+66120 var rowHints []string67121 for _, hint := range hints {68122 hintCell := fmt.Sprintf("%2d", hint)69123 rowHints = append(rowHints, hintCell)70124 }71- renderedRow := baseStyle.Width(width).125+ renderedRow := style.Width(width).72126 Align(lipgloss.Right).73127 Render(strings.Join(rowHints, " "))74128 renderedRows = append(renderedRows, renderedRow)
@@ -77,34 +131,72 @@ func rowHintView(r TomographyDefinition, width int) string {
77131 return s78132 }79133 80-func gridView(g grid, c cursor) string {134+func gridView(g grid, c cursor, solved bool) string {81135 var rows []string82136 for y, row := range g {83- var rowBuider []string137+ var rowBuilder []string84138 for x, cell := range row {85- highlighted := x == c.x && y == c.y86- cell := tileView(cell, highlighted)87- rowBuider = append(rowBuider, cell)88-139+ isCursor := x == c.x && y == c.y140+ inCursorRow := y == c.y141+ inCursorCol := x == c.x142+ cell := tileView(cell, isCursor, inCursorRow, inCursorCol, solved)143+ rowBuilder = append(rowBuilder, cell)89144 }90- row := lipgloss.JoinHorizontal(lipgloss.Top, rowBuider...)145+ row := lipgloss.JoinHorizontal(lipgloss.Top, rowBuilder...)91146 rows = append(rows, row)92147 }93148 grid := lipgloss.JoinVertical(lipgloss.Left, rows...)94149 return grid95150 }96151 97-func tileView(val rune, h bool) string {152+func tileView(val rune, isCursor, inCursorRow, inCursorCol, solved bool) string {98153 s, ok := renderStyleMap[val]99154 if !ok {100155 s = renderStyleMap[emptyTile]101156 }102- if h {103- s = highlightStyle157+158+ if isCursor && !solved {159+ s = cursorStyle160+ } else if !solved && (inCursorRow || inCursorCol) {161+ // Apply crosshair background tint162+ s = s.Background(crosshairBG)163+ }164+165+ if solved {166+ // Brighten filled tiles when solved167+ if val == filledTile {168+ s = s.Background(lipgloss.Color("#2a6a2a"))169+ }104170 }171+105172 r, ok := renderRuneMap[val]106173 if !ok {107174 r = renderRuneMap[emptyTile]108175 }109176 return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)110177 }178+179+func nonoTitleBarView(modeName string, solved bool) string {180+ title := nonoTitleStyle.Render("Nonogram " + modeName)181+ if solved {182+ badge := nonoSolvedBadgeStyle.Render(" SOLVED")183+ return title + badge184+ }185+ return title186+}187+188+func nonoStatusBarView(_ KeyMap) string {189+ return nonoStatusBarStyle.Render("arrows/wasd: move z: fill x: mark bkspc: clear ctrl+n: menu ctrl+e: debug")190+}191+192+func intSliceEqual(a, b []int) bool {193+ if len(a) != len(b) {194+ return false195+ }196+ for i, v := range a {197+ if v != b[i] {198+ return false199+ }200+ }201+ return true202+}
M sudoku/Model.go +64 -32
@@ -13,12 +13,6 @@ import (
1313 1414 const GRIDSIZE = 91515 16-const (17- cellBGColor = lipgloss.Color("249")18- providedFGColor = lipgloss.Color("229")19- cursorBGColor = lipgloss.Color("10")20-)21-2216 type cursor struct {2317 x, y int2418 }
@@ -28,6 +22,7 @@ type Model struct {
2822 grid grid2923 provided []cell3024 keys KeyMap25+ modeName string3126 }3227 3328 // New creates a new sudoku game using the provided cell values.
@@ -37,6 +32,7 @@ func New(mode SudokuMode, provided []cell, save ...string) (game.Gamer, error) {
3732 grid: g,3833 provided: provided,3934 keys: DefaultKeyMap,35+ modeName: mode.title,4036 }4137 return m, nil4238 }
@@ -54,6 +50,8 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
5450 case key.Matches(msg, m.keys.FillValue):5551 val, _ := strconv.Atoi(msg.String())5652 m.updateCell(val)53+ case key.Matches(msg, m.keys.ClearCell):54+ m.updateCell(0)5755 case key.Matches(msg, m.keys.Up):5856 if m.cursor.y > 0 {5957 m.cursor.y--
@@ -80,43 +78,77 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
8078 func (m *Model) updateCell(v int) {8179 c := &m.grid[m.cursor.y][m.cursor.x]8280 if slices.Contains(m.provided, *c) {83-81+ return8482 }8583 c.v = v8684 }8785 8886 // View implements game.Gamer.8987 func (m Model) View() string {90- var rows []string91- for _, row := range m.grid {92- var cells []string93- for _, cell := range row {94-95- cellStyle := lipgloss.NewStyle().Background(cellBGColor)96- if slices.Contains(m.provided, cell) {97- cellStyle = cellStyle.Foreground(providedFGColor)98- }99- if m.cursor.x == cell.x && m.cursor.y == cell.y {100- cellStyle = cellStyle.Background(cursorBGColor)101- }88+ title := titleBarView(m.modeName, m.isSolved())89+ grid := renderGrid(m)90+ status := statusBarView()10291 103- c := cellStyle.Render(fmt.Sprintf("%d", cell.v))104- cells = append(cells, c)105- }106- r := lipgloss.JoinHorizontal(lipgloss.Right, cells...)107- rows = append(rows, r)108- }109- return lipgloss.JoinVertical(lipgloss.Left, rows...)92+ return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)11093 }11194 11295 // GetDebugInfo implements game.Gamer.11396 func (m Model) GetDebugInfo() string {114- return fmt.Sprintf(`115-# Sudoko116-Cursor (%dx%d)117-Provided Cells: '%v'118-`,97+ cursorCell := m.grid[m.cursor.y][m.cursor.x]98+ isProvided := slices.Contains(m.provided, cursorCell)99+ conflict := hasConflict(m, cursorCell, m.cursor.x, m.cursor.y)100+ solved := m.isSolved()101+102+ filledCount := 0103+ conflictCount := 0104+ for y := range GRIDSIZE {105+ for x := range GRIDSIZE {106+ c := m.grid[y][x]107+ if c.v != 0 {108+ filledCount++109+ }110+ if c.v != 0 && hasConflict(m, c, x, y) {111+ conflictCount++112+ }113+ }114+ }115+116+ status := "In Progress"117+ if solved {118+ status = "Solved"119+ }120+121+ s := fmt.Sprintf(122+ "# Sudoku\n\n"+123+ "## Game State\n\n"+124+ "| Property | Value |\n"+125+ "| :--- | :--- |\n"+126+ "| Status | %s |\n"+127+ "| Cursor | (%d, %d) |\n"+128+ "| Cell Value | %s |\n"+129+ "| Is Provided | %v |\n"+130+ "| Has Conflict | %v |\n"+131+ "| Cells Filled | %d / 81 |\n"+132+ "| Conflict Count | %d |\n"+133+ "| Provided Count | %d |\n",134+ status,119135 m.cursor.x, m.cursor.y,120- m.provided,136+ cellContent(cursorCell),137+ isProvided,138+ conflict,139+ filledCount,140+ conflictCount,141+ len(m.provided),121142 )143+144+ if len(m.provided) > 0 {145+ s += "\n## Provided Cells\n\n"146+ s += "| Row | Col | Value |\n"147+ s += "| :--- | :--- | :--- |\n"148+ for _, p := range m.provided {149+ s += fmt.Sprintf("| %d | %d | %d |\n", p.y, p.x, p.v)150+ }151+ }152+153+ return s122154 }
M sudoku/cell.go +15 -0
M sudoku/keys.go +4 -1
A sudoku/style.go +197 -0
@@ -0,0 +1,197 @@
1+package sudoku2+3+import (4+ "fmt"5+ "slices"6+ "strings"7+8+ "github.com/charmbracelet/lipgloss"9+)10+11+var (12+ titleStyle = lipgloss.NewStyle().13+ Bold(true).14+ Foreground(lipgloss.Color("#ffffff")).15+ Background(lipgloss.Color("#2E86AB")).16+ Padding(0, 1)17+18+ solvedBadgeStyle = lipgloss.NewStyle().19+ Bold(true).20+ Foreground(lipgloss.Color("#00ff00"))21+22+ emptyCellStyle = lipgloss.NewStyle().23+ Foreground(lipgloss.Color("#555555")).24+ Background(lipgloss.Color("#1a1a1a"))25+26+ providedCellStyle = lipgloss.NewStyle().27+ Bold(true).28+ Foreground(lipgloss.Color("#FFD700")).29+ Background(lipgloss.Color("#1a1a1a"))30+31+ userCellStyle = lipgloss.NewStyle().32+ Foreground(lipgloss.Color("#ffffff")).33+ Background(lipgloss.Color("#1a1a1a"))34+35+ cursorCellStyle = lipgloss.NewStyle().36+ Bold(true).37+ Foreground(lipgloss.Color("#ffffff")).38+ Background(lipgloss.Color("#ff00ff"))39+40+ conflictCellStyle = lipgloss.NewStyle().41+ Foreground(lipgloss.Color("#ff4444")).42+ Background(lipgloss.Color("#3a1111"))43+44+ crosshairBG = lipgloss.Color("#2a2a2a")45+46+ boxBorderFG = lipgloss.Color("#555555")47+48+ gridBorderStyle = lipgloss.NewStyle().49+ Border(lipgloss.RoundedBorder()).50+ BorderForeground(lipgloss.Color("#555555"))51+52+ gridBorderSolvedStyle = lipgloss.NewStyle().53+ Border(lipgloss.RoundedBorder()).54+ BorderForeground(lipgloss.Color("#00ff00"))55+56+ statusBarStyle = lipgloss.NewStyle().57+ Foreground(lipgloss.Color("#888888")).58+ MarginTop(1)59+60+ sudokuCellWidth = 261+)62+63+func renderGrid(m Model) string {64+ var rows []string65+66+ for y := range GRIDSIZE {67+ var cells []string68+ for x := range GRIDSIZE {69+ c := m.grid[y][x]70+ style := getCellStyle(m, c, x, y)71+ content := cellContent(c)72+ rendered := style.Width(sudokuCellWidth).Align(lipgloss.Center).Render(content)73+ cells = append(cells, rendered)74+75+ // Insert vertical box separator after columns 3 and 676+ if x == 2 || x == 5 {77+ sep := lipgloss.NewStyle().Foreground(boxBorderFG).Render("│")78+ cells = append(cells, sep)79+ }80+ }81+ row := lipgloss.JoinHorizontal(lipgloss.Top, cells...)82+ rows = append(rows, row)83+84+ // Insert horizontal box separator after rows 3 and 685+ if y == 2 || y == 5 {86+ sepLine := strings.Repeat("─", sudokuCellWidth)87+ var sepParts []string88+ for x := range GRIDSIZE {89+ sepParts = append(sepParts, sepLine)90+ if x == 2 || x == 5 {91+ sepParts = append(sepParts, "┼")92+ }93+ }94+ sep := lipgloss.NewStyle().Foreground(boxBorderFG).Render(strings.Join(sepParts, ""))95+ rows = append(rows, sep)96+ }97+ }98+99+ grid := lipgloss.JoinVertical(lipgloss.Left, rows...)100+101+ if m.isSolved() {102+ return gridBorderSolvedStyle.Render(grid)103+ }104+ return gridBorderStyle.Render(grid)105+}106+107+func getCellStyle(m Model, c cell, x, y int) lipgloss.Style {108+ // Priority: cursor > conflict > provided > user > empty109+ if m.cursor.x == x && m.cursor.y == y {110+ return cursorCellStyle111+ }112+113+ if c.v != 0 && hasConflict(m, c, x, y) {114+ return conflictCellStyle115+ }116+117+ isProvided := slices.Contains(m.provided, c)118+ inCursorRow := m.cursor.y == y119+ inCursorCol := m.cursor.x == x120+ inCursorBox := (x/3 == m.cursor.x/3) && (y/3 == m.cursor.y/3)121+ inCrosshair := inCursorRow || inCursorCol || inCursorBox122+123+ if isProvided {124+ s := providedCellStyle125+ if inCrosshair {126+ s = s.Background(crosshairBG)127+ }128+ return s129+ }130+131+ if c.v != 0 {132+ s := userCellStyle133+ if inCrosshair {134+ s = s.Background(crosshairBG)135+ }136+ return s137+ }138+139+ s := emptyCellStyle140+ if inCrosshair {141+ s = s.Background(crosshairBG)142+ }143+ return s144+}145+146+func cellContent(c cell) string {147+ if c.v == 0 {148+ return "·"149+ }150+ return fmt.Sprintf("%d", c.v)151+}152+153+func hasConflict(m Model, c cell, x, y int) bool {154+ if c.v == 0 {155+ return false156+ }157+158+ // Check row159+ for cx := range GRIDSIZE {160+ if cx != x && m.grid[y][cx].v == c.v {161+ return true162+ }163+ }164+165+ // Check column166+ for cy := range GRIDSIZE {167+ if cy != y && m.grid[cy][x].v == c.v {168+ return true169+ }170+ }171+172+ // Check 3x3 box173+ boxStartX := (x / 3) * 3174+ boxStartY := (y / 3) * 3175+ for by := boxStartY; by < boxStartY+3; by++ {176+ for bx := boxStartX; bx < boxStartX+3; bx++ {177+ if (bx != x || by != y) && m.grid[by][bx].v == c.v {178+ return true179+ }180+ }181+ }182+183+ return false184+}185+186+func titleBarView(modeName string, solved bool) string {187+ title := titleStyle.Render("Sudoku " + modeName)188+ if solved {189+ badge := solvedBadgeStyle.Render(" SOLVED")190+ return title + badge191+ }192+ return title193+}194+195+func statusBarView() string {196+ return statusBarStyle.Render("arrows/wasd: move 1-9: fill bkspc: clear ctrl+n: menu ctrl+e: debug")197+}
M wordsearch/generator.go +10 -12
@@ -3,24 +3,22 @@ package wordsearch
33 import (44 "math/rand"55 "strings"6- "time"76 )87 98 // Word list organized by length for difficulty filtering109 var wordsByLength = map[int][]string{11- 3: {"CAT", "DOG", "SUN", "HAT", "BAT", "RUN", "FUN", "HOT", "TOP", "BOX"},12- 4: {"TREE", "BIRD", "FISH", "MOON", "STAR", "WIND", "RAIN", "SNOW", "BLUE", "JUMP", "PLAY", "SWIM", "WALK", "TALK", "BOOK", "DOOR", "FIRE", "GOLD", "HAND", "KING"},13- 5: {"HAPPY", "WATER", "LIGHT", "BROWN", "GREEN", "HOUSE", "MUSIC", "OCEAN", "PLANT", "QUICK", "RIVER", "SMILE", "TIGER", "UNDER", "WATCH", "YOUTH", "ZEBRA", "APPLE", "BEACH", "CLOUD"},14- 6: {"FRIEND", "GARDEN", "JUNGLE", "KITTEN", "LAPTOP", "MONKEY", "ORANGE", "PENCIL", "PUZZLE", "QUEEN", "RABBIT", "SCHOOL", "TRAVEL", "WINTER", "YELLOW", "ANIMAL", "BASKET", "CASTLE", "DRAGON", "FOREST"},15- 7: {"ABILITY", "BALANCE", "CAPTAIN", "DIAMOND", "ELECTRIC", "FREEDOM", "GIRAFFE", "HARMONY", "JOURNEY", "KITCHEN", "LIBRARY", "MONSTER", "MYSTERY", "PICTURE", "QUALITY", "RAINBOW", "SUCCESS", "THUNDER", "VICTORY", "WEATHER"},16- 8: {"ADVENTURE", "BIRTHDAY", "CALENDAR", "DAUGHTER", "ELEPHANT", "FOOTBALL", "GIGANTIC", "HOSPITAL", "INFINITY", "KANGAROO", "LANGUAGE", "MOUNTAIN", "NEIGHBOR", "PARADISE", "QUESTION", "SANDWICH", "TREASURE", "UNIVERSE", "VACATION", "WILDLIFE"},17- 9: {"AMBITIOUS", "BEAUTIFUL", "CHALLENGE", "DANGEROUS", "EDUCATION", "FANTASTIC", "GENERATOR", "HALLOWEEN", "IMPORTANT", "KNOWLEDGE", "LANDSCAPE", "MEMORABLE", "NECESSARY", "CHOCOLATE", "PINEAPPLE", "RASPBERRY", "SUBMARINE", "TELEPHONE", "CROCODILE", "BUTTERFLY"},10+ 3: {"CAT", "DOG", "SUN", "HAT", "BAT", "RUN", "FUN", "HOT", "TOP", "BOX"},11+ 4: {"TREE", "BIRD", "FISH", "MOON", "STAR", "WIND", "RAIN", "SNOW", "BLUE", "JUMP", "PLAY", "SWIM", "WALK", "TALK", "BOOK", "DOOR", "FIRE", "GOLD", "HAND", "KING"},12+ 5: {"HAPPY", "WATER", "LIGHT", "BROWN", "GREEN", "HOUSE", "MUSIC", "OCEAN", "PLANT", "QUICK", "RIVER", "SMILE", "TIGER", "UNDER", "WATCH", "YOUTH", "ZEBRA", "APPLE", "BEACH", "CLOUD"},13+ 6: {"FRIEND", "GARDEN", "JUNGLE", "KITTEN", "LAPTOP", "MONKEY", "ORANGE", "PENCIL", "PUZZLE", "QUEEN", "RABBIT", "SCHOOL", "TRAVEL", "WINTER", "YELLOW", "ANIMAL", "BASKET", "CASTLE", "DRAGON", "FOREST"},14+ 7: {"ABILITY", "BALANCE", "CAPTAIN", "DIAMOND", "ELECTRIC", "FREEDOM", "GIRAFFE", "HARMONY", "JOURNEY", "KITCHEN", "LIBRARY", "MONSTER", "MYSTERY", "PICTURE", "QUALITY", "RAINBOW", "SUCCESS", "THUNDER", "VICTORY", "WEATHER"},15+ 8: {"ADVENTURE", "BIRTHDAY", "CALENDAR", "DAUGHTER", "ELEPHANT", "FOOTBALL", "GIGANTIC", "HOSPITAL", "INFINITY", "KANGAROO", "LANGUAGE", "MOUNTAIN", "NEIGHBOR", "PARADISE", "QUESTION", "SANDWICH", "TREASURE", "UNIVERSE", "VACATION", "WILDLIFE"},16+ 9: {"AMBITIOUS", "BEAUTIFUL", "CHALLENGE", "DANGEROUS", "EDUCATION", "FANTASTIC", "GENERATOR", "HALLOWEEN", "IMPORTANT", "KNOWLEDGE", "LANDSCAPE", "MEMORABLE", "NECESSARY", "CHOCOLATE", "PINEAPPLE", "RASPBERRY", "SUBMARINE", "TELEPHONE", "CROCODILE", "BUTTERFLY"},1817 10: {"ACCOMPLISH", "BACKGROUND", "BASKETBALL", "CELEBRATION", "DELIGHTFUL", "EVERYTHING", "GENERATION", "JAVASCRIPT", "MOTIVATION", "WATERMELON", "STRAWBERRY", "TRAMPOLINE", "SKATEBOARD", "GREENHOUSE", "UNDERSTAND", "California", "GIRLFRIEND", "TOURNAMENT", "PLAYGROUND", "REVOLUTION"},1918 }2019 2120 // GenerateWordSearch creates a new word search grid with the specified parameters2221 func GenerateWordSearch(width, height, wordCount, minLen, maxLen int, allowedDirs []Direction) (grid, []Word) {23- rand.Seed(time.Now().UnixNano())2422 2523 g := createEmptyGrid(height, width)2624 words := selectWords(wordCount, minLen, maxLen)
@@ -68,7 +66,7 @@ func tryPlaceWord(g grid, text string, allowedDirs []Direction, maxAttempts int)
6866 height := len(g)6967 width := len(g[0])7068 71- for attempt := 0; attempt < maxAttempts; attempt++ {69+ for range maxAttempts {7270 // Random starting position7371 x := rand.Intn(width)7472 y := rand.Intn(height)
@@ -87,7 +85,7 @@ func tryPlaceWord(g grid, text string, allowedDirs []Direction, maxAttempts int)
8785 8886 // Check if placement is valid (no conflicts, allow letter overlap)8987 valid := true90- for i := 0; i < len(text); i++ {88+ for i := range len(text) {9189 cx := x + dx*i9290 cy := y + dy*i9391 existing := g.Get(cx, cy)
@@ -103,7 +101,7 @@ func tryPlaceWord(g grid, text string, allowedDirs []Direction, maxAttempts int)
103101 }104102 105103 // Place the word106- for i := 0; i < len(text); i++ {104+ for i := range len(text) {107105 cx := x + dx*i108106 cy := y + dy*i109107 g.Set(cx, cy, rune(text[i]))