Skip to content

Instantly share code, notes, and snippets.

@infinitechris
Last active July 15, 2026 00:10
Show Gist options
  • Select an option

  • Save infinitechris/aa6002262206e495bc8b27b246282611 to your computer and use it in GitHub Desktop.

Select an option

Save infinitechris/aa6002262206e495bc8b27b246282611 to your computer and use it in GitHub Desktop.
NonoSolver
<!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.&#10;3&#10;1 1&#10;5"></textarea>
<label for="cols">Col Clues (one col per line, spaces between numbers):</label>
<textarea id="cols" placeholder="e.g.&#10;1 1&#10;3&#10;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