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>
        @import url(‘https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap’);

        :root {
            –bg-color: #f4f1ea; /* Warm paper */
            –text-color: #2c3e50;
            –board-bg: #ffffff;
            –line-color: #2c3e50;
            –dashed-color: rgba(44, 62, 80, 0.25);
            –error-color: #e74c3c;
            –btn-bg: #4a69bd;
            –panel-bg: #fdfbf7;
            –card-text: #444;
            –fab-bg: rgba(253, 251, 247, 0.9);
            –cross-color: #34495e; /* Pencil lead */
        }

        [data-theme=”dark”] {
            –bg-color: #1a1a1a;
            –text-color: #e0e0e0;
            –board-bg: #222222;
            –line-color: #aaaaaa;
            –dashed-color: rgba(255, 255, 255, 0.2);
            –panel-bg: #2a2a2a;
            –card-text: #bbbbbb;
            –fab-bg: rgba(42, 42, 42, 0.9);
            –cross-color: #bdc3c7;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }

        body {
            font-family: ‘Patrick Hand’, ‘Comic Sans MS’, cursive, 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;
            overflow-x: hidden;
            background-image: url(“data:image/svg+xml,%3Csvg width=’100′ height=’100′ viewBox=’0 0 100 100′ xmlns=’http://www.w3.org/2000/svg’%3E%3Cfilter id=’noise’%3E%3CfeTurbulence type=’fractalNoise’ baseFrequency=’0.8′ numOctaves=’4′ stitchTiles=’stitch’/%3E%3C/filter%3E%3Crect width=’100′ height=’100′ filter=’url(%23noise)’ opacity=’0.05’/%3E%3C/svg%3E”);
        }

        /* Confetti Layer */
        #confetti-canvas {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            pointer-events: none; z-index: 9999;
        }

        .app-container {
            width: 100%; max-width: 500px; padding: 15px;
            display: flex; flex-direction: column; gap: 14px;
            flex: 1; padding-bottom: 100px;
        }

        header {
            display: flex; justify-content: space-between; align-items: center; margin-bottom: -5px;
        }

        h1 {
            font-size: 2.8rem; color: inherit; letter-spacing: 1px;
            display: flex; align-items: center; gap: 8px; line-height: 1;
        }
       
        h1 svg { width: 44px; height: 44px; }

        .header-tools { display: flex; gap: 10px; }

        .icon-btn-header {
            background: none; border: none; font-size: 1.5rem;
            cursor: pointer; padding: 5px; color: var(–text-color);
            transition: transform 0.2s; display: flex; justify-content: center; align-items: center;
        }
        .icon-btn-header:active { transform: scale(0.8) rotate(-10deg); }

        .sketchy-border {
            border: 2px solid var(–line-color);
            border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
        }

        .panel {
            background: var(–panel-bg); padding: 12px;
            box-shadow: 2px 4px 10px 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; font-size: 1.2rem; font-weight: normal;
            font-family: inherit; letter-spacing: 1px;
            cursor: pointer; transition: transform 0.1s, background-color 0.2s, opacity 0.2s;
            flex: 1; text-align: center; white-space: nowrap; display: flex; justify-content: center; align-items: center; gap: 6px;
        }

        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.info { background-color: #3498db; }
        button.secondary { background-color: #95a5a6; }
        button.working { background-color: #8e44ad; animation: pulse 1.5s infinite; pointer-events: none;}
       
        input, select {
            padding: 10px; font-size: 1.1rem; outline: none; flex: 1; min-width: 0;
            color: var(–text-color); background: var(–bg-color); font-family: inherit;
        }

        /* Board Styles */
        .board-container {
            display: flex; justify-content: center; align-items: center;
            width: 100%; position: relative;
        }

        .board-wrapper {
            width: 100%; aspect-ratio: 1 / 1;
            background-color: var(–board-bg);
            box-shadow: 4px 6px 15px rgba(0,0,0,0.08);
            border-radius: 4px;
            position: relative;
        }

        #game-board {
            display: grid; width: 100%; height: 100%;
            transition: opacity 0.3s;
            filter: url(#wobble);
        }
        #game-board.generating { opacity: 0.3; pointer-events: none; }

        #loading-overlay {
            position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
            font-size: 1.5rem; 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;
            cursor: pointer; background-size: cover;
            transition: background-color 0.2s; position: relative;
        }

        /* Animations */
        .bloom-in { animation: bloom 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; }

        .cell svg {
            pointer-events: none;
            animation: popIn 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        }

        .cell.cat svg { width: 75%; height: 75%; }
       
        .cell.cross svg { stroke: var(–cross-color); fill: none; stroke-linecap: round; stroke-linejoin: round; width: 60%; height: 60%; opacity: 0.85; }
        .cell.auto-cross svg { stroke: var(–cross-color); fill: none; stroke-linecap: round; stroke-linejoin: round; opacity: 0.30; animation: none; width: 50%; height: 50%; }
       
        .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;
        }

        /* Floating Action Bar (FAB) */
        .fab-menu {
            position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
            background: var(–fab-bg); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
            padding: 8px 15px; box-shadow: 2px 6px 20px rgba(0,0,0,0.15);
            display: flex; gap: 15px; z-index: 100;
        }
       
        .fab-btn {
            background: none; border: none; display: flex; flex-direction: column;
            align-items: center; justify-content: center; color: var(–text-color);
            font-size: 0.9rem; font-weight: normal; cursor: pointer; padding: 5px 10px;
            transition: transform 0.1s, color 0.2s; gap: 2px; opacity: 1; font-family: inherit;
        }
        .fab-btn svg { width: 26px; height: 26px; stroke-width: 2.5; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round;}
        .fab-btn:active { transform: scale(0.85) rotate(-5deg); }
        .fab-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none;}
        .fab-btn.active-tool { color: #3498db; }

        /* Rules & Modal */
        .rules-card { background: var(–panel-bg); padding: 15px; font-size: 1rem; line-height: 1.3; color: var(–card-text); transition: background-color 0.3s; }

        .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; text-align: center;
            transform: scale(0.8) rotate(-2deg); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
            box-shadow: 4px 10px 30px rgba(0,0,0,0.3); width: 80%; max-width: 320px;
            display: flex; flex-direction: column; gap: 15px;
        }
        .modal-overlay.active .modal-content { transform: scale(1) rotate(0deg); }
        .modal-content h2 { color: #2ecc71; margin-bottom: 5px; font-size: 2.5rem;}
        .modal-content p { color: var(–card-text); margin-bottom: 5px; font-size: 1.2rem;}
       
        .modal-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 15px; }

        /* Toast */
        #toast {
            position: fixed; top: 80px; left: 50%; transform: translateX(-50%) translateY(-50px);
            background: #34495e; color: white; padding: 12px 20px; border-radius: 12px;
            font-size: 1.1rem; line-height: 1.2; 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) rotate(-20deg); opacity: 0; } 100% { transform: scale(1) rotate(0deg); opacity: 1; } }
        @keyframes bloom { 0% { transform: scale(0.2); opacity: 0; border-radius: 50%;} 100% { transform: scale(1); opacity: 1; border-radius: 0;} }
        @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-4px) rotate(-1deg); } 75% { transform: translateX(4px) rotate(1deg); } }
        @keyframes fadeOut { 0% { opacity: 1; } 100% { opacity: 0; } }
        @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }

    </style>
</head>
<body>
    <!– Wobbly line filter for Hand-Drawn aesthetic –>
    <svg width=”0″ height=”0″ style=”position:absolute;z-index:-1;”>
        <defs>
            <filter id=”wobble” x=”-5%” y=”-5%” width=”110%” height=”110%”>
                <feTurbulence type=”fractalNoise” baseFrequency=”0.04″ numOctaves=”2″ result=”noise” />
                <feDisplacementMap in=”SourceGraphic” in2=”noise” scale=”2.5″ xChannelSelector=”R” yChannelSelector=”G” />
            </filter>
        </defs>
    </svg>

    <canvas id=”confetti-canvas”></canvas>

    <div class=”app-container”>
        <header>
            <h1>
                <!– Title Icon –>
                <svg viewBox=”0 0 100 100″>
                    <path d=”M20 50 L10 15 L45 35 M80 50 L90 15 L55 35″ fill=”#f39c12″ stroke=”#d35400″ stroke-width=”4″ stroke-linejoin=”round”/>
                    <path d=”M22 40 L16 22 L35 32 M78 40 L84 22 L65 32″ fill=”#f5b7b1″ />
                    <path d=”M 15 55 C 15 20, 85 20, 85 55 C 85 90, 50 95, 50 95 C 50 95, 15 90, 15 55 Z” fill=”#f1c40f” stroke=”#d35400″ stroke-width=”4″ stroke-linejoin=”round”/>
                    <path d=”M35 25 Q50 45 65 25 M45 20 L50 35 L55 20 M20 45 Q35 50 40 45 M80 45 Q65 50 60 45 M15 55 Q30 55 35 52 M85 55 Q70 55 65 52″ stroke=”#e67e22″ stroke-width=”4″ fill=”none” stroke-linecap=”round”/>
                    <circle cx=”35″ cy=”55″ r=”6″ fill=”#f1c40f”/><circle cx=”35″ cy=”55″ r=”3″ fill=”#2c3e50″/>
                    <circle cx=”65″ cy=”55″ r=”6″ fill=”#f1c40f”/><circle cx=”65″ cy=”55″ r=”3″ fill=”#2c3e50″/>
                    <!– High Contrast Deep Pink Nose with Black Dot –>
                    <path d=”M46 68 L54 68 L50 73 Z” fill=”#ff1493″/>
                    <circle cx=”50″ cy=”70.5″ r=”1.5″ fill=”#1a1a1a”/>
                    <path d=”M50 73 Q45 80 40 75 M50 73 Q55 80 60 75″ stroke=”#d35400″ stroke-width=”3″ fill=”none” stroke-linecap=”round”/>
                    <path d=”M30 65 L10 60 M28 70 L8 70 M30 75 L10 80 M70 65 L90 60 M72 70 L92 70 M70 75 L90 80″ stroke=”#d35400″ stroke-width=”2″ fill=”none” stroke-linecap=”round”/>
                </svg>
                Catoku
            </h1>
            <div class=”header-tools”>
                <button id=”muteToggle” onclick=”toggleMute()” class=”icon-btn-header” title=”Toggle Sound”>
                    <svg id=”vol-icon” viewBox=”0 0 24 24″ width=”28″ height=”28″ fill=”none” stroke=”currentColor” stroke-width=”2.5″ stroke-linecap=”round” stroke-linejoin=”round”><polygon points=”11 5 6 9 2 9 2 15 6 15 11 19 11 5″></polygon><path d=”M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07″></path></svg>
                </button>
                <button id=”themeToggle” onclick=”toggleTheme()” class=”icon-btn-header” title=”Toggle Dark Mode”>
                    <svg viewBox=”0 0 24 24″ width=”28″ height=”28″ fill=”none” stroke=”currentColor” stroke-width=”2.5″ stroke-linecap=”round” stroke-linejoin=”round”><path d=”M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z”></path></svg>
                </button>
            </div>
        </header>

        <div class=”board-container”>
            <div class=”board-wrapper sketchy-border”>
                <div id=”game-board”></div>
            </div>
            <div id=”loading-overlay”>Sketching New<br/>Puzzle…</div>
        </div>

        <div class=”panel sketchy-border”>
            <div class=”controls”>
                <select id=”boardSizeSelect” class=”sketchy-border”>
                    <option value=”5″ selected>5×5 (Easy)</option>
                    <option value=”6″>6×6 (Medium)</option>
                    <option value=”8″>8×8 (Hard)</option>
                    <option value=”10″>10×10 (Very Hard)</option>
                </select>
                <button id=”genBtn” onclick=”handleGenerate()” class=”success sketchy-border”>✨ Draw Puzzle</button>
            </div>
        </div>

        <div class=”panel sketchy-border”>
            <div class=”controls”>
                <input type=”text” id=”pwdInput” placeholder=”Paste share code…” autocomplete=”off” class=”sketchy-border” />
                <button onclick=”handleLoad()” class=”sketchy-border” style=”flex: 0.5;”>Load</button>
                <button onclick=”handleShare()” class=”info sketchy-border” style=”flex: 0.6;”>Share</button>
            </div>
        </div>
       
        <div class=”controls” style=”justify-content: center;”>
            <button onclick=”resetBoard()” class=”danger sketchy-border” style=”width:100%; max-width: 200px;”>↺ Erase Board</button>
        </div>

        <div class=”rules-card sketchy-border”>
            <strong>Rules:</strong> Exactly 1 cat per row, column, and colored block. Cats cannot touch, even diagonally. Tap to draw a Cat, an ✕, or erase.
        </div>
    </div>

    <!– Floating Action Bar –>
    <div class=”fab-menu sketchy-border”>
        <button id=”autoXBtn” onclick=”toggleAutoX()” class=”fab-btn”>
            <svg viewBox=”0 0 24 24″><path d=”M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z”></path><path d=”M15 9l-6 6″></path><path d=”M9 9l6 6″></path></svg>
            Auto-✕
        </button>
        <button id=”hintBtn” onclick=”handleHint()” class=”fab-btn” style=”color: #e67e22;”>
            <svg viewBox=”0 0 24 24″><path d=”M9 18h6″></path><path d=”M10 22h4″></path><path d=”M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14″></path></svg>
            Hint
        </button>
        <button id=”undoBtn” onclick=”handleUndo()” class=”fab-btn” disabled>
            <svg viewBox=”0 0 24 24″><path d=”M3 7v6h6″></path><path d=”M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13″></path></svg>
            Undo
        </button>
    </div>

    <div class=”modal-overlay” id=”winModal”>
        <div class=”modal-content sketchy-border”>
            <h2>Purr-fect! 🐾</h2>
            <p>You solved this masterpiece!</p>
            <div class=”modal-actions”>
                <button class=”success sketchy-border” style=”width: 100%” onclick=”nextLevelFromModal()”>Draw Next Puzzle</button>
                <button class=”secondary sketchy-border” style=”width: 100%” onclick=”closeWinModal()”>View Completed Board</button>
            </div>
        </div>
    </div>

    <div id=”toast” class=”sketchy-border”>Message</div>

    <script>
        // — Pastel “Marker” Palettes —
        const lightRegionColors = [ ‘#ffcccc’, ‘#ffe6cc’, ‘#ffffcc’, ‘#ccffcc’, ‘#ccffff’, ‘#cce5ff’, ‘#ccccff’, ‘#ffccff’, ‘#e5ccff’, ‘#d9f2e6’ ];
        const darkRegionColors = [ ‘#5c3333’, ‘#5c4a33’, ‘#5c5c33’, ‘#335c33’, ‘#335c5c’, ‘#334a5c’, ‘#33335c’, ‘#5c335c’, ‘#4a335c’, ‘#335c4a’ ];

        // — Hand-Drawn SVG Variants —
        const SVG_CATS = [
            // 1. Orange Tabby (High Contrast Pink Nose with Black Dot, Yellow Eyes)
            `<svg viewBox=”0 0 100 100″>
                <path d=”M20 50 L10 15 L45 35 M80 50 L90 15 L55 35″ fill=”#f39c12″ stroke=”#d35400″ stroke-width=”4″ stroke-linejoin=”round”/>
                <path d=”M22 40 L16 22 L35 32 M78 40 L84 22 L65 32″ fill=”#f5b7b1″ />
                <path d=”M 15 55 C 15 20, 85 20, 85 55 C 85 90, 50 95, 50 95 C 50 95, 15 90, 15 55 Z” fill=”#f1c40f” stroke=”#d35400″ stroke-width=”4″ stroke-linejoin=”round”/>
                <path d=”M35 25 Q50 45 65 25 M45 20 L50 35 L55 20 M20 45 Q35 50 40 45 M80 45 Q65 50 60 45 M15 55 Q30 55 35 52 M85 55 Q70 55 65 52″ stroke=”#e67e22″ stroke-width=”4″ fill=”none” stroke-linecap=”round”/>
                <circle cx=”35″ cy=”55″ r=”6″ fill=”#f1c40f”/><circle cx=”35″ cy=”55″ r=”3″ fill=”#2c3e50″/>
                <circle cx=”65″ cy=”55″ r=”6″ fill=”#f1c40f”/><circle cx=”65″ cy=”55″ r=”3″ fill=”#2c3e50″/>
                <path d=”M46 68 L54 68 L50 73 Z” fill=”#ff1493″/>
                <circle cx=”50″ cy=”70.5″ r=”1.5″ fill=”#1a1a1a”/>
                <path d=”M50 73 Q45 80 40 75 M50 73 Q55 80 60 75″ stroke=”#d35400″ stroke-width=”3″ fill=”none” stroke-linecap=”round”/>
                <path d=”M30 65 L10 60 M28 70 L8 70 M30 75 L10 80 M70 65 L90 60 M72 70 L92 70 M70 75 L90 80″ stroke=”#d35400″ stroke-width=”2″ fill=”none” stroke-linecap=”round”/>
            </svg>`,
            // 2. Brown Tabby (Green Eyes, White Chin)
            `<svg viewBox=”0 0 100 100″>
                <path d=”M25 45 L15 10 L45 25 M75 45 L85 10 L55 25″ fill=”#a07855″ stroke=”#2c3e50″ stroke-width=”4″ stroke-linejoin=”round”/>
                <path d=”M26 35 L20 18 L38 26 M74 35 L80 18 L62 26″ fill=”#e6b0aa” />
                <path d=”M 20 50 C 20 25, 80 25, 80 50 C 80 85, 20 85, 20 50 Z” fill=”#c49a6c” stroke=”#2c3e50″ stroke-width=”4″ stroke-linejoin=”round”/>
                <path d=”M40 25 L50 40 L60 25 M30 30 L45 42 M70 30 L55 42 M20 40 L35 45 M80 40 L65 45 M20 50 L35 52 M80 50 L65 52″ stroke=”#2c3e50″ stroke-width=”4.5″ fill=”none” stroke-linecap=”round”/>
                <ellipse cx=”50″ cy=”74″ rx=”20″ ry=”14″ fill=”#ffffff”/>
                <circle cx=”35″ cy=”55″ r=”7″ fill=”#2ecc71″/><circle cx=”35″ cy=”55″ r=”4″ fill=”#2c3e50″/>
                <circle cx=”65″ cy=”55″ r=”7″ fill=”#2ecc71″/><circle cx=”65″ cy=”55″ r=”4″ fill=”#2c3e50″/>
                <path d=”M47 70 L53 70 L50 74 Z” fill=”#2c3e50″/>
                <path d=”M50 74 Q45 80 40 76 M50 74 Q55 80 60 76″ stroke=”#2c3e50″ stroke-width=”3″ fill=”none” stroke-linecap=”round”/>
                <path d=”M30 65 L10 65 M28 72 L8 75 M70 65 L90 65 M72 72 L92 75″ stroke=”#2c3e50″ stroke-width=”2″ fill=”none” stroke-linecap=”round”/>
            </svg>`,
            // 3. Black Cat (Yellow Eyes)
            `<svg viewBox=”0 0 100 100″>
                <path d=”M20 50 L10 15 L45 35 M80 50 L90 15 L55 35″ fill=”#2c3e50″ stroke=”#1a252f” stroke-width=”4″ stroke-linejoin=”round”/>
                <path d=”M22 40 L16 22 L35 32 M78 40 L84 22 L65 32″ fill=”#ffb3b3″ />
                <path d=”M 15 55 C 15 20, 85 20, 85 55 C 85 90, 50 95, 50 95 C 50 95, 15 90, 15 55 Z” fill=”#34495e” stroke=”#1a252f” stroke-width=”4″ stroke-linejoin=”round”/>
                <ellipse cx=”35″ cy=”55″ rx=”8″ ry=”6″ fill=”#f1c40f”/>
                <ellipse cx=”35″ cy=”55″ rx=”3″ ry=”5″ fill=”#1a252f”/>
                <ellipse cx=”65″ cy=”55″ rx=”8″ ry=”6″ fill=”#f1c40f”/>
                <ellipse cx=”65″ cy=”55″ rx=”3″ ry=”5″ fill=”#1a252f”/>
                <path d=”M46 68 L54 68 L50 73 Z” fill=”#ffb3b3″/>
                <path d=”M50 73 Q45 80 40 75 M50 73 Q55 80 60 75″ stroke=”#ffb3b3″ stroke-width=”3″ fill=”none” stroke-linecap=”round”/>
                <path d=”M30 65 L10 60 M28 70 L8 70 M30 75 L10 80 M70 65 L90 60 M72 70 L92 70 M70 75 L90 80″ stroke=”#7f8c8d” stroke-width=”2″ fill=”none” stroke-linecap=”round”/>
            </svg>`,
            // 4. Siamese / Blocky
            `<svg viewBox=”0 0 100 100″>
                <path d=”M25 40 L15 10 L45 25 M75 40 L85 10 L55 25″ fill=”#5d4037″ stroke=”#3e2723″ stroke-width=”4″ stroke-linejoin=”round”/>
                <path d=”M 25 35 C 40 30, 60 30, 75 35 L 85 80 L 15 80 Z” fill=”#fff3e0″ stroke=”#8d6e63″ stroke-width=”4″ stroke-linejoin=”round”/>
                <path d=”M 50 45 C 25 45, 30 80, 50 80 C 70 80, 75 45, 50 45 Z” fill=”#5d4037″ stroke-linejoin=”round”/>
                <circle cx=”35″ cy=”55″ r=”6″ fill=”#3498db”/><circle cx=”35″ cy=”55″ r=”3″ fill=”#1a252f”/>
                <circle cx=”65″ cy=”55″ r=”6″ fill=”#3498db”/><circle cx=”65″ cy=”55″ r=”3″ fill=”#1a252f”/>
                <path d=”M47 70 L53 70 L50 74 Z” fill=”#1a252f”/>
                <path d=”M50 74 Q45 80 40 76 M50 74 Q55 80 60 76″ stroke=”#1a252f” stroke-width=”3″ fill=”none” stroke-linecap=”round”/>
                <path d=”M30 65 L10 65 M28 72 L8 75 M70 65 L90 65 M72 72 L92 75″ stroke=”#8d6e63″ stroke-width=”2″ fill=”none” stroke-linecap=”round”/>
            </svg>`
        ];

        // Bold and visually distinct crosses
        const SVG_CROSSES = [
            `<svg viewBox=”0 0 100 100″><path d=”M 20 20 L 80 80 M 80 20 L 20 80″ stroke-width=”12″/></svg>`,
            `<svg viewBox=”0 0 100 100″><path d=”M 15 25 C 40 40, 60 60, 85 75 M 85 25 C 60 40, 40 60, 15 75″ stroke-width=”14″/></svg>`,
            `<svg viewBox=”0 0 100 100″><path d=”M 25 10 L 75 90 M 85 20 L 15 80″ stroke-width=”10″/></svg>`
        ];

        let boardSize = 5;
        let currentMap = [];
        let cellStates = [];
        let cellVariants = [];
        let moveHistory = [];
        let isGenerating = false;
        let autoXEnabled = false;
        let isDarkMode = false;
        let isMuted = false;
        let hasWon = 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’);
        const volIcon = document.getElementById(‘vol-icon’);

        // — Audio System (Web Audio API Synth) —
        const AudioContext = window.AudioContext || window.webkitAudioContext;
        let audioCtx;

        function initAudio() {
            if (!audioCtx) audioCtx = new AudioContext();
            if (audioCtx.state === ‘suspended’) audioCtx.resume();
        }

        function playSound(type) {
            if (isMuted) return;
            initAudio();
            const now = audioCtx.currentTime;
            const osc = audioCtx.createOscillator();
            const gain = audioCtx.createGain();
            osc.connect(gain); gain.connect(audioCtx.destination);

            if (type === ‘cat’) {
                osc.type = ‘sine’;
                osc.frequency.setValueAtTime(350, now);
                osc.frequency.exponentialRampToValueAtTime(700, now + 0.1);
                gain.gain.setValueAtTime(0.4, now);
                gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
                osc.start(now); osc.stop(now + 0.15);
            } else if (type === ‘cross’) {
                osc.type = ‘triangle’;
                osc.frequency.setValueAtTime(250, now);
                osc.frequency.exponentialRampToValueAtTime(150, now + 0.05);
                gain.gain.setValueAtTime(0.2, now);
                gain.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
                osc.start(now); osc.stop(now + 0.1);
            } else if (type === ‘win’) {
                const notes = [523.25, 659.25, 783.99, 1046.50];
                notes.forEach((freq, i) => {
                    const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
                    o.type = ‘sine’; o.connect(g); g.connect(audioCtx.destination);
                    o.frequency.value = freq;
                    g.gain.setValueAtTime(0, now + i * 0.1);
                    g.gain.linearRampToValueAtTime(0.2, now + i * 0.1 + 0.05);
                    g.gain.exponentialRampToValueAtTime(0.01, now + i * 0.1 + 0.6);
                    o.start(now + i * 0.1); o.stop(now + i * 0.1 + 0.7);
                });
            }
        }

        function toggleMute() {
            isMuted = !isMuted;
            localStorage.setItem(‘muted’, isMuted);
            if(isMuted) {
                volIcon.innerHTML = `<line x1=”1″ y1=”1″ x2=”23″ y2=”23″></line><path d=”M9 9v6a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6″></path><path d=”M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23″></path><line x1=”12″ y1=”19″ x2=”12″ y2=”23″></line><line x1=”8″ y1=”23″ x2=”16″ y2=”23″></line>`;
            } else {
                initAudio();
                volIcon.innerHTML = `<polygon points=”11 5 6 9 2 9 2 15 6 15 11 19 11 5″></polygon><path d=”M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07″></path>`;
                playSound(‘cat’);
            }
        }

        // — Confetti Engine —
        const canvas = document.getElementById(‘confetti-canvas’);
        const ctx = canvas.getContext(‘2d’);
        let particles = [];
        let confettiActive = false;

        function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
        window.addEventListener(‘resize’, resizeCanvas); resizeCanvas();

        function fireConfetti() {
            particles = []; confettiActive = true;
            for(let i=0; i<80; i++) {
                particles.push({
                    x: canvas.width/2, y: canvas.height/2 + 100,
                    r: Math.random() * 6 + 3,
                    dx: Math.random() * 14 – 7, dy: Math.random() * -12 – 6,
                    color: `hsl(${Math.random()*360}, 80%, 60%)`,
                    tilt: Math.floor(Math.random() * 10) – 10,
                    tiltAngleInc: (Math.random() * 0.07) + 0.05, tiltAngle: 0
                });
            }
            requestAnimationFrame(renderConfetti);
        }

        function renderConfetti() {
            if(!confettiActive) return;
            requestAnimationFrame(renderConfetti);
            ctx.clearRect(0,0,canvas.width, canvas.height);
            let activeCount = 0;
            for(let i=0; i<particles.length; i++) {
                let p = particles[i];
                p.tiltAngle += p.tiltAngleInc;
                p.y += (Math.cos(p.tiltAngle) + 1 + p.r / 2) / 2;
                p.x += Math.sin(p.tiltAngle) * 2;
                p.dy += 0.15; // Gravity
                p.x += p.dx; p.y += p.dy;
               
                if (p.y <= canvas.height) activeCount++;
               
                ctx.beginPath(); ctx.lineWidth = p.r; ctx.strokeStyle = p.color;
                ctx.moveTo(p.x + p.tilt + p.r, p.y); ctx.lineTo(p.x + p.tilt, p.y + p.tilt + p.r); ctx.stroke();
            }
            if(activeCount === 0) confettiActive = false;
        }

        // — Core Initialization —
        window.onload = () => {
            if (localStorage.getItem(‘theme’) === ‘dark’) toggleTheme();
            if (localStorage.getItem(‘muted’) === ‘true’) toggleMute();
            document.body.addEventListener(‘touchstart’, initAudio, {once:true});
            document.body.addEventListener(‘click’, initAudio, {once:true});
            handleGenerate();
        };

        function triggerHaptic(type) {
            if (!navigator.vibrate) return;
            if (type === ‘tap’) navigator.vibrate(10);
            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.innerHTML = ‘<svg viewBox=”0 0 24 24″ width=”28″ height=”28″ fill=”none” stroke=”currentColor” stroke-width=”2.5″ stroke-linecap=”round” stroke-linejoin=”round”><circle cx=”12″ cy=”12″ r=”5″></circle><line x1=”12″ y1=”1″ x2=”12″ y2=”3″></line><line x1=”12″ y1=”21″ x2=”12″ y2=”23″></line><line x1=”4.22″ y1=”4.22″ x2=”5.64″ y2=”5.64″></line><line x1=”18.36″ y1=”18.36″ x2=”19.78″ y2=”19.78″></line><line x1=”1″ y1=”12″ x2=”3″ y2=”12″></line><line x1=”21″ y1=”12″ x2=”23″ y2=”12″></line><line x1=”4.22″ y1=”19.78″ x2=”5.64″ y2=”18.36″></line><line x1=”18.36″ y1=”5.64″ x2=”19.78″ y2=”4.22″></line></svg>’;
                localStorage.setItem(‘theme’, ‘dark’);
            } else {
                document.documentElement.removeAttribute(‘data-theme’);
                themeToggle.innerHTML = ‘<svg viewBox=”0 0 24 24″ width=”28″ height=”28″ fill=”none” stroke=”currentColor” stroke-width=”2.5″ stroke-linecap=”round” stroke-linejoin=”round”><path d=”M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z”></path></svg>’;
                localStorage.setItem(‘theme’, ‘light’);
            }
            if (currentMap.length > 0) renderBoard(false);
        }

        function toggleAutoX() {
            autoXEnabled = !autoXEnabled;
            triggerHaptic(‘tap’); playSound(‘cross’);
            if (autoXEnabled) {
                autoXBtn.classList.add(‘active-tool’); showToast(“Auto-✕ enabled.”);
            } else {
                autoXBtn.classList.remove(‘active-tool’); showToast(“Auto-✕ disabled.”);
            }
            renderBoard(false);
        }

        // — State Management —
        function saveState() {
            moveHistory.push(JSON.parse(JSON.stringify(cellStates)));
            undoBtn.disabled = false;
        }

        function handleUndo() {
            if (moveHistory.length === 0 || isGenerating || hasWon) return;
            triggerHaptic(‘tap’); playSound(‘cross’);
            cellStates = moveHistory.pop();
            undoBtn.disabled = moveHistory.length === 0;
            renderBoard(false);
        }

        function clearHistory() {
            moveHistory = []; undoBtn.disabled = true;
        }

        function loadCustom(mapData) {
            currentMap = mapData;
            boardSize = currentMap.length;
            cellStates = Array.from({ length: boardSize }, () => Array(boardSize).fill(0));
            hasWon = false;
           
            // Assign permanent visual style variations for this specific board
            cellVariants = Array.from({ length: boardSize }, () =>
                Array(boardSize).fill(0).map(() => ({
                    cat: Math.floor(Math.random() * SVG_CATS.length),
                    cross: Math.floor(Math.random() * SVG_CROSSES.length)
                }))
            );

            clearHistory();
            winModal.classList.remove(‘active’);
            pwdInput.value = generatePassword(currentMap);
           
            const select = document.getElementById(‘boardSizeSelect’);
            if(select.querySelector(`option[value=”${boardSize}”]`)) select.value = boardSize;

            renderBoard(true);
        }

        function resetBoard() {
            if (isGenerating) return;
            hasWon = false;
            triggerHaptic(‘tap’); playSound(‘cross’);
            cellStates = Array.from({ length: boardSize }, () => Array(boardSize).fill(0));
            clearHistory(); renderBoard(false);
        }

        function closeWinModal() {
            winModal.classList.remove(‘active’);
        }

        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;
                    }
                }
            }
            return axMap;
        }

        // — Rendering & Interactions —
        function renderBoard(isNewGen = false) {
            boardEl.innerHTML = ”;
            boardEl.style.gridTemplateColumns = `repeat(${boardSize}, 1fr)`;
            boardEl.style.gridTemplateRows = `repeat(${boardSize}, 1fr)`;
           
            let axMap = autoXEnabled ? computeAutoX() : null;
            let activePalette = isDarkMode ? darkRegionColors : lightRegionColors;
           
            const centerR = (boardSize – 1) / 2; const centerC = (boardSize – 1) / 2;

            // Scale borders based on grid density
            const thick = boardSize >= 10 ? `2.5px solid var(–line-color)` : `3.5px solid var(–line-color)`;
            const thin = boardSize >= 10 ? `1px dashed var(–dashed-color)` : `1.5px dashed var(–dashed-color)`;

            for (let r = 0; r < boardSize; r++) {
                for (let c = 0; c < boardSize; c++) {
                    const region = currentMap[r][c];
                    const variant = cellVariants[r][c];
                   
                    const cell = document.createElement(‘div’);
                    cell.className = ‘cell’; cell.id = `cell-${r}-${c}`;
                    cell.style.backgroundColor = activePalette[region % activePalette.length];
                   
                    if (isNewGen) {
                        const dist = Math.sqrt(Math.pow(r – centerR, 2) + Math.pow(c – centerC, 2));
                        cell.style.animationDelay = `${dist * 0.05}s`;
                        cell.classList.add(‘bloom-in’);
                    }

                    let borderCSS = ”;
                    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 = SVG_CATS[variant.cat];
                        cell.classList.add(‘cat’);
                    } else if (state === 2) {
                        cell.innerHTML = SVG_CROSSES[variant.cross]; cell.classList.add(‘cross’);
                    } else if (autoXEnabled && axMap[r][c]) {
                        cell.innerHTML = SVG_CROSSES[variant.cross]; cell.classList.add(‘cross’, ‘auto-cross’);
                    }

                    cell.addEventListener(‘click’, () => {
                        if (isGenerating || hasWon) return; // Prevent edits if board is solved/locked
                        saveState();
                       
                        let nextState = (cellStates[r][c] + 1) % 3;
                        cellStates[r][c] = nextState;
                       
                        if(nextState === 1) { triggerHaptic(‘tap’); playSound(‘cat’); }
                        else if (nextState === 2) { triggerHaptic(‘tap’); playSound(‘cross’); }
                        else { triggerHaptic(‘tap’); playSound(‘cross’); }
                       
                        renderBoard(false);
                    });
                    boardEl.appendChild(cell);
                }
            }
            if(!isGenerating && currentMap.length > 0 && !isNewGen && !hasWon) 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 && !hasWon) {
                hasWon = true;
                triggerHaptic(‘win’); playSound(‘win’); fireConfetti();
                setTimeout(() => winModal.classList.add(‘active’), 600);
            }
        }

        // — Logic Validation Suite —
       
        // This explicitly counts ALL valid solutions using backtracking.
        // It guarantees mathematical uniqueness.
        function countSolutions(map, size) {
            let board = Array(size).fill(-1);
            let regUsed = Array(size).fill(false);
            let solutions = 0;
           
            function solve(row) {
                if (row === size) {
                    solutions++;
                    return;
                }
                for(let c=0; c<size; c++) {
                    let reg = map[row][c];
                    if (!regUsed[reg] && isValid(row, c)) {
                        board[row] = c; regUsed[reg] = true;
                        solve(row + 1);
                        if (solutions > 1) return; // Exit early if multiple found
                        regUsed[reg] = false; board[row] = -1;
                    }
                }
            }
            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 solutions;
        }

        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;
        }

        // — STRICT 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. 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 || hasWon) return;
            if (document.querySelectorAll(‘.error’).length > 0) {
                showToast(“Fix red highlighted errors first!”);
                triggerHaptic(‘error’); playSound(‘cross’);
                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(false);
                        showToast(“Mistake found: Removed an incorrect mark.”);
                        highlightCell(r, c, ‘mistake-flash’);
                        triggerHaptic(‘error’); playSound(‘cross’);
                        return;
                    }
                }
            }

            // 2. Logic engine deductions
            let currentLogic = solveLogically(currentMap, boardSize, cellStates);
            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(false);
               
                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’); });
                }
                highlightCell(step.r, step.c, ‘target-flash’);
               
                triggerHaptic(‘tap’);
                if(step.val === 1) playSound(‘cat’); else playSound(‘cross’);
                showToast(`💡 Hint: ${step.reason}`);
                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(false);
                        showToast(trueState[r][c] === 1 ? “Advanced: Placed a forced 🐱” : “Advanced: Placed a ✕”);
                        highlightCell(r, c, ‘target-flash’);
                        triggerHaptic(‘tap’);
                        if(trueState[r][c]===1) playSound(‘cat’); else playSound(‘cross’);
                        return;
                    }
                }
            }
        }

        // — Generator Loop —
        function handleGenerate() {
            if (isGenerating) return;
            isGenerating = true;
            hasWon = false;
           
            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)) {
                           
                            // MATHEMATICAL UNIQUENESS CHECK
                            // Throws the board away if there are 2 or more valid solutions!
                            if (countSolutions(map, size) !== 1) {
                                continue;
                            }
                           
                            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++;
                           
                            // 10×10 is hard enough, ensure 2 easy cats first.
                            let requiredEasyCats = size >= 8 ? 2 : (size >= 6 ? 2 : 1);

                            if (catsFoundEasy >= requiredEasyCats) {
                                isGenerating = false;
                                genBtn.textContent = “✨ Draw Puzzle”; genBtn.classList.add(‘success’); genBtn.classList.remove(‘working’);
                                boardEl.classList.remove(‘generating’); loadingOverlay.classList.remove(‘active’);
                               
                                loadCustom(map);
                                showToast(`Perfect, unique 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;
        }

        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!’); triggerHaptic(‘tap’); }
            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 > 12 || 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(“Puzzle loaded successfully!”); triggerHaptic(‘tap’); playSound(‘cat’);
        }

        let toastTimeout;
        function showToast(msg) {
            toastEl.textContent = msg; toastEl.classList.add(‘show’);
            clearTimeout(toastTimeout);
            toastTimeout = setTimeout(() => { toastEl.classList.remove(‘show’); }, 4000);
        }
    </script>
</body>
</html>

Leave a Reply