🎮ArcadeLab

🐭 Extreme Chase · Smart Cats

by SonicBear35
883 lines32.6 KB
▶ Play
<!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>🐭 Extreme Chase · Smart Cats</title>
    <style>
        * {
            box-sizing: border-box;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }
        body {
            margin: 0;
            min-height: 100vh;
            background: #0b1219;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: system-ui, 'Segoe UI', Roboto, sans-serif;
            touch-action: none;
        }
        .game-wrapper {
            background: #1f2a36;
            padding: 18px 14px 22px;
            border-radius: 48px 48px 32px 32px;
            box-shadow: 0 16px 30px rgba(0,0,0,0.8);
            max-width: 520px;
            width: 100%;
            margin: 10px;
        }
        .status-bar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 0 6px 14px 6px;
            color: #f2e8d5;
            font-weight: bold;
            font-size: 1.1rem;
            flex-wrap: wrap;
            gap: 6px 10px;
        }
        .status-item {
            background: #0f1a24;
            padding: 4px 14px;
            border-radius: 40px;
            box-shadow: inset 0 2px 4px #2d4155;
            display: flex;
            align-items: center;
            gap: 6px;
        }
        .status-item span {
            color: #f7d44a;
        }
        canvas {
            display: block;
            width: 100%;
            aspect-ratio: 1 / 1;
            background: #131f2b;
            border-radius: 28px;
            box-shadow: inset 0 0 0 2px #2f4459, 0 8px 0 #0d141c;
            touch-action: none;
            cursor: grab;
        }
        .controls {
            display: flex;
            justify-content: center;
            gap: 12px;
            margin-top: 16px;
            padding: 4px 0;
        }
        .ctrl-btn {
            background: #2a3e50;
            border: none;
            border-bottom: 6px solid #111d28;
            color: #f0e6d0;
            font-size: 1.4rem;
            padding: 10px 18px;
            border-radius: 40px;
            box-shadow: 0 4px 0 #0a1118;
            transition: all 0.06s ease;
            touch-action: manipulation;
            cursor: pointer;
            font-weight: bold;
            min-width: 80px;
            text-align: center;
        }
        .ctrl-btn:active {
            transform: translateY(4px);
            border-bottom-width: 2px;
            background: #1d2f3d;
        }
        .reset-area {
            display: flex;
            justify-content: center;
            margin-top: 12px;
            gap: 12px;
        }
        .action-btn {
            background: #3d2c2c;
            border-bottom: 6px solid #201717;
            padding: 8px 20px;
            font-size: 1.1rem;
            min-width: 100px;
            color: #ffdbb5;
        }
        .action-btn:active {
            transform: translateY(4px);
            border-bottom-width: 2px;
        }
        .message {
            text-align: center;
            font-size: 1.3rem;
            font-weight: bold;
            color: #ffd966;
            margin-top: 6px;
            min-height: 2.4rem;
            padding: 0 6px;
        }
        .swipe-hint {
            color: #8aaec9;
            font-size: 0.9rem;
            text-align: center;
            margin-top: 4px;
        }
        @media (max-width: 480px) {
            .ctrl-btn { padding: 6px 12px; min-width: 60px; font-size: 1.2rem; }
            .status-bar { font-size: 0.85rem; }
        }
    </style>
</head>
<body>
<div class="game-wrapper">
    <div class="status-bar">
        <div class="status-item">🏆 <span id="levelDisplay">1</span></div>
        <div class="status-item">⏱️ <span id="timerDisplay">0:45</span></div>
        <div class="status-item">🐱 <span id="catCountDisplay">1</span></div>
        <div class="status-item">🍎 <span id="pelletCount">0</span></div>
    </div>
    <canvas id="gameCanvas" width="400" height="400"></canvas>
    <div class="controls">
        <button class="ctrl-btn" id="resetBtn">↺ restart</button>
        <button class="ctrl-btn" id="nextBtn">▶ next</button>
    </div>
    <div id="messageBox" class="message">👆 swipe to move · survive the time!</div>
    <div class="swipe-hint">⬆️ ⬇️ ⬅️ ➡️ swipe on the maze</div>
</div>
<script>
    (function(){
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const levelSpan = document.getElementById('levelDisplay');
        const timerSpan = document.getElementById('timerDisplay');
        const catCountSpan = document.getElementById('catCountDisplay');
        const pelletCountSpan = document.getElementById('pelletCount');
        const messageBox = document.getElementById('messageBox');

        const GRID_SIZE = 21;
        const CELL_SIZE = canvas.width / GRID_SIZE;

        // ---------- maze: open walls with dead ends ----------
        let maze = [];
        function generateMaze(rows, cols) {
            const m = [];
            for (let r = 0; r < rows; r++) {
                const row = [];
                for (let c = 0; c < cols; c++) {
                    if (r === 0 || r === rows-1 || c === 0 || c === cols-1) {
                        row.push(1);
                    } else {
                        row.push(1);
                    }
                }
                m.push(row);
            }

            function carve(r, c) {
                m[r][c] = 0;
                const dirs = [
                    [0, -2], [0, 2], [-2, 0], [2, 0]
                ];
                for (let i = dirs.length - 1; i > 0; i--) {
                    const j = Math.floor(Math.random() * (i + 1));
                    [dirs[i], dirs[j]] = [dirs[j], dirs[i]];
                }
                for (let d of dirs) {
                    const nr = r + d[0];
                    const nc = c + d[1];
                    if (nr > 0 && nr < rows-1 && nc > 0 && nc < cols-1 && m[nr][nc] === 1) {
                        m[r + d[0]/2][c + d[1]/2] = 0;
                        carve(nr, nc);
                    }
                }
            }

            carve(1, 1);

            // add dead ends and loops
            for (let i = 0; i < 40; i++) {
                const r = 1 + Math.floor(Math.random() * (rows-2));
                const c = 1 + Math.floor(Math.random() * (cols-2));
                if (m[r][c] === 1) {
                    m[r][c] = 0;
                }
            }

            // ensure start positions open
            m[10][10] = 0;
            m[5][5] = 0;
            m[15][15] = 0;
            m[10][5] = 0;
            m[5][10] = 0;
            m[15][10] = 0;
            m[10][15] = 0;

            return m;
        }

        // ---------- game state ----------
        let level = 1;
        let timeLeft = 45;
        let maxTime = 45;
        let gameActive = true;
        let winFlag = false;
        let timerInterval = null;
        let catMoveInterval = null;
        let pelletSpawnInterval = null;
        let celebrationTimeout = null;

        let player = { x: 10, y: 10 };
        let cats = [];
        let pellets = [];
        let showCelebration = false;

        // swipe detection
        let touchStartX = 0, touchStartY = 0;
        let isSwiping = false;

        // ---------- helpers ----------
        function randomInt(max) { return Math.floor(Math.random() * max); }

        function isWalkable(x, y) {
            if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return false;
            return maze[y][x] === 0;
        }

        // ---------- spawn cats ----------
        function spawnCats(count) {
            const newCats = [];
            const occupied = new Set();
            occupied.add(`${player.x},${player.y}`);
            for (let i = 0; i < count * 5 && newCats.length < count; i++) {
                const x = 1 + randomInt(GRID_SIZE-2);
                const y = 1 + randomInt(GRID_SIZE-2);
                if (isWalkable(x, y) && !occupied.has(`${x},${y}`) && !(x === player.x && y === player.y)) {
                    occupied.add(`${x},${y}`);
                    newCats.push({ x, y, size: 1.0, pelletsEaten: 0, happy: false });
                }
            }
            while (newCats.length < count) {
                const x = 1 + randomInt(GRID_SIZE-2);
                const y = 1 + randomInt(GRID_SIZE-2);
                if (isWalkable(x, y) && !occupied.has(`${x},${y}`) && !(x === player.x && y === player.y)) {
                    occupied.add(`${x},${y}`);
                    newCats.push({ x, y, size: 1.0, pelletsEaten: 0, happy: false });
                }
            }
            return newCats;
        }

        // ---------- pellet management ----------
        function spawnPellets(count) {
            const newPellets = [];
            const occupied = new Set();
            occupied.add(`${player.x},${player.y}`);
            for (let cat of cats) {
                occupied.add(`${cat.x},${cat.y}`);
            }
            for (let p of pellets) {
                occupied.add(`${p.x},${p.y}`);
            }
            
            for (let i = 0; i < count * 5 && newPellets.length < count; i++) {
                const x = 1 + randomInt(GRID_SIZE-2);
                const y = 1 + randomInt(GRID_SIZE-2);
                if (isWalkable(x, y) && !occupied.has(`${x},${y}`)) {
                    occupied.add(`${x},${y}`);
                    newPellets.push({ x, y, eaten: false });
                }
            }
            while (newPellets.length < count) {
                const x = 1 + randomInt(GRID_SIZE-2);
                const y = 1 + randomInt(GRID_SIZE-2);
                if (isWalkable(x, y) && !occupied.has(`${x},${y}`)) {
                    occupied.add(`${x},${y}`);
                    newPellets.push({ x, y, eaten: false });
                }
            }
            return newPellets;
        }

        function formatTime(sec) {
            const m = Math.floor(sec / 60);
            const s = Math.floor(sec % 60);
            return `${m}:${s.toString().padStart(2, '0')}`;
        }

        // ---------- level setup ----------
        function setupLevel(lvl) {
            maze = generateMaze(GRID_SIZE, GRID_SIZE);
            maze[10][10] = 0;
            let baseTime = 45 + (lvl - 1) * 10;
            if (baseTime > 300) baseTime = 300;
            maxTime = baseTime;
            timeLeft = maxTime;

            // More cats at higher levels: level 1=1, level 2=2, level 3=3, etc.
            let catCount = lvl;
            if (catCount > 12) catCount = 12;

            player.x = 10; player.y = 10;
            cats = spawnCats(catCount);
            pellets = spawnPellets(15 + level * 4);

            winFlag = false;
            gameActive = true;
            showCelebration = false;
            updateUI();
            messageBox.textContent = `🐀 level ${lvl} · survive until time ends!`;
            messageBox.style.color = '#ffd966';

            if (timerInterval) {
                clearInterval(timerInterval);
                timerInterval = null;
            }
            timerInterval = setInterval(() => {
                if (!gameActive) return;
                timeLeft--;
                timerSpan.textContent = formatTime(timeLeft);
                if (timeLeft <= 0) {
                    timeLeft = 0;
                    timerSpan.textContent = '0:00';
                    if (gameActive) {
                        gameActive = false;
                        winFlag = true;
                        messageBox.textContent = `🎉 YOU SURVIVED! level ${level} completed 🎉`;
                        messageBox.style.color = '#7bed9f';
                        clearInterval(timerInterval);
                        timerInterval = null;
                        if (catMoveInterval) {
                            clearInterval(catMoveInterval);
                            catMoveInterval = null;
                        }
                        if (pelletSpawnInterval) {
                            clearInterval(pelletSpawnInterval);
                            pelletSpawnInterval = null;
                        }
                        drawGame();
                    }
                }
                drawGame();
            }, 1000);

            // Cat movement: extremely fast and smart
            if (catMoveInterval) {
                clearInterval(catMoveInterval);
                catMoveInterval = null;
            }
            // Very fast: level 1=350ms, level 12=80ms
            const speed = Math.max(80, 350 - (level - 1) * 25);
            catMoveInterval = setInterval(() => {
                if (gameActive) moveCats();
            }, speed);

            // Spawn new pellets frequently
            if (pelletSpawnInterval) {
                clearInterval(pelletSpawnInterval);
                pelletSpawnInterval = null;
            }
            pelletSpawnInterval = setInterval(() => {
                if (!gameActive) return;
                const count = 3 + Math.floor(Math.random() * 4);
                const newPellets = spawnPellets(count);
                pellets = pellets.concat(newPellets);
                updateUI();
                drawGame();
            }, 6000);

            drawGame();
        }

        function updateUI() {
            levelSpan.textContent = level;
            catCountSpan.textContent = cats.length;
            timerSpan.textContent = formatTime(timeLeft);
            const remaining = pellets.filter(p => !p.eaten).length;
            pelletCountSpan.textContent = remaining;
        }

        // ---------- win / lose ----------
        function loseGame() {
            if (!gameActive) return;
            gameActive = false;
            winFlag = false;
            if (timerInterval) {
                clearInterval(timerInterval);
                timerInterval = null;
            }
            if (catMoveInterval) {
                clearInterval(catMoveInterval);
                catMoveInterval = null;
            }
            if (pelletSpawnInterval) {
                clearInterval(pelletSpawnInterval);
                pelletSpawnInterval = null;
            }
            
            // Set cats to happy state
            for (let cat of cats) {
                cat.happy = true;
            }
            showCelebration = true;
            messageBox.textContent = `😸 CATS ARE HAPPY! They caught you! 😸`;
            messageBox.style.color = '#ff6b6b';
            drawGame();
            
            // Clear celebration after 2 seconds
            if (celebrationTimeout) {
                clearTimeout(celebrationTimeout);
            }
            celebrationTimeout = setTimeout(() => {
                showCelebration = false;
                for (let cat of cats) {
                    cat.happy = false;
                }
                drawGame();
            }, 2500);
        }

        // ---------- player movement (swipe) ----------
        function tryMovePlayer(dx, dy) {
            if (!gameActive) return false;
            const nx = player.x + dx;
            const ny = player.y + dy;
            if (!isWalkable(nx, ny)) return false;
            player.x = nx;
            player.y = ny;

            for (let cat of cats) {
                if (cat.x === player.x && cat.y === player.y) {
                    loseGame();
                    drawGame();
                    return true;
                }
            }
            drawGame();
            return true;
        }

        // ---------- cat AI: EXTREMELY SMART pathfinding ----------
        function moveCats() {
            if (!gameActive) return;

            // First, cats eat any pellets they're on
            for (let cat of cats) {
                for (let p of pellets) {
                    if (!p.eaten && p.x === cat.x && p.y === cat.y) {
                        p.eaten = true;
                        cat.pelletsEaten++;
                        cat.size = Math.min(4.0, 1.0 + cat.pelletsEaten * 0.12);
                        updateUI();
                        break;
                    }
                }
            }

            // Smart chase using BFS pathfinding for each cat
            for (let cat of cats) {
                // BFS to find shortest path to player
                const queue = [{ x: cat.x, y: cat.y, path: [] }];
                const visited = new Set();
                visited.add(`${cat.x},${cat.y}`);
                let bestPath = null;
                let found = false;
                
                while (queue.length > 0 && !found) {
                    const current = queue.shift();
                    const dirs = [
                        { dx: 0, dy: -1 }, { dx: 0, dy: 1 },
                        { dx: -1, dy: 0 }, { dx: 1, dy: 0 }
                    ];
                    // Shuffle for variety
                    for (let i = dirs.length - 1; i > 0; i--) {
                        const j = Math.floor(Math.random() * (i + 1));
                        [dirs[i], dirs[j]] = [dirs[j], dirs[i]];
                    }
                    
                    for (let d of dirs) {
                        const nx = current.x + d.dx;
                        const ny = current.y + d.dy;
                        const key = `${nx},${ny}`;
                        
                        if (nx === player.x && ny === player.y) {
                            // Found player!
                            bestPath = [...current.path, { dx: d.dx, dy: d.dy }];
                            found = true;
                            break;
                        }
                        
                        if (isWalkable(nx, ny) && !visited.has(key)) {
                            // Check if another cat is there
                            let blocked = false;
                            for (let other of cats) {
                                if (other === cat) continue;
                                if (other.x === nx && other.y === ny) { blocked = true; break; }
                            }
                            if (!blocked) {
                                visited.add(key);
                                queue.push({ 
                                    x: nx, 
                                    y: ny, 
                                    path: [...current.path, { dx: d.dx, dy: d.dy }]
                                });
                            }
                        }
                    }
                }

                // If we found a path, take the first step
                if (bestPath && bestPath.length > 0) {
                    const firstStep = bestPath[0];
                    const nx = cat.x + firstStep.dx;
                    const ny = cat.y + firstStep.dy;
                    
                    // Double-check the move is valid
                    if (isWalkable(nx, ny)) {
                        let blocked = false;
                        for (let other of cats) {
                            if (other === cat) continue;
                            if (other.x === nx && other.y === ny) { blocked = true; break; }
                        }
                        if (!blocked) {
                            cat.x = nx;
                            cat.y = ny;
                            continue;
                        }
                    }
                }

                // Fallback: if no path found, move toward player greedily
                const dirs = [
                    { dx: 0, dy: -1 }, { dx: 0, dy: 1 },
                    { dx: -1, dy: 0 }, { dx: 1, dy: 0 }
                ];
                let validMoves = [];
                for (let d of dirs) {
                    const nx = cat.x + d.dx;
                    const ny = cat.y + d.dy;
                    if (isWalkable(nx, ny)) {
                        let blocked = false;
                        for (let other of cats) {
                            if (other === cat) continue;
                            if (other.x === nx && other.y === ny) { blocked = true; break; }
                        }
                        if (!blocked) {
                            validMoves.push({ nx, ny, dx: d.dx, dy: d.dy });
                        }
                    }
                }
                if (validMoves.length > 0) {
                    validMoves.sort((a, b) => {
                        const da = Math.abs(a.nx - player.x) + Math.abs(a.ny - player.y);
                        const db = Math.abs(b.nx - player.x) + Math.abs(b.ny - player.y);
                        return da - db;
                    });
                    const chosen = validMoves[0];
                    cat.x = chosen.nx;
                    cat.y = chosen.ny;
                }
            }

            // Check collision after all cats move
            for (let cat of cats) {
                if (cat.x === player.x && cat.y === player.y) {
                    loseGame();
                    drawGame();
                    return;
                }
            }
            drawGame();
        }

        // ---------- drawing ----------
        function drawGame() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            const wallColors = [
                '#4a6a7f', '#5d7f8f', '#6a8f9f', '#7f9faf', '#8fafbf',
                '#a0b8c9', '#b0c8d9', '#7a9aad', '#5a7a8f', '#6a8aa0'
            ];

            for (let r = 0; r < GRID_SIZE; r++) {
                for (let c = 0; c < GRID_SIZE; c++) {
                    const x = c * CELL_SIZE, y = r * CELL_SIZE;
                    if (maze[r][c] === 1) {
                        const idx = (r + c) % wallColors.length;
                        ctx.fillStyle = wallColors[idx];
                        ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE);
                        ctx.fillStyle = 'rgba(255,255,255,0.10)';
                        ctx.fillRect(x+2, y+2, CELL_SIZE-4, CELL_SIZE-4);
                    } else {
                        ctx.fillStyle = '#1c2b38';
                        ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE);
                        ctx.strokeStyle = '#2a3d4d';
                        ctx.lineWidth = 0.5;
                        ctx.strokeRect(x, y, CELL_SIZE, CELL_SIZE);
                    }
                }
            }

            // Draw pellets
            for (let p of pellets) {
                if (p.eaten) continue;
                const x = p.x * CELL_SIZE + CELL_SIZE/2;
                const y = p.y * CELL_SIZE + CELL_SIZE/2;
                ctx.beginPath();
                ctx.arc(x, y, CELL_SIZE/6, 0, Math.PI*2);
                ctx.fillStyle = '#ff6b6b';
                ctx.shadowColor = '#ff6b6b';
                ctx.shadowBlur = 12;
                ctx.fill();
                ctx.shadowBlur = 0;
            }

            // Draw cats
            for (let cat of cats) {
                const x = cat.x * CELL_SIZE, y = cat.y * CELL_SIZE;
                const sizeMultiplier = cat.size || 1.0;
                const radius = (CELL_SIZE/2.2) * sizeMultiplier;
                const isHappy = cat.happy || showCelebration;
                
                ctx.shadowColor = isHappy ? '#ffdd44' : '#ff884d';
                ctx.shadowBlur = isHappy ? 30 : 16;
                
                // Body
                ctx.fillStyle = isHappy ? '#ffcc00' : '#e67e22';
                ctx.beginPath();
                ctx.arc(x + CELL_SIZE/2, y + CELL_SIZE/2, radius, 0, Math.PI*2);
                ctx.fill();
                
                // Ears
                const earOffset = 4 * sizeMultiplier;
                ctx.fillStyle = isHappy ? '#ff9900' : '#d35400';
                ctx.beginPath();
                ctx.moveTo(x + earOffset, y + earOffset);
                ctx.lineTo(x + earOffset*3, y - earOffset);
                ctx.lineTo(x + earOffset*5, y + earOffset*1.5);
                ctx.fill();
                ctx.beginPath();
                ctx.moveTo(x + CELL_SIZE - earOffset, y + earOffset);
                ctx.lineTo(x + CELL_SIZE - earOffset*3, y - earOffset);
                ctx.lineTo(x + CELL_SIZE - earOffset*5, y + earOffset*1.5);
                ctx.fill();
                
                // Eyes
                const eyeSize = 4 + (cat.pelletsEaten * 0.3);
                if (isHappy) {
                    // Happy eyes = curved (^_^)
                    ctx.fillStyle = '#1a0000';
                    ctx.shadowBlur = 0;
                    ctx.beginPath();
                    ctx.arc(x + CELL_SIZE/2 - 6 - (sizeMultiplier-1)*2, y + CELL_SIZE/2 - 3, eyeSize*0.7, 0, Math.PI);
                    ctx.fill();
                    ctx.beginPath();
                    ctx.arc(x + CELL_SIZE/2 + 6 + (sizeMultiplier-1)*2, y + CELL_SIZE/2 - 3, eyeSize*0.7, 0, Math.PI);
                    ctx.fill();
                    // Happy mouth
                    ctx.strokeStyle = '#1a0000';
                    ctx.lineWidth = 2;
                    ctx.beginPath();
                    ctx.arc(x + CELL_SIZE/2, y + CELL_SIZE/2 + 6, 8, 0, Math.PI);
                    ctx.stroke();
                } else {
                    // Angry glowing red eyes
                    ctx.fillStyle = '#ff0000';
                    ctx.shadowColor = '#ff0000';
                    ctx.shadowBlur = 25;
                    ctx.beginPath();
                    ctx.arc(x + CELL_SIZE/2 - 6 - (sizeMultiplier-1)*2, y + CELL_SIZE/2 - 3, eyeSize, 0, Math.PI*2);
                    ctx.fill();
                    ctx.beginPath();
                    ctx.arc(x + CELL_SIZE/2 + 6 + (sizeMultiplier-1)*2, y + CELL_SIZE/2 - 3, eyeSize, 0, Math.PI*2);
                    ctx.fill();
                    // Pupils
                    ctx.fillStyle = '#1a0000';
                    ctx.shadowBlur = 0;
                    ctx.beginPath();
                    ctx.arc(x + CELL_SIZE/2 - 4 - (sizeMultiplier-1)*2, y + CELL_SIZE/2 - 2, eyeSize*0.5, 0, Math.PI*2);
                    ctx.fill();
                    ctx.beginPath();
                    ctx.arc(x + CELL_SIZE/2 + 8 + (sizeMultiplier-1)*2, y + CELL_SIZE/2 - 2, eyeSize*0.5, 0, Math.PI*2);
                    ctx.fill();
                }
                ctx.shadowBlur = 0;
            }

            // Draw player (rat)
            const px = player.x * CELL_SIZE, py = player.y * CELL_SIZE;
            ctx.shadowColor = '#b0b0b0';
            ctx.shadowBlur = 14;
            ctx.fillStyle = '#b0b0b0';
            ctx.beginPath();
            ctx.ellipse(px + CELL_SIZE/2, py + CELL_SIZE/2, CELL_SIZE/2.3, CELL_SIZE/2.8, 0, 0, Math.PI*2);
            ctx.fill();
            ctx.strokeStyle = '#8a8a8a';
            ctx.lineWidth = 4;
            ctx.beginPath();
            ctx.moveTo(px+4, py+CELL_SIZE-4);
            ctx.quadraticCurveTo(px-6, py+CELL_SIZE+8, px+2, py+CELL_SIZE+14);
            ctx.stroke();
            ctx.fillStyle = '#d4a0a0';
            ctx.beginPath();
            ctx.arc(px+6, py+6, 5, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(px+CELL_SIZE-6, py+6, 5, 0, Math.PI*2);
            ctx.fill();
            ctx.fillStyle = 'white';
            ctx.beginPath();
            ctx.arc(px+8, py+10, 3, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(px+CELL_SIZE-8, py+10, 3, 0, Math.PI*2);
            ctx.fill();
            ctx.fillStyle = '#1a1a1a';
            ctx.beginPath();
            ctx.arc(px+7, py+9, 1.5, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(px+CELL_SIZE-9, py+9, 1.5, 0, Math.PI*2);
            ctx.fill();
            ctx.shadowBlur = 0;

            if (!gameActive && !showCelebration) {
                ctx.fillStyle = 'rgba(0,0,0,0.45)';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                ctx.font = 'bold 28px system-ui';
                ctx.fillStyle = winFlag ? '#7bed9f' : '#ff6b6b';
                ctx.shadowColor = '#000000';
                ctx.shadowBlur = 20;
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(winFlag ? '✨ SURVIVED ✨' : '☠️ GAME OVER', canvas.width/2, canvas.height/2-8);
                ctx.shadowBlur = 0;
            }
        }

        // ---------- navigation ----------
        function resetGame() {
            if (timerInterval) {
                clearInterval(timerInterval);
                timerInterval = null;
            }
            if (catMoveInterval) {
                clearInterval(catMoveInterval);
                catMoveInterval = null;
            }
            if (pelletSpawnInterval) {
                clearInterval(pelletSpawnInterval);
                pelletSpawnInterval = null;
            }
            if (celebrationTimeout) {
                clearTimeout(celebrationTimeout);
                celebrationTimeout = null;
            }
            showCelebration = false;
            level = 1;
            setupLevel(level);
        }

        function nextLevel() {
            if (!gameActive && winFlag) {
                level++;
                setupLevel(level);
            } else if (!gameActive && !winFlag) {
                setupLevel(level);
            } else {
                messageBox.textContent = '⚡ survive the current level first!';
                setTimeout(() => { messageBox.textContent = `🐀 level ${level} · survive until time ends!`; }, 1200);
            }
        }

        // ---------- swipe controls ----------
        function initSwipe() {
            canvas.addEventListener('touchstart', (e) => {
                e.preventDefault();
                const touch = e.touches[0];
                touchStartX = touch.clientX;
                touchStartY = touch.clientY;
                isSwiping = true;
            }, { passive: false });

            canvas.addEventListener('touchmove', (e) => {
                e.preventDefault();
                if (!isSwiping || !gameActive) return;
                const touch = e.touches[0];
                const dx = touch.clientX - touchStartX;
                const dy = touch.clientY - touchStartY;
                const threshold = 15;
                if (Math.abs(dx) < threshold && Math.abs(dy) < threshold) return;

                let moveX = 0, moveY = 0;
                if (Math.abs(dx) > Math.abs(dy)) {
                    moveX = dx > 0 ? 1 : -1;
                } else {
                    moveY = dy > 0 ? 1 : -1;
                }

                touchStartX = touch.clientX;
                touchStartY = touch.clientY;

                tryMovePlayer(moveX, moveY);
            }, { passive: false });

            canvas.addEventListener('touchend', (e) => {
                e.preventDefault();
                isSwiping = false;
            }, { passive: false });

            // Mouse drag fallback
            let mouseDown = false;
            let mouseStartX = 0, mouseStartY = 0;
            canvas.addEventListener('mousedown', (e) => {
                mouseDown = true;
                mouseStartX = e.clientX;
                mouseStartY = e.clientY;
            });
            canvas.addEventListener('mousemove', (e) => {
                if (!mouseDown || !gameActive) return;
                const dx = e.clientX - mouseStartX;
                const dy = e.clientY - mouseStartY;
                const threshold = 15;
                if (Math.abs(dx) < threshold && Math.abs(dy) < threshold) return;
                let moveX = 0, moveY = 0;
                if (Math.abs(dx) > Math.abs(dy)) {
                    moveX = dx > 0 ? 1 : -1;
                } else {
                    moveY = dy > 0 ? 1 : -1;
                }
                mouseStartX = e.clientX;
                mouseStartY = e.clientY;
                tryMovePlayer(moveX, moveY);
            });
            canvas.addEventListener('mouseup', () => { mouseDown = false; });
            canvas.addEventListener('mouseleave', () => { mouseDown = false; });
        }

        // ---------- button controls ----------
        function initButtons() {
            document.getElementById('resetBtn').addEventListener('click', (e) => {
                e.preventDefault();
                resetGame();
            });
            document.getElementById('nextBtn').addEventListener('click', (e) => {
                e.preventDefault();
                nextLevel();
            });
            canvas.addEventListener('click', () => {
                if (!gameActive && !showCelebration) {
                    if (winFlag) nextLevel();
                    else resetGame();
                }
            });
        }

        // ---------- start ----------
        setupLevel(1);
        initSwipe();
        initButtons();
    })();
</script>
</body>
</html>

Game Source: 🐭 Extreme Chase · Smart Cats

Creator: SonicBear35

Libraries: none

Complexity: complex (883 lines, 32.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: extreme-chase-smart-cats-sonicbear35" to link back to the original. Then publish at arcadelab.ai/publish.