Catduko upgrades

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>

Score one for Roanoke!

So glad Roanoke is against local data centers, and got rid of raven… flock cameras can go next,  and immediately,  please

Roanoke and nearby Botetourt County face intense debate over data centers. While there are smaller private colocation sites like MFX Northern Virginia, the region is fighting new megaprojects due to high water and electricity usage.

The Roanoke City Council did not vote to completely ban data centers, but they did vote unanimously on July 6, 2026, to heavily restrict where and how they can be built.

https://www.wdbj7.com/2026/07/07/roanoke-city-council-votes-zoning-changes-vape-shops-housing-data-centers/

Added bonus, fewer vape shops.

The city council voted unanimously to restrict vaping and tobacco stores from opening Roanoke. Vape and tobacco establishments are prohibited from opening within 2,000 feet of school, religious institution, childcare center, public park, or an existing vape and tobacco establishment.

The data center zoning changes were approved by the city council. The changes include data centers projects to be permitted by special exception in IPUD and data processing facility permitted in INPUD and IPUD

(IPUD most commonly stands for Industrial Planned Unit Development. It is a specific type of zoning category used by city planners to design and build planned industrial parks. Local governments, like the Roanoke City Council in Virginia, use this designation to oversee large-scale commercial and data center projects while protecting nearby neighborhoods)

Catduko experiment .4-.5

(edit- more updated https://svonberg.org/wp-content/uploads/2026/07/catduko_7.html

https://svonberg.org/wp-content/uploads/2026/07/catduko.html

fixed some logic issues in .4

https://svonberg.org/wp-content/uploads/2026/07/catduko4.html

Making a little web app for BHK’s birthday, so she doesn’t have to wait for ads on the app.


Features:

Password to recreate the same puzzle to share or do again sometime

Hint system (incomplete).

Dynamic puzzle creation, checks to be sure purely logic based solutions should work, give iterations until a feasible solution was found. (Still testing, I think it works)

For debugging, may stay after – auto x incompatible cells after placing a cat

Highlight cats red if placed in an illegal cell

Should work on a smartphone

Dynamic css grid to make it scale

“pop-in” animations for emojis, shaking for errors, glowing for hints, and a pulsing loading overlay.

Automatically calculates thick and dashed borders based on the generated region map.

Share button attempts to copy the exact board code to the user’s clipboard.

highlights rule violations (cats touching, two cats in a row, etc.) by making the cells shake and glow red.


Things to consider – better ux, be able to “paint” with a finger swipe

Larger or different grids, different themes instead of pastels? Could this be played on hexagons or triangles?

Sounds

Auto-play?

Better “success” bubble

Misfit Cult of Pepito

Cult of Pepito

every neighborhood has one animal that somehow becomes everybody’s friend

on this block

it’s pepito

the shop cat at misfit beauty

i don’t think he takes a paycheck

not officially, anyhow

he just wanders the salon like he’s the one who signed the lease

curious about everyone who walks through the door

greeting regulars like they’ve been expected

investigating bags

sniffing shoes

skittering after a toy on the hardwood floor

making sure every new face gets a proper inspection

don’t be surprised if he hops right into your chair while Mickey is cutting your hair

like he’s there to supervise

nobody seems to mind

if anything

it makes the appointment feel less like an errand

and even more like stopping by a friend’s house

outside

salem keeps moving

traffic rolls by

phones buzz

people hurry to the next thing

inside

pepito has his own schedule

one minute he’s chasing a sunbeam across the floor

the next he’s curled up beside someone who didn’t know they needed a cat in their lap today

every town needs places with personality

the kind you remember long after the haircut grows out

sometimes that personality is a great stylist

sometimes

it’s a friendly gray tabby who thinks every customer came to see him

and honestly

he might be right

#thegleest #salemva #misfitbeauty #pepito #shopcat

Meta is looking to screw you again

Meta sucks and is trying more nonsense. If you must stay on instagram or facebook, be aware of what is happening.  (Best thing is still leaving the platform altogether)

🔒 🚨 MEGA PRIVACY UPDATE: What Meta isn’t telling you about their latest change.
If you see this notification on Instagram or Facebook, pay close attention. Meta is quietly removing the option to fully “Disconnect Future Activity” from your account.

Here is exactly how this limits your digital privacy and what it actually means:

🛑 The illusion of choice: You used to be able to completely sever the link between your off-Facebook/off-Instagram browsing habits and your account. Now, Meta is replacing it with a setting called “Activity from other businesses.”

🕵️‍♂️ Tracking stays on: Instead of stopping the tracking, this new setting only lets you choose whether Meta uses that tracking data to show you personalized ads and content. The tracking itself doesn’t stop—they just change how they utilize the data they’ve gathered about you.

📉 Less control for you: By retiring the original “disconnect” feature, Meta is making it harder to keep your data siloed. Your external app usage, shopping habits, and website visits will remain tied to your Meta identity.

What you can do right now:

1️⃣ Go to your Accounts Center.
2️⃣ Find Your Information and Permissions.
3️⃣ Click on Your Activity Off Meta Technologies.
4️⃣ Look for Activity from other businesses and adjust your settings to limit how they use your data.

Don’t let them quietly strip away your privacy controls. Check your settings today!

#PrivacyMatters #DigitalPrivacy #MetaUpdate #DataProtection #TechNews #InstagramPrivacy #FacebookPrivacy #CyberSecurity #ConsumerRights

First feedback from using the gmrs channel 20 repeater

Called out as “WSJP248 checking in, anyone on the air?

Got a response, and asked about a fire up the hill, and it helped me to find out this info

ROANOKE COUNTY, Va. (WDBJ) – Roanoke County Fire & Rescue crews are responding to the Havens Wildlife Management area in the Fort Lewis area for a brush fire as of early Friday afternoon.

The County said at noon, “There are no structures in the area and no threat to the public at this time.”

One to two acres have burned as of noon, according to the County, and the fire will likely grow. “There’s a lot of thick brush in the area which will produce heavy smoke.”

The cause of the fire has not been determined.

The confederacy chose treason to preserve slavery. It’s really that simple.

This season of Rebel Spirit is right on target.

[Rebel Spirit] Episode 7: The Daughters
https://podcastaddict.com/rebel-spirit/episode/227279930 via @PodcastAddict

Throughout this season Akilah has spoken with many people – historians, politicians, journalists, artists, designers – and in every conversation one name keeps coming up. The Daughters of the Confederacy. So in today’s episode, Akilah is taking a closer look.

Rebel Spirit is a production of Ninth Planet Audio in association with iHeart Podcasts.

Reporting and writing by Akilah Hughes, she is also the Host and Executive Producer.

Produced and Written by Dan Sinker.

Welcome to my wall scrawls.