Last active
July 15, 2026 00:10
-
-
Save infinitechris/aa6002262206e495bc8b27b246282611 to your computer and use it in GitHub Desktop.
NonoSolver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nonogram Companion</title> | |
| <link rel="manifest" href="data:application/manifest+json,{%22name%22:%22Nonogram%20Companion%22,%22short_name%22:%22NonoSolver%22,%22start_url%22:%22.%22,%22display%22:%22standalone%22,%22background_color%22:%22%23121212%22,%22theme_color%22:%22%231f1f1f%22}"> | |
| <style> | |
| :root { | |
| --bg: #121212; | |
| --surface: #1e1e1e; | |
| --primary: #bb86fc; | |
| --text: #e0e0e0; | |
| --grid-line: #333333; | |
| } | |
| body { | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background-color: var(--bg); | |
| color: var(--text); | |
| margin: 0; | |
| padding: 16px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 500px; | |
| background: var(--surface); | |
| padding: 16px; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.3); | |
| } | |
| h1 { | |
| font-size: 1.5rem; | |
| text-align: center; | |
| margin-top: 0; | |
| color: var(--primary); | |
| } | |
| label { | |
| font-weight: bold; | |
| display: block; | |
| margin: 10px 0 5px; | |
| font-size: 0.9rem; | |
| } | |
| textarea { | |
| width: 100%; | |
| height: 100px; | |
| background: #2d2d2d; | |
| border: 1px solid #444; | |
| color: #fff; | |
| border-radius: 6px; | |
| padding: 8px; | |
| box-sizing: border-box; | |
| font-family: monospace; | |
| font-size: 1rem; | |
| resize: none; | |
| } | |
| button { | |
| width: 100%; | |
| padding: 12px; | |
| background: var(--primary); | |
| border: none; | |
| border-radius: 6px; | |
| color: #121212; | |
| font-size: 1rem; | |
| font-weight: bold; | |
| cursor: pointer; | |
| margin-top: 15px; | |
| } | |
| button:active { | |
| opacity: 0.8; | |
| } | |
| .grid-container { | |
| margin-top: 20px; | |
| overflow-x: auto; | |
| display: flex; | |
| justify-content: center; | |
| width: 100%; | |
| } | |
| .grid { | |
| display: grid; | |
| border: 2px solid var(--text); | |
| background: var(--grid-line); | |
| gap: 1px; | |
| } | |
| .cell { | |
| width: 22px; | |
| height: 22px; | |
| background: #1e1e1e; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| font-weight: bold; | |
| user-select: none; | |
| } | |
| .cell.filled { | |
| background: #fff; | |
| } | |
| .cell.empty::before { | |
| content: "×"; | |
| color: #ff5555; | |
| } | |
| #error { | |
| color: #ff5555; | |
| text-align: center; | |
| font-size: 0.9rem; | |
| margin-top: 10px; | |
| white-space: pre-wrap; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>NonoSolver Companion</h1> | |
| <label for="rows">Row Clues (one row per line, spaces between numbers):</label> | |
| <textarea id="rows" placeholder="e.g. 3 1 1 5"></textarea> | |
| <label for="cols">Col Clues (one col per line, spaces between numbers):</label> | |
| <textarea id="cols" placeholder="e.g. 1 1 3 3"></textarea> | |
| <button onclick="runSolver()">Solve Board</button> | |
| <div id="error"></div> | |
| <div class="grid-container"> | |
| <div id="grid" class="grid"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // 5x5 heart as default | |
| document.getElementById('rows').value = "1 1\n5\n5\n3\n1"; | |
| document.getElementById('cols').value = "2\n4\n4\n4\n2"; | |
| function parseInput(text) { | |
| return text.trim().split('\n').map(line => { | |
| const numbers = line.trim().split(/\s+/).map(Number).filter(n => n > 0); | |
| return numbers.length === 0 ? [] : numbers; | |
| }); | |
| } | |
| function runSolver() { | |
| const errorDiv = document.getElementById('error'); | |
| errorDiv.textContent = ""; | |
| const rowClues = parseInput(document.getElementById('rows').value); | |
| const colClues = parseInput(document.getElementById('cols').value); | |
| const height = rowClues.length; | |
| const width = colClues.length; | |
| if (height === 0 || width === 0) { | |
| errorDiv.textContent = "Please enter valid clues."; | |
| return; | |
| } | |
| let grid = Array(height).fill(null).map(() => Array(width).fill(0)); | |
| // Generate all possible valid configurations for a single line | |
| function getConfigs(length, clues) { | |
| let memo = new Map(); | |
| function solve(clueIdx, pos) { | |
| let key = `${clueIdx},${pos}`; | |
| if (memo.has(key)) return memo.get(key); | |
| if (clueIdx === clues.length) { | |
| return [Array(length - pos).fill(-1)]; | |
| } | |
| let targetClue = clues[clueIdx]; | |
| let minSpaceNeeded = clues.slice(clueIdx).reduce((a, b) => a + b, 0) + (clues.length - clueIdx - 1); | |
| let maxStart = length - minSpaceNeeded; | |
| let results = []; | |
| for (let start = pos; start <= maxStart; start++) { | |
| let pattern = Array(start - pos).fill(-1); | |
| pattern.push(...Array(targetClue).fill(1)); | |
| if (clueIdx < clues.length - 1) { | |
| pattern.push(-1); // space gap | |
| let nextConfigs = solve(clueIdx + 1, start + targetClue + 1); | |
| for (let next of nextConfigs) { | |
| results.push([...pattern, ...next]); | |
| } | |
| } else { | |
| let nextConfigs = solve(clueIdx + 1, start + targetClue); | |
| for (let next of nextConfigs) { | |
| results.push([...pattern, ...next]); | |
| } | |
| } | |
| } | |
| memo.set(key, results); | |
| return results; | |
| } | |
| return solve(0, 0); | |
| } | |
| let rowConfigs, colConfigs; | |
| try { | |
| rowConfigs = rowClues.map(clue => getConfigs(width, clue)); | |
| colConfigs = colClues.map(clue => getConfigs(height, clue)); | |
| } catch (e) { | |
| errorDiv.textContent = "Error generating line combinations. Check your clues to ensure they can physically fit on the grid."; | |
| return; | |
| } | |
| let progress = true; | |
| let limit = 200; | |
| while (progress && limit-- > 0) { | |
| progress = false; | |
| // Solve Rows | |
| for (let r = 0; r < height; r++) { | |
| let currentLine = grid[r]; | |
| rowConfigs[r] = rowConfigs[r].filter(config => | |
| config.every((val, c) => currentLine[c] === 0 || currentLine[c] === val) | |
| ); | |
| if (rowConfigs[r].length === 0) { | |
| errorDiv.textContent = `Clue mismatch! Row ${r + 1} has no valid layouts. Double check your entries.`; | |
| return; | |
| } | |
| for (let c = 0; c < width; c++) { | |
| if (grid[r][c] === 0) { | |
| let firstVal = rowConfigs[r][0][c]; | |
| let matchesAll = rowConfigs[r].every(config => config[c] === firstVal); | |
| if (matchesAll) { | |
| grid[r][c] = firstVal; | |
| progress = true; | |
| } | |
| } | |
| } | |
| } | |
| // Solve Columns | |
| for (let c = 0; c < width; c++) { | |
| let currentLine = grid.map(row => row[c]); | |
| colConfigs[c] = colConfigs[c].filter(config => | |
| config.every((val, r) => currentLine[r] === 0 || currentLine[r] === val) | |
| ); | |
| if (colConfigs[c].length === 0) { | |
| errorDiv.textContent = `Clue mismatch! Column ${c + 1} has no valid layouts. Double check your entries.`; | |
| return; | |
| } | |
| for (let r = 0; r < height; r++) { | |
| if (grid[r][c] === 0) { | |
| let firstVal = colConfigs[c][0][r]; // Fixed: correctly target cell inside config | |
| let matchesAll = colConfigs[c].every(config => config[r] === firstVal); | |
| if (matchesAll) { | |
| grid[r][c] = firstVal; | |
| progress = true; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Check if fully solved or if we got stuck | |
| let unsolvedCount = 0; | |
| for (let r = 0; r < height; r++) { | |
| for (let c = 0; c < width; c++) { | |
| if (grid[r][c] === 0) unsolvedCount++; | |
| } | |
| } | |
| if (unsolvedCount > 0) { | |
| errorDiv.textContent = `Partial solve completed. ${unsolvedCount} cells left blank because they require advanced backtracking/guessing.`; | |
| } | |
| renderGrid(grid, width); | |
| } | |
| function renderGrid(grid, width) { | |
| const gridDiv = document.getElementById('grid'); | |
| gridDiv.innerHTML = ""; | |
| gridDiv.style.gridTemplateColumns = `repeat(${width}, 22px)`; | |
| for (let r = 0; r < grid.length; r++) { | |
| for (let c = 0; c < width; c++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'cell'; | |
| if (grid[r][c] === 1) cell.classList.add('filled'); | |
| if (grid[r][c] === -1) cell.classList.add('empty'); | |
| gridDiv.appendChild(cell); | |
| } | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment