Catduko upgrades


<!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>

Leave a Reply