https://svonberg.org/wp-content/uploads/2026/07/catduko_7.html
Win Celebration (Confetti)
Sound Effects: Added very soft, satisfying “bloop” sounds when placing an ✕, a gentle “meow” or pop when placing a 🐱, and a rewarding chime on victory. (With a mute toggle, of course).
Grid Entry Animations: When a new puzzle is generated, instead of the grid just appearing, the colored regions softly “bloom” or cascade into view from the center outward.
Floating Action Menu: On mobile, reaching to the top of the screen can be annoying. Moved the primary tools (Undo, Hint, Auto-✕) into a floating bar pinned to the bottom of the screen, right under the player’s thumbs.
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no”>
<title>Catoku Puzzle</title>
<style>
:root {
–bg-color: #f7f9fc;
–text-color: #2c3e50;
–board-bg: #ffffff;
–line-color: #2d3436;
–dashed-color: rgba(0, 0, 0, 0.15);
–error-color: #ff4757;
–btn-bg: #4a69bd;
–panel-bg: #ffffff;
–card-text: #555;
}
[data-theme=”dark”] {
–bg-color: #121212;
–text-color: #ecf0f1;
–board-bg: #1e1e1e;
–line-color: #888888;
–dashed-color: rgba(255, 255, 255, 0.2);
–panel-bg: #1e1e1e;
–card-text: #cccccc;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, “Segoe UI”, Roboto, sans-serif;
background-color: var(–bg-color);
color: var(–text-color);
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
touch-action: manipulation;
transition: background-color 0.3s, color 0.3s;
}
.app-container {
width: 100%;
max-width: 500px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
padding-bottom: 40px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: -5px;
}
h1 {
font-size: 2.2rem;
color: inherit;
letter-spacing: 1px;
}
.theme-btn {
background: none;
border: none;
font-size: 1.8rem;
cursor: pointer;
padding: 5px;
transition: transform 0.2s;
}
.theme-btn:active { transform: scale(0.8); }
.panel {
background: var(–panel-bg);
padding: 12px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
gap: 10px;
transition: background-color 0.3s;
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
button {
background-color: var(–btn-bg);
color: white;
border: none;
padding: 10px 12px;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s, background-color 0.2s, opacity 0.2s;
flex: 1;
text-align: center;
white-space: nowrap;
}
button:active { transform: scale(0.95); }
button:disabled { background-color: #95a5a6; cursor: not-allowed; transform: none; opacity: 0.5; }
button.danger { background-color: #e74c3c; }
button.success { background-color: #2ecc71; }
button.warning { background-color: #f39c12; color: #fff; }
button.info { background-color: #3498db; color: #fff; }
button.off { background-color: #95a5a6; color: #fff; }
button.working { background-color: #8e44ad; animation: pulse 1.5s infinite; pointer-events: none;}
.icon-btn { flex: 0 0 auto; padding: 10px; font-size: 1.1rem; }
input, select {
padding: 10px;
border: 2px solid #ecf0f1;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
flex: 1;
min-width: 0;
color: var(–text-color);
background: var(–bg-color);
font-family: inherit;
}
input:focus, select:focus { border-color: #3498db; }
[data-theme=”dark”] input, [data-theme=”dark”] select { border-color: #333; }
[data-theme=”dark”] input:focus, [data-theme=”dark”] select:focus { border-color: #3498db; }
/* Board Styles */
.board-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
position: relative;
}
#game-board {
display: grid;
width: 100%;
aspect-ratio: 1 / 1;
background-color: var(–board-bg);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
border-radius: 6px;
overflow: hidden;
transition: opacity 0.3s, background-color 0.3s;
}
#game-board.generating { opacity: 0.3; pointer-events: none; }
#loading-overlay {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 1.2rem;
font-weight: bold;
color: #8e44ad;
display: none;
pointer-events: none;
text-align: center;
text-shadow: 0px 0px 10px var(–board-bg);
}
#loading-overlay.active { display: block; animation: pulse 1.5s infinite; }
.cell {
display: flex;
justify-content: center;
align-items: center;
font-size: clamp(1.5rem, 5vw, 2.5rem);
cursor: pointer;
background-size: cover;
transition: background-color 0.2s;
position: relative;
}
.cell span {
pointer-events: none;
animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
line-height: 1;
}
.cell.cross { color: rgba(0, 0, 0, 0.4); font-weight: 300; font-size: clamp(1rem, 4vw, 1.5rem); }
.cell.auto-cross { color: rgba(0, 0, 0, 0.15); animation: none; }
[data-theme=”dark”] .cell.cross { color: rgba(255, 255, 255, 0.6); }
[data-theme=”dark”] .cell.auto-cross { color: rgba(255, 255, 255, 0.25); }
.cell.error::after {
content: ”; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
box-shadow: inset 0 0 15px var(–error-color);
background: rgba(255, 71, 87, 0.2);
animation: shake 0.3s ease-in-out;
pointer-events: none;
}
/* Visual Hint Animations */
.cell.cause-flash::after {
content: ”; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
box-shadow: inset 0 0 25px rgba(241, 196, 15, 0.8);
background: rgba(241, 196, 15, 0.5);
animation: fadeOut 4s ease-out forwards;
pointer-events: none;
}
.cell.target-flash::after {
content: ”; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
box-shadow: inset 0 0 25px rgba(52, 152, 219, 0.9);
background: rgba(52, 152, 219, 0.6);
animation: fadeOut 4s ease-out forwards;
pointer-events: none;
z-index: 10;
}
.cell.mistake-flash::after {
content: ”; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
box-shadow: inset 0 0 25px rgba(231, 76, 60, 0.9);
background: rgba(231, 76, 60, 0.5);
animation: fadeOut 3s ease-out forwards;
pointer-events: none;
}
/* Rules */
.rules-card { background: var(–panel-bg); padding: 15px; border-radius: 12px; font-size: 0.85rem; line-height: 1.4; color: var(–card-text); transition: background-color 0.3s; }
/* Modal */
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6); display: flex; justify-content: center; align-items: center;
opacity: 0; pointer-events: none; transition: opacity 0.3s; z-index: 1000;
}
.modal-overlay.active { opacity: 1; pointer-events: all; }
.modal-content {
background: var(–panel-bg); color: var(–text-color); padding: 30px; border-radius: 16px; text-align: center;
transform: scale(0.8); transition: transform 0.3s; box-shadow: 0 10px 30px rgba(0,0,0,0.5); width: 80%; max-width: 320px;
}
.modal-overlay.active .modal-content { transform: scale(1); }
.modal-content h2 { color: #2ecc71; margin-bottom: 10px; }
.modal-content p { color: var(–card-text); margin-bottom: 20px; }
/* Toast Notification */
#toast {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(100px);
background: #34495e; color: white; padding: 12px 20px; border-radius: 12px;
font-size: 0.95rem; line-height: 1.4; opacity: 0; transition: all 0.3s; z-index: 2000; pointer-events: none;
box-shadow: 0 8px 16px rgba(0,0,0,0.3); text-align: center; width: max-content; max-width: 90vw;
}
#toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
@keyframes popIn { 0% { transform: scale(0); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
@keyframes fadeOut { 0% { opacity: 1; } 100% { opacity: 0; } }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
@media (max-width: 400px) {
button { font-size: 0.85rem; padding: 8px 10px; }
input, select { font-size: 0.85rem; padding: 8px; }
.icon-btn { padding: 8px 10px; }
}
</style>
</head>
<body>
<div class=”app-container”>
<header>
<h1>Catoku 🐱</h1>
<button id=”themeToggle” onclick=”toggleTheme()” class=”theme-btn” title=”Toggle Dark Mode”>🌙</button>
</header>
<div class=”panel”>
<div class=”controls” style=”margin-bottom: 5px;”>
<button id=”autoXBtn” onclick=”toggleAutoX()” class=”off” style=”flex: 1;”>Auto-✕: OFF</button>
<button id=”undoBtn” onclick=”handleUndo()” class=”info icon-btn” title=”Undo Move” disabled>↶ Undo</button>
</div>
<div class=”controls”>
<button onclick=”handleHint()” class=”warning”>💡 Hint</button>
<button onclick=”resetBoard()” class=”danger”>↺ Restart</button>
</div>
</div>
<div class=”board-container”>
<div id=”game-board”></div>
<div id=”loading-overlay”>Generating Clean<br/>Visual Flow…</div>
</div>
<div class=”panel”>
<div class=”controls”>
<select id=”boardSizeSelect”>
<option value=”5″ selected>5×5 (Easy)</option>
<option value=”6″>6×6 (Medium)</option>
<option value=”8″>8×8 (Hard)</option>
</select>
<button id=”genBtn” onclick=”handleGenerate()” class=”success”>✨ New Puzzle</button>
</div>
</div>
<div class=”panel”>
<div class=”controls”>
<input type=”text” id=”pwdInput” placeholder=”Paste share code…” autocomplete=”off” />
<button onclick=”handleLoad()” style=”flex: 0.5;”>Load</button>
<button onclick=”handleShare()” style=”flex: 0.6;” class=”info”>Share</button>
</div>
</div>
<div class=”rules-card”>
<strong>Rules:</strong> Exactly 1 cat per row, column, and colored region. Cats cannot touch, even diagonally. Tap to place 🐱, ✕, or blank.
</div>
</div>
<div class=”modal-overlay” id=”winModal”>
<div class=”modal-content”>
<h2>Purr-fect! 🐾</h2>
<p>You logically conquered this board!</p>
<button class=”success” style=”width: 100%” onclick=”nextLevelFromModal()”>Generate Next</button>
</div>
</div>
<div id=”toast”>Message</div>
<script>
// Color Palettes
const lightRegionColors = [ ‘#FFADAD’, ‘#FFD6A5’, ‘#FDFFB6’, ‘#CAFFBF’, ‘#9BF6FF’, ‘#A0C4FF’, ‘#BDB2FF’, ‘#FFC6FF’, ‘#E4C1F9’, ‘#D3F8E2’ ];
const darkRegionColors = [ ‘#5c2e2e’, ‘#5c4520’, ‘#5c5c29’, ‘#3e5c38’, ‘#2a5c5c’, ‘#2e435c’, ‘#40385c’, ‘#5c3e5c’, ‘#4e385c’, ‘#3c5c4d’ ];
let boardSize = 5;
let currentMap = [];
let cellStates = [];
let moveHistory = [];
let isGenerating = false;
let autoXEnabled = false;
let isDarkMode = false;
const boardEl = document.getElementById(‘game-board’);
const loadingOverlay = document.getElementById(‘loading-overlay’);
const winModal = document.getElementById(‘winModal’);
const toastEl = document.getElementById(‘toast’);
const pwdInput = document.getElementById(‘pwdInput’);
const genBtn = document.getElementById(‘genBtn’);
const autoXBtn = document.getElementById(‘autoXBtn’);
const undoBtn = document.getElementById(‘undoBtn’);
const themeToggle = document.getElementById(‘themeToggle’);
// — Core Initialization —
window.onload = () => {
if (localStorage.getItem(‘theme’) === ‘dark’) toggleTheme();
handleGenerate();
};
// — Hardware & UI Features —
function triggerHaptic(type) {
if (!navigator.vibrate) return;
if (type === ‘tap’) navigator.vibrate(15);
else if (type === ‘error’) navigator.vibrate([30, 50, 30]);
else if (type === ‘win’) navigator.vibrate([50, 50, 50, 50, 100]);
}
function toggleTheme() {
isDarkMode = !isDarkMode;
if (isDarkMode) {
document.documentElement.setAttribute(‘data-theme’, ‘dark’);
themeToggle.textContent = ‘☀️’;
localStorage.setItem(‘theme’, ‘dark’);
} else {
document.documentElement.removeAttribute(‘data-theme’);
themeToggle.textContent = ‘🌙’;
localStorage.setItem(‘theme’, ‘light’);
}
if (currentMap.length > 0) renderBoard();
}
function toggleAutoX() {
autoXEnabled = !autoXEnabled;
triggerHaptic(‘tap’);
if (autoXEnabled) {
autoXBtn.classList.remove(‘off’);
autoXBtn.classList.add(‘info’);
autoXBtn.textContent = “Auto-✕: ON”;
showToast(“Auto-✕ enabled! Faint crosses will auto-fill.”);
} else {
autoXBtn.classList.add(‘off’);
autoXBtn.classList.remove(‘info’);
autoXBtn.textContent = “Auto-✕: OFF”;
showToast(“Auto-✕ disabled.”);
}
renderBoard();
}
// — State Management (Undo) —
function saveState() {
moveHistory.push(JSON.parse(JSON.stringify(cellStates)));
undoBtn.disabled = false;
}
function handleUndo() {
if (moveHistory.length === 0 || isGenerating) return;
triggerHaptic(‘tap’);
cellStates = moveHistory.pop();
undoBtn.disabled = moveHistory.length === 0;
renderBoard();
}
function clearHistory() {
moveHistory = [];
undoBtn.disabled = true;
}
// — Load & Reset —
function loadCustom(mapData) {
currentMap = mapData;
boardSize = currentMap.length;
cellStates = Array.from({ length: boardSize }, () => Array(boardSize).fill(0));
clearHistory();
winModal.classList.remove(‘active’);
pwdInput.value = generatePassword(currentMap);
const select = document.getElementById(‘boardSizeSelect’);
if(select.querySelector(`option[value=”${boardSize}”]`)) select.value = boardSize;
renderBoard();
}
function resetBoard() {
if (isGenerating) return;
triggerHaptic(‘tap’);
cellStates = Array.from({ length: boardSize }, () => Array(boardSize).fill(0));
clearHistory();
renderBoard();
}
function nextLevelFromModal() {
winModal.classList.remove(‘active’);
handleGenerate();
}
// — Auto-X Calculation —
function computeAutoX() {
let axMap = Array.from({length: boardSize}, () => Array(boardSize).fill(false));
for(let r=0; r<boardSize; r++) {
for(let c=0; c<boardSize; c++) {
if (cellStates[r][c] === 1) {
for(let dr=-1; dr<=1; dr++) {
for(let dc=-1; dc<=1; dc++) {
if(dr===0 && dc===0) continue;
let nr=r+dr, nc=c+dc;
if(nr>=0 && nr<boardSize && nc>=0 && nc<boardSize) axMap[nr][nc] = true;
}
}
for(let i=0; i<boardSize; i++) { axMap[r][i] = true; axMap[i][c] = true; }
let reg = currentMap[r][c];
for(let rr=0; rr<boardSize; rr++) {
for(let cc=0; cc<boardSize; cc++) {
if(currentMap[rr][cc] === reg) axMap[rr][cc] = true;
}
}
axMap[r][c] = false; // Don’t cross the cat itself
}
}
}
return axMap;
}
// — Rendering & Interactions —
function renderBoard() {
boardEl.innerHTML = ”;
boardEl.style.gridTemplateColumns = `repeat(${boardSize}, 1fr)`;
boardEl.style.gridTemplateRows = `repeat(${boardSize}, 1fr)`;
let axMap = autoXEnabled ? computeAutoX() : null;
let activePalette = isDarkMode ? darkRegionColors : lightRegionColors;
for (let r = 0; r < boardSize; r++) {
for (let c = 0; c < boardSize; c++) {
const region = currentMap[r][c];
const cell = document.createElement(‘div’);
cell.className = ‘cell’;
cell.id = `cell-${r}-${c}`;
cell.style.backgroundColor = activePalette[region % activePalette.length];
let borderCSS = ”;
const thick = `3px solid var(–line-color)`;
const thin = `1px dashed var(–dashed-color)`;
borderCSS += (r === 0 || currentMap[r-1][c] !== region) ? `border-top: ${thick}; ` : `border-top: ${thin}; `;
borderCSS += (c === 0 || currentMap[r][c-1] !== region) ? `border-left: ${thick}; ` : `border-left: ${thin}; `;
if (r === boardSize – 1) borderCSS += `border-bottom: ${thick}; `;
if (c === boardSize – 1) borderCSS += `border-right: ${thick}; `;
cell.style.cssText += borderCSS;
const state = cellStates[r][c];
if (state === 1) {
cell.innerHTML = ‘<span>🐱</span>’;
} else if (state === 2) {
cell.innerHTML = ‘<span>✕</span>’; cell.classList.add(‘cross’);
} else if (autoXEnabled && axMap[r][c]) {
cell.innerHTML = ‘<span>✕</span>’; cell.classList.add(‘cross’, ‘auto-cross’);
}
cell.addEventListener(‘click’, () => {
if (isGenerating) return;
saveState();
triggerHaptic(‘tap’);
cellStates[r][c] = (cellStates[r][c] + 1) % 3;
renderBoard();
});
boardEl.appendChild(cell);
}
}
if(!isGenerating && currentMap.length > 0) validateBoard();
}
function highlightCell(r, c, animClass) {
const cell = document.getElementById(`cell-${r}-${c}`);
if(cell) {
cell.classList.remove(’cause-flash’, ‘target-flash’, ‘mistake-flash’);
void cell.offsetWidth;
cell.classList.add(animClass);
}
}
function validateBoard() {
let errors = Array.from({ length: boardSize }, () => Array(boardSize).fill(false));
let catCount = 0;
for (let r = 0; r < boardSize; r++) {
for (let c = 0; c < boardSize; c++) {
if (cellStates[r][c] === 1) {
catCount++;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
let nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < boardSize && nc >= 0 && nc < boardSize && cellStates[nr][nc] === 1) {
errors[r][c] = true; errors[nr][nc] = true;
}
}
}
}
}
}
for (let i = 0; i < boardSize; i++) {
let rCats = [], cCats = [];
for (let j = 0; j < boardSize; j++) {
if (cellStates[i][j] === 1) rCats.push({r: i, c: j});
if (cellStates[j][i] === 1) cCats.push({r: j, c: i});
}
if (rCats.length > 1) rCats.forEach(p => errors[p.r][p.c] = true);
if (cCats.length > 1) cCats.forEach(p => errors[p.r][p.c] = true);
}
let regionMap = {};
for (let r = 0; r < boardSize; r++) {
for (let c = 0; c < boardSize; c++) {
if (cellStates[r][c] === 1) {
let reg = currentMap[r][c];
if (!regionMap[reg]) regionMap[reg] = [];
regionMap[reg].push({r, c});
}
}
}
Object.values(regionMap).forEach(cats => {
if (cats.length > 1) cats.forEach(p => errors[p.r][p.c] = true);
});
let anyErrors = false;
for (let r = 0; r < boardSize; r++) {
for (let c = 0; c < boardSize; c++) {
if (errors[r][c]) {
anyErrors = true;
document.getElementById(`cell-${r}-${c}`).classList.add(‘error’);
}
}
}
if (catCount === boardSize && !anyErrors) {
triggerHaptic(‘win’);
setTimeout(() => winModal.classList.add(‘active’), 250);
}
}
// — Ground Truth Solver —
function getTrueStateFast(map, size) {
let board = Array(size).fill(-1);
let solvedState = null;
let regUsed = Array(size).fill(false);
function solve(row) {
if (row === size) {
solvedState = Array.from({length: size}, () => Array(size).fill(2));
for(let r=0; r<size; r++) solvedState[r][board[r]] = 1;
return true;
}
for(let c=0; c<size; c++) {
let reg = map[row][c];
if (!regUsed[reg] && isValid(row, c)) {
board[row] = c; regUsed[reg] = true;
if (solve(row + 1)) return true;
regUsed[reg] = false; board[row] = -1;
}
}
return false;
}
function isValid(r, c) {
for(let i=0; i<r; i++) {
if (board[i] === c) return false;
if (r – i <= 1 && Math.abs(c – board[i]) <= 1) return false;
}
return true;
}
solve(0);
return solvedState;
}
function validateCompleteState(state, map, size) {
let catCount = 0;
let regCats = Array(size).fill(0);
for (let r = 0; r < size; r++) {
let rCats = 0, cCats = 0;
for (let c = 0; c < size; c++) {
if (state[r][c] === 1) {
rCats++; catCount++; regCats[map[r][c]]++;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
let nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < size && nc >= 0 && nc < size && state[nr][nc] === 1) return false;
}
}
}
if (state[c][r] === 1) cCats++;
}
if (rCats !== 1 || cCats !== 1) return false;
}
if (catCount !== size || regCats.some(v => v !== 1)) return false;
return true;
}
// — VISUAL DEDUCTIVE TUTOR SOLVER —
function solveLogically(map, size, initialState = null) {
let state = initialState ? initialState.map(row => […row]) : Array.from({length: size}, () => Array(size).fill(0));
let changed = true;
let deductions = [];
const mark = (r, c, val, reason, causeArray = []) => {
if (state[r][c] === 0) {
state[r][c] = val;
deductions.push({r, c, val, reason, cause: causeArray});
changed = true;
}
};
while(changed) {
changed = false;
// 1. Existing Cats claim territory
for(let r=0; r<size; r++) {
for(let c=0; c<size; c++) {
if (state[r][c] === 1) {
let catCause = [{r, c}];
for(let dr=-1; dr<=1; dr++) {
for(let dc=-1; dc<=1; dc++) {
if(dr===0 && dc===0) continue;
let nr=r+dr, nc=c+dc;
if(nr>=0 && nr<size && nc>=0 && nc<size) mark(nr,nc,2,”Cats cannot touch”, catCause);
}
}
for(let i=0; i<size; i++) {
if(i!==c) mark(r, i, 2, “Row already has a cat”, catCause);
if(i!==r) mark(i, c, 2, “Column already has a cat”, catCause);
}
let reg = map[r][c];
for(let rr=0; rr<size; rr++) {
for(let cc=0; cc<size; cc++) {
if((rr!==r || cc!==c) && map[rr][cc] === reg) mark(rr, cc, 2, “Region already has a cat”, catCause);
}
}
}
}
}
if (changed) continue;
let rowInfo = Array.from({length: size}, () => ({possible: [], cats: 0}));
let colInfo = Array.from({length: size}, () => ({possible: [], cats: 0}));
let regInfo = Array.from({length: size}, () => ({possible: [], cats: 0}));
for(let r=0; r<size; r++) {
for(let c=0; c<size; c++) {
let reg = map[r][c];
if(state[r][c] === 1) {
rowInfo[r].cats++; colInfo[c].cats++; regInfo[reg].cats++;
} else if (state[r][c] === 0) {
rowInfo[r].possible.push({r,c}); colInfo[c].possible.push({r,c}); regInfo[reg].possible.push({r,c});
}
}
}
// 2. Only 1 spot left
for(let i=0; i<size; i++) {
if(rowInfo[i].cats===0 && rowInfo[i].possible.length===1) {
let cause = Array.from({length:size}, (_, idx) => ({r:i, c:idx}));
mark(rowInfo[i].possible[0].r, rowInfo[i].possible[0].c, 1, “Only valid spot left in this row”, cause);
}
if(colInfo[i].cats===0 && colInfo[i].possible.length===1) {
let cause = Array.from({length:size}, (_, idx) => ({r:idx, c:i}));
mark(colInfo[i].possible[0].r, colInfo[i].possible[0].c, 1, “Only valid spot left in this column”, cause);
}
if(regInfo[i].cats===0 && regInfo[i].possible.length===1) {
let cause = [];
for(let rr=0; rr<size; rr++) for(let cc=0; cc<size; cc++) if(map[rr][cc]===i) cause.push({r:rr, c:cc});
mark(regInfo[i].possible[0].r, regInfo[i].possible[0].c, 1, “Only valid spot left in this color region”, cause);
}
}
if (changed) continue;
// 3. Pointing Rules
for(let i=0; i<size; i++) {
if(regInfo[i].cats === 0 && regInfo[i].possible.length > 0) {
let rSet = new Set(regInfo[i].possible.map(p=>p.r));
if(rSet.size === 1) {
let r = regInfo[i].possible[0].r;
for(let c=0; c<size; c++) if(state[r][c] === 0 && map[r][c] !== i) mark(r, c, 2, “The yellow region forces a cat into this row”, regInfo[i].possible);
}
let cSet = new Set(regInfo[i].possible.map(p=>p.c));
if(cSet.size === 1) {
let c = regInfo[i].possible[0].c;
for(let r=0; r<size; r++) if(state[r][c] === 0 && map[r][c] !== i) mark(r, c, 2, “The yellow region forces a cat into this column”, regInfo[i].possible);
}
}
if(rowInfo[i].cats === 0 && rowInfo[i].possible.length > 0) {
let regSet = new Set(rowInfo[i].possible.map(p=>map[p.r][p.c]));
if(regSet.size === 1) {
let reg = map[rowInfo[i].possible[0].r][rowInfo[i].possible[0].c];
for(let rr=0; rr<size; rr++) {
for(let cc=0; cc<size; cc++) {
if(state[rr][cc] === 0 && rr !== i && map[rr][cc] === reg) mark(rr, cc, 2, “The yellow row forces a cat into this region”, rowInfo[i].possible);
}
}
}
}
if(colInfo[i].cats === 0 && colInfo[i].possible.length > 0) {
let regSet = new Set(colInfo[i].possible.map(p=>map[p.r][p.c]));
if(regSet.size === 1) {
let reg = map[colInfo[i].possible[0].r][colInfo[i].possible[0].c];
for(let rr=0; rr<size; rr++) {
for(let cc=0; cc<size; cc++) {
if(state[rr][cc] === 0 && cc !== i && map[rr][cc] === reg) mark(rr, cc, 2, “The yellow column forces a cat into this region”, colInfo[i].possible);
}
}
}
}
}
if (changed) continue;
// 4. Neighborhood Blocking
let allGroups = [
…rowInfo.map(g => ({…g, name: `Row`})),
…colInfo.map(g => ({…g, name: `Column`})),
…regInfo.map(g => ({…g, name: `Region`}))
];
for(let g of allGroups) {
if (g.cats === 0 && g.possible.length > 1) {
for(let r=0; r<size; r++) {
for(let c=0; c<size; c++) {
if (state[r][c] === 0) {
let isInsideGroup = g.possible.some(p => p.r === r && p.c === c);
if (!isInsideGroup) {
let blocksAll = g.possible.every(p =>
r === p.r || c === p.c || (Math.abs(r – p.r) <= 1 && Math.abs(c – p.c) <= 1) || map[r][c] === map[p.r][p.c]
);
if (blocksAll) {
mark(r, c, 2, `This cell touches/blocks all valid spots in the yellow ${g.name}`, g.possible);
}
}
}
}
}
}
}
}
let isSolved = state.every(row => row.every(val => val !== 0));
return { isSolved, state, deductions };
}
// — Interactive Hint & Tutor System —
function handleHint() {
if (isGenerating) return;
if (document.querySelectorAll(‘.error’).length > 0) {
showToast(“Fix red highlighted errors first!”);
triggerHaptic(‘error’);
return;
}
let trueState = getTrueStateFast(currentMap, boardSize);
if (!trueState) { showToast(“This puzzle is corrupt and unsolvable!”); return; }
// 1. Clear incorrect marks
for(let r=0; r<boardSize; r++) {
for(let c=0; c<boardSize; c++) {
if (cellStates[r][c] !== 0 && cellStates[r][c] !== trueState[r][c]) {
saveState();
cellStates[r][c] = 0;
renderBoard();
showToast(“Mistake found: Removed an incorrect mark.”);
highlightCell(r, c, ‘mistake-flash’);
triggerHaptic(‘error’);
return;
}
}
}
// 2. Logic engine deductions
let currentLogic = solveLogically(currentMap, boardSize, cellStates);
// SMART FAST-FORWARD: Skip crosses visually implied by Auto-X
let stepIndex = 0;
if (autoXEnabled) {
let axMap = computeAutoX();
while (stepIndex < currentLogic.deductions.length) {
let step = currentLogic.deductions[stepIndex];
if (step.val === 2 && axMap[step.r][step.c]) {
cellStates[step.r][step.c] = 2; // Progress logic silently
stepIndex++;
} else {
break;
}
}
}
if (stepIndex < currentLogic.deductions.length) {
saveState();
let step = currentLogic.deductions[stepIndex];
cellStates[step.r][step.c] = step.val;
renderBoard();
// FLASH CAUSE CELLS (Yellow)
if (step.cause && step.cause.length > 0) {
step.cause.forEach(p => {
if(p.r !== step.r || p.c !== step.c) highlightCell(p.r, p.c, ’cause-flash’);
});
}
// FLASH TARGET CELL (Blue)
highlightCell(step.r, step.c, ‘target-flash’);
triggerHaptic(‘tap’);
let icon = step.val === 1 ? “🐱” : “✕”;
showToast(`💡 Hint: ${step.reason} ${icon}`);
return;
}
// 3. Fallback
let isComplete = true;
for(let r=0; r<boardSize; r++) {
for(let c=0; c<boardSize; c++) {
if(cellStates[r][c] === 0) {
isComplete = false;
saveState();
cellStates[r][c] = trueState[r][c];
renderBoard();
showToast(trueState[r][c] === 1 ? “Advanced: Placed a forced 🐱” : “Advanced: Placed a ✕”);
highlightCell(r, c, ‘target-flash’);
triggerHaptic(‘tap’);
return;
}
}
}
if (isComplete) showToast(“Puzzle is already complete!”);
}
// — Generator Loop with Difficulty Smoothing —
function handleGenerate() {
if (isGenerating) return;
isGenerating = true;
genBtn.textContent = “⚙️…”;
genBtn.classList.remove(‘success’); genBtn.classList.add(‘working’);
boardEl.classList.add(‘generating’);
loadingOverlay.classList.add(‘active’);
boardEl.innerHTML = ”;
const size = parseInt(document.getElementById(‘boardSizeSelect’).value);
let attempts = 0;
function generationBatch() {
for (let i = 0; i < 40; i++) {
attempts++;
const placement = generateCatPlacement(size);
if (placement) {
const map = generateRegions(size, placement);
const logicResult = solveLogically(map, size);
if (logicResult.isSolved && validateCompleteState(logicResult.state, map, size)) {
// Determine how smoothly the board flows logically.
let firstHardIdx = logicResult.deductions.findIndex(d =>
d.reason.includes(“forces”) || d.reason.includes(“touches/blocks”)
);
let catsFoundEasy = 0;
if (firstHardIdx === -1) {
catsFoundEasy = size;
} else {
for(let j = 0; j < firstHardIdx; j++) {
if (logicResult.deductions[j].val === 1) catsFoundEasy++;
}
}
let requiredEasyCats = size >= 6 ? 2 : 1;
if (catsFoundEasy >= requiredEasyCats) {
isGenerating = false;
genBtn.textContent = “✨ New Puzzle”;
genBtn.classList.add(‘success’); genBtn.classList.remove(‘working’);
boardEl.classList.remove(‘generating’);
loadingOverlay.classList.remove(‘active’);
loadCustom(map);
showToast(`Smooth logic puzzle found in ${attempts} iterations!`);
return;
}
}
}
}
if (isGenerating) requestAnimationFrame(generationBatch);
}
requestAnimationFrame(generationBatch);
}
function generateCatPlacement(N) {
let board = Array(N).fill(-1);
let result = null;
function solve(row) {
if (row === N) { result = […board]; return true; }
let cols = Array.from({length: N}, (_, i) => i).sort(() => Math.random() – 0.5);
for (let c of cols) {
if (isValid(row, c)) {
board[row] = c; if (solve(row + 1)) return true; board[row] = -1;
}
}
return false;
}
function isValid(r, c) {
for (let i = 0; i < r; i++) {
if (board[i] === c) return false;
if (Math.abs(r – i) <= 1 && Math.abs(c – board[i]) <= 1) return false;
}
return true;
}
solve(0);
return result;
}
function generateRegions(N, placement) {
let map = Array.from({length: N}, () => Array(N).fill(-1));
let unassigned = N * N – N;
for (let r = 0; r < N; r++) map[r][placement[r]] = r;
while (unassigned > 0) {
let candidates = [];
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
if (map[r][c] === -1) {
let n = [];
if (r > 0 && map[r-1][c] !== -1) n.push(map[r-1][c]);
if (r < N-1 && map[r+1][c] !== -1) n.push(map[r+1][c]);
if (c > 0 && map[r][c-1] !== -1) n.push(map[r][c-1]);
if (c < N-1 && map[r][c+1] !== -1) n.push(map[r][c+1]);
if (n.length > 0) candidates.push({r, c, neighbors: n});
}
}
}
if (candidates.length === 0) break;
let chosen = candidates[Math.floor(Math.random() * candidates.length)];
map[chosen.r][chosen.c] = chosen.neighbors[Math.floor(Math.random() * chosen.neighbors.length)];
unassigned–;
}
return map;
}
// — Sharing & Loading Logic —
function generatePassword(map) {
let size = map.length; let str = size + ‘x’;
for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) str += String.fromCharCode(65 + map[r][c]);
return str;
}
function handleShare() {
if (isGenerating) return;
const pwd = generatePassword(currentMap); pwdInput.value = pwd;
const input = document.createElement(‘input’); input.value = pwd;
document.body.appendChild(input); input.select(); input.setSelectionRange(0, 99999);
try { document.execCommand(‘copy’); showToast(‘Share Code copied to clipboard!’); }
catch (err) { showToast(‘Share this code: ‘ + pwd); }
document.body.removeChild(input);
}
function handleLoad() {
if (isGenerating) return;
const pwd = pwdInput.value.trim().toUpperCase();
if (!pwd) { showToast(“Enter a code first.”); return; }
const parts = pwd.split(‘X’);
if (parts.length !== 2) { showToast(“Invalid format.”); return; }
const size = parseInt(parts[0]), data = parts[1];
if (isNaN(size) || size < 4 || size > 10 || data.length !== size * size) { showToast(“Invalid code data.”); return; }
let newMap = [], k = 0;
for (let r = 0; r < size; r++) {
let row = [];
for (let c = 0; c < size; c++) {
let val = data.charCodeAt(k) – 65;
if (val < 0 || val >= size) { showToast(“Corrupted characters.”); return; }
row.push(val); k++;
}
newMap.push(row);
}
const testSolve = getTrueStateFast(newMap, size);
if (!testSolve || !validateCompleteState(testSolve, newMap, size)) {
showToast(“This code contains an unsolvable puzzle!”); return;
}
loadCustom(newMap); showToast(“Logical puzzle loaded successfully!”);
}
let toastTimeout;
function showToast(msg) {
toastEl.textContent = msg; toastEl.classList.add(‘show’);
clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => { toastEl.classList.remove(‘show’); }, 5000); // 5 sec to read long hints
}
</script>
</body>
</html>










































