- commit
- f9c13f19a4d05ed1532986b36d4cd682436f2f7b
- parent
- d5eb67357501649dac704080f12ae36574296755
- author
- Dami <dami.etoile@gmail.com>
| D | plan/ | 415 | ------------------------------ |
| D | plan/ | 142 | ---------- |
@@ -1,415 +0,0 @@
1-# Hitori Puzzle Generation2-3-This document outlines a strategy for generating NxN Hitori puzzles with a guaranteed unique solution.4-5-## Overview6-7-Hitori puzzles start with a **complete grid of numbers**. The player must shade some cells black so that:8-1. No number appears twice in any row or column among the unshaded (white) cells9-2. No two black cells share an edge (diagonal is allowed)10-3. All white cells remain orthogonally connected11-12-The generation approach:13-1. **Create a Latin Square** (valid solved state with no duplicates in rows/cols)14-2. **Generate a valid cell mask** (which cells are black, obeying Hitori rules)15-3. **Construct the puzzle** (add duplicates for black cells in the solution)16-4. **Verify uniqueness** via backtracking solver17-18----19-20-## Step 1: Create a Latin Square21-22-A Latin Square is an NxN grid where each number (1-N) appears exactly once in each row and column. This forms the "white cell" solution.23-24-### Algorithm25-26-```go27-func generateLatinSquare(size int) grid {28- // 1. Create first row with shuffled 1-N29- firstRow := make([]rune, size)30- nums := make([]rune, size)31- for i := range size {32- nums[i] = rune('1' + i)33- }34- rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] })35- copy(firstRow, nums)36-37- // 2. Generate subsequent rows by cyclic shift38- g := make(grid, size)39- for y := range size {40- g[y] = make([]rune, size)41- for x := range size {42- g[y][x] = firstRow[(x+y)%size]43- }44- }45-46- // 3. Shuffle columns (preserves Latin property)47- cols := make([]int, size)48- for i := range size { cols[i] = i }49- rand.Shuffle(len(cols), func(i, j int) { cols[i], cols[j] = cols[j], cols[i] })50-51- result := make(grid, size)52- for y := range size {53- result[y] = make([]rune, size)54- for x := range size {55- result[y][x] = g[y][cols[x]]56- }57- }58-59- return result60-}61-```62-63-### Why this works64-- Cyclic shifts guarantee each row has all N numbers65-- Column shuffling preserves the Latin property (each column still has all N numbers)66-- The result is a valid Hitori "white cell" solution67-68----69-70-## Step 2: Generate a Valid Cell Mask71-72-We need to decide which cells are black. The mask must satisfy:73-1. **No adjacent black cells**: Black cells cannot share an edge (can touch diagonally)74-2. **White cells connected**: All white cells must form one orthogonal component75-76-### Algorithm77-78-```go79-func generateValidMask(size int, blackRatio float64) [][]bool {80- mask := make([][]bool, size)81- for y := range size {82- mask[y] = make([]bool, size)83- }84-85- targetBlack := int(blackRatio * float64(size*size))86- attempts := 087- const maxAttempts = 100088-89- for countBlack(mask, size) < targetBlack && attempts < maxAttempts {90- attempts++91- x, y := rand.IntN(size), rand.IntN(size)92- 93- if mask[y][x] {94- continue // Already black95- }96-97- // Check no adjacent black98- if hasOrthogonalNeighbor(mask, size, x, y) {99- continue // Would violate adjacency rule100- }101-102- mask[y][x] = true103-104- // Verify white connectivity105- if !whiteCellsConnected(mask, size) {106- mask[y][x] = false // Revert: would isolate white cells107- }108- }109-110- return mask111-}112-```113-114-### Helper: Check Orthogonal Neighbor115-116-```go117-func hasOrthogonalNeighbor(mask [][]bool, size, x, y int) bool {118- if x > 0 && mask[y][x-1] { return true }119- if x < size-1 && mask[y][x+1] { return true }120- if y > 0 && mask[y-1][x] { return true }121- if y < size-1 && mask[y+1][x] { return true }122- return false123-}124-```125-126-### Helper: White Cell Connectivity (BFS/DFS)127-128-```go129-func whiteCellsConnected(mask [][]bool, size int) bool {130- // Find first white cell131- startX, startY := -1, -1132- found := false133- for y := range size {134- for x := range size {135- if !mask[y][x] {136- startX, startY = x, y137- found = true138- break139- }140- }141- if found {142- break143- }144- }145- if startX == -1 {146- return true // No white cells (all black)147- }148-149- // BFS to count reachable white cells150- visited := make([][]bool, size)151- for y := range size {152- visited[y] = make([]bool, size)153- }154-155- queue := []cellPos{{startX, startY}}156- visited[startY][startX] = true157- visitedCount := 1158-159- for len(queue) > 0 {160- curr := queue[len(queue)-1]161- queue = queue[:len(queue)-1]162-163- // Check 4 orthogonal neighbors164- for _, d := range []struct{dx, dy int}{{0,-1},{0,1},{-1,0},{1,0}} {165- nx, ny := curr.x+d.dx, curr.y+d.dy166- if nx >= 0 && nx < size && ny >= 0 && ny < size {167- if !mask[ny][nx] && !visited[ny][nx] {168- visited[ny][nx] = true169- visitedCount++170- queue = append(queue, cellPos{nx, ny})171- }172- }173- }174- }175-176- // Count total white cells177- whiteCount := 0178- for y := range size {179- for x := range size {180- if !mask[y][x] {181- whiteCount++182- }183- }184- }185-186- return visitedCount == whiteCount187-}188-```189-190----191-192-## Step 3: Construct the Puzzle193-194-Now combine the Latin Square and mask to create the puzzle grid:195-- White cells: keep their original number196-- Black cells: copy a number from another white cell in the same row OR column197-198-```go199-func constructPuzzle(baseGrid grid, mask [][]bool) grid {200- size := len(baseGrid)201- puzzle := make(grid, size)202- for y := range size {203- puzzle[y] = make([]rune, size)204- for x := range size {205- if mask[y][x] {206- // Black cell: introduce duplicate from same row or column207- // Choose random white cell in row or column208- whiteCellsInRow := findWhiteCellsInRow(mask, size, y, x)209- whiteCellsInCol := findWhiteCellsInCol(mask, size, y, x)210- 211- allWhite := append(whiteCellsInRow, whiteCellsInCol...)212- if len(allWhite) == 0 {213- panic("no white cells to copy from")214- }215- 216- pick := allWhite[rand.IntN(len(allWhite))]217- puzzle[y][x] = baseGrid[pick.y][pick.x]218- } else {219- puzzle[y][x] = baseGrid[y][x]220- }221- }222- }223- return puzzle224-}225-226-func findWhiteCellsInRow(mask [][]bool, size, y, excludeX int) []cellPos {227- var result []cellPos228- for x := range size {229- if x != excludeX && !mask[y][x] {230- result = append(result, cellPos{x, y})231- }232- }233- return result234-}235-236-func findWhiteCellsInCol(mask [][]bool, size, x, excludeY int) []cellPos {237- var result []cellPos238- for y := range size {239- if y != excludeY && !mask[y][x] {240- result = append(result, cellPos{x, y})241- }242- }243- return result244-}245-```246-247----248-249-## Step 4: Verify Uniqueness via Backtracking Solver250-251-The puzzle may have multiple valid solutions. We must verify exactly one exists.252-253-### Algorithm254-255-```go256-func countSolutions(puzzle grid, size, limit int) int {257- // Convert puzzle to mutable state: cell can be 'white' or 'black'258- // Start with all cells marked as unknown259- 260- return countSolutionsRecursive(state, 0, limit)261-}262-263-func countSolutionsRecursive(state [][]cellState, pos int, limit int) int {264- if pos == size*size {265- if isValidComplete(state) {266- return 1267- }268- return 0269- }270-271- x, y := pos%size, pos/size272- 273- // Skip cells that are already determined (if using progressive solving)274- // For pure backtracking, try both states:275- 276- // Try marking as white277- if canBeWhite(state, size, x, y) {278- state[y][x] = white279- count := countSolutionsRecursive(state, pos+1, limit)280- state[y][x] = unknown281- if count >= limit {282- return count283- }284- }285-286- // Try marking as black287- if canBeBlack(state, size, x, y) {288- state[y][x] = black289- if countSolutionsRecursive(state, pos+1, limit) >= limit {290- return limit291- }292- state[y][x] = unknown293- }294-295- return 0296-}297-```298-299-### Constraint Checking Functions300-301-```go302-func canBeWhite(state [][]cellState, size, x, y int) bool {303- // Get the number from puzzle at this position304- num := puzzle[y][x]305- 306- // Check: if this cell is white, no duplicate in row307- for i := range size {308- if i != x && state[y][i] == white && puzzle[y][i] == num {309- return false310- }311- }312- // Check: if this cell is white, no duplicate in column313- for i := range size {314- if i != y && state[i][x] == white && puzzle[i][x] == num {315- return false316- }317- }318- return true319-}320-321-func canBeBlack(state [][]cellState, size, x, y int) bool {322- // Check: no adjacent black cells323- if x > 0 && state[y][x-1] == black { return false }324- if x < size-1 && state[y][x+1] == black { return false }325- if y > 0 && state[y-1][x] == black { return false }326- if y < size-1 && state[y+1][x] == black { return false }327- 328- // Check: removing this cell doesn't isolate white cells329- // (This is expensive - use connectivity check)330- return whiteCellsConnectedAfter(state, size, x, y)331-}332-```333-334----335-336-## Step 5: The Generation Loop337-338-```go339-func Generate(size int, difficulty float64) (grid, [][]bool, error) {340- blackRatio := 0.2 + (1-difficulty)*0.2 // 0.2 (easy) to 0.4 (hard)341- 342- for attempt := 0; attempt < 100; attempt++ {343- // Phase 1: Generate candidate344- baseGrid := generateLatinSquare(size)345- mask := generateValidMask(size, blackRatio)346- puzzle := constructPuzzle(baseGrid, mask)347- 348- // Phase 2: Verify uniqueness349- solutionCount := countSolutions(puzzle, size, 2)350- 351- if solutionCount == 1 {352- // Convert mask to 'provided' format (true = given, false = player filled)353- provided := make([][]bool, size)354- for y := range size {355- provided[y] = make([]bool, size)356- for x := range size {357- provided[y][x] = true // All numbers are given358- }359- }360- return puzzle, provided, nil361- }362- // else: discard and retry363- }364- 365- return nil, nil, errors.New("failed to generate puzzle after max attempts")366-}367-```368-369----370-371-## Tuning Parameters372-373-| Parameter | Range | Effect |374-|-----------|-------|--------|375-| `blackRatio` | 0.15-0.35 | Higher = more black cells = harder to deduce |376-| `maxAttempts` | 100-1000 | Retry limit for generation loop |377-| `size` | 4-12 | Puzzle dimensions |378-379----380-381-## Common Issues & Solutions382-383-| Issue | Cause | Solution |384-|-------|-------|----------|385-| Generator hangs | Mask creates impossible connectivity | Increase `maxAttempts`, lower `blackRatio` |386-| Few solutions found | Too many black cells | Reduce `blackRatio` |387-| Generation too slow | Connectivity check O(n²) per cell | Use Union-Find instead of BFS |388-389-### Optimization: Union-Find for Connectivity390-391-```go392-type UnionFind struct {393- parent []int394- size []int395-}396-397-func (uf *UnionFind) find(x int) int {398- if uf.parent[x] != x {399- uf.parent[x] = uf.find(uf.parent[x])400- }401- return uf.parent[x]402-}403-404-func (uf *UnionFind) union(x, y int) {405- px, py := uf.find(x), uf.find(y)406- if px == py {407- return408- }409- if uf.size[px] < uf.size[py] {410- px, py = py, px411- }412- uf.parent[py] = px413- uf.size[px] += uf.size[py]414-}415-```@@ -1,142 +0,0 @@
1-# Hitori Rendering Implementation Plan2-3-## Overview4-5-This document outlines the implementation of the Hitori puzzle rendering using Bubble Tea and Lipgloss. The rendering builds on patterns established in the existing Nonogram implementation (`nonogram/style.go`, `nonogram/Model.go`).6-7-## Hitori Puzzle Rules8-9-1. **No duplicate numbers** in any row/column among white (unshaded) cells10-2. **No adjacent black cells** - black cells cannot share an edge (diagonal allowed)11-3. **All white cells orthogonally connected** - single contiguous region12-13-## Implementation Components14-15-### 1. Files to Create16-17-| File | Purpose |18-|------|---------|19-| `hitori/Model.go` | Main game model, implements `game.Gamer` interface |20-| `hitori/style.go` | Lipgloss styles and rendering functions |21-| `hitori/grid.go` | Grid data structure and manipulation |22-| `hitori/keys.go` | Key bindings |23-| `hitori/Gamemode.go` | Menu entry and spawner |24-| `hitori/solver.go` | Validation/solving logic |25-26-### 2. Model Structure27-28-```go29-type Model struct {30- width, height int31- grid grid // current puzzle state (numbers + shading)32- solutionMask [][]bool // true = cell should be white in solution33- cursor game.Cursor34- keys KeyMap35- modeTitle string36- showFullHelp bool37- solved bool38-}39-```40-41-### 3. Grid Cell States42-43-Unlike Nonogram, Hitori has three states per cell:44-- **Number cell**: displayed with number (rune '1'-'9')45-- **Marked (X)**: player marked as black46-- **Cleared**: player marked as white (empty)47-48-```go49-const (50- filledTile = 'X' // marked black51- emptyTile = ' ' // cleared/white52- // Numbers 1-9 stored directly as rune53-)54-```55-56-### 4. Key Bindings (from keys.go)57-58-- Arrow keys / WASD: Move cursor59-- `X` or `Z`: Mark cell as black (shaded)60-- `Space` or `Backspace`: Clear cell (mark as white)61-- `Enter`: Toggle cell state (number ā X ā space ā number)62-- `Ctrl+R`: Reset puzzle63-- `Ctrl+H`: Toggle help64-65-### 5. Rendering with Lipgloss66-67-Following `nonogram/style.go` patterns:68-69-```go70-// Cell styles71-var (72- baseStyle = lipgloss.NewStyle()73- 74- numberStyle = baseStyle.75- Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "250"}).76- Background(lipgloss.AdaptiveColor{Light: "255", Dark: "235"})77- 78- markedStyle = baseStyle.79- Foreground(lipgloss.AdaptiveColor{Light: "88", Dark: "196"}).80- Background(lipgloss.AdaptiveColor{Light: "254", Dark: "235"})81- 82- cursorStyle = baseStyle.83- Bold(true).84- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).85- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})86- 87- solvedStyle = baseStyle.88- Background(lipgloss.AdaptiveColor{Light: "151", Dark: "22"})89-)90-```91-92-### 6. Grid Rendering93-94-The grid renders similarly to Nonogram:95-- Each cell is `cellWidth = 3` characters wide96-- Separator lines every 5 cells (`spacerEvery = 5`)97-- Use `lipgloss.JoinHorizontal` / `lipgloss.JoinVertical` to assemble98-- Crosshair highlight on cursor row/column99-100-```go101-func gridView(g grid, c game.Cursor, solved bool) string {102- // Iterate rows, render each cell with appropriate style103- // Apply cursor highlighting, crosshair for cursor row/col104- // Join with vertical separators105-}106-```107-108-### 7. Validation (Solver)109-110-The `IsSolved()` check must verify:111-1. No duplicate numbers in any row among non-marked cells112-2. No adjacent marked cells113-3. All non-marked cells are orthogonally connected114-115-### 8. Game.Gamer Interface Methods116-117-```go118-func (m Model) Init() tea.Cmd119-func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd)120-func (m Model) View() string121-func (m Model) GetDebugInfo() string122-func (m Model) GetFullHelp() [][]key.Binding123-func (m Model) GetSave() ([]byte, error)124-func (m Model) IsSolved() bool125-func (m Model) Reset() game.Gamer126-func (m Model) SetTitle(string) game.Gamer127-```128-129-### 9. Integration130-131-Register in main menu via `cmd.go`:132-```go133-game.Registry["hitori"] = hitori.ImportModel134-```135-136----137-138-## References139-140-- Bubble Tea: https://github.com/charmbracelet/bubbletea141-- Lipgloss: https://github.com/charmbracelet/lipgloss142-- Nonogram implementation: `nonogram/style.go`, `nonogram/Model.go`