Maze solver, wrapped to solids

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0, user-scalable=no”>
    <title>3D Maze Mapper Deluxe / Solver</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #050505;
            font-family: ‘Segoe UI’, Roboto, Helvetica, Arial, sans-serif;
            color: #ffffff;
            touch-action: none;
        }

        #canvas-container {
            width: 100vw;
            height: 100vh;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
        }

        #ui-layer {
            position: absolute;
            bottom: 0;
            left: 0;
            width: 100%;
            z-index: 2;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding-bottom: max(20px, env(safe-area-inset-bottom));
            background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
        }

        .controls {
            pointer-events: auto;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            justify-content: center;
            padding: 10px;
            width: 95%;
            max-width: 600px;
        }

        .control-group {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }

        label {
            font-size: 0.7rem;
            text-transform: uppercase;
            color: #888;
            font-weight: bold;
            margin-left: 2px;
        }

        select, button {
            background: rgba(40, 40, 50, 0.9);
            color: white;
            border: 1px solid rgba(255, 255, 255, 0.2);
            padding: 12px;
            border-radius: 12px;
            font-size: 0.9rem;
            outline: none;
            backdrop-filter: blur(5px);
            -webkit-appearance: none;
        }

        button {
            font-weight: 700;
            text-transform: uppercase;
            cursor: pointer;
            transition: transform 0.1s, background 0.2s;
            display: flex;
            align-items: center;
            justify-content: center;
            min-width: 80px;
        }

        button:active {
            transform: scale(0.96);
        }

        /* Toggle Button State */
        button.active {
            box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
            border-color: #fff;
        }

        #header {
            position: absolute;
            top: 20px;
            left: 0;
            width: 100%;
            text-align: center;
            z-index: 2;
            pointer-events: none;
            text-shadow: 0 2px 4px rgba(0,0,0,0.8);
        }

        h1 {
            margin: 0;
            font-size: 1.8rem;
            font-weight: 800;
            letter-spacing: 1px;
        }

        p {
            margin: 5px 0 0 0;
            font-size: 0.8rem;
            opacity: 0.7;
        }
    </style>
    <script src=”https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js”></script>
</head>
<body>

    <div id=”header”>
        <h1 id=”titleText”>MAZE SOLVER</h1>
        <p>Drag to Rotate &bull; Pinch to Zoom</p>
    </div>

    <div id=”canvas-container”></div>

    <div id=”ui-layer”>
        <div class=”controls”>
            <div class=”control-group”>
                <label>Shape</label>
                <select id=”shapeSelect”>
                    <option value=”cube”>Cube</option>
                    <option value=”sphere”>Sphere</option>
                    <option value=”cylinder”>Cylinder</option>
                    <option value=”torus”>Donut</option>
                    <option value=”knot” selected>Pretzel</option>
                    <option value=”dodecahedron”>D20</option>
                    <option value=”cone”>Cone</option>
                </select>
            </div>

            <div class=”control-group”>
                <label>Theme</label>
                <select id=”themeSelect”>
                    <option value=”cyberpunk”>Cyberpunk</option>
                    <option value=”matrix”>Matrix</option>
                    <option value=”magma”>Magma</option>
                    <option value=”ice”>Ice</option>
                    <option value=”gold”>Gold</option>
                </select>
            </div>

            <div class=”control-group”>
                <label>Complexity</label>
                <select id=”difficultySelect”>
                    <option value=”15″>Easy</option>
                    <option value=”25″ selected>Medium</option>
                    <option value=”45″>Hard</option>
                    <option value=”65″>Extreme</option>
                </select>
            </div>
           
            <div class=”control-group”>
                <label>Action</label>
                <div style=”display:flex; gap:5px;”>
                    <button id=”regenerateBtn” style=”flex:1;”>New</button>
                    <button id=”solveBtn” style=”flex:1;”>Solve</button>
                </div>
            </div>
        </div>
    </div>

    <script>
        // — CONFIGURATION —
        const THEMES = {
            cyberpunk: { bg: ‘#050505’, path: ‘#00ffcc’, wall: ‘#000000’, glow: ‘#00ffcc’, solve: ‘#ff00ff’ },
            matrix:    { bg: ‘#000000’, path: ‘#00cc00’, wall: ‘#001100’, glow: ‘#00ff00’, solve: ‘#ffffff’ },
            magma:     { bg: ‘#1a0000’, path: ‘#ff3300’, wall: ‘#2a0000’, glow: ‘#ff4400’, solve: ‘#00ffff’ },
            ice:       { bg: ‘#000510’, path: ‘#0066cc’, wall: ‘#001122’, glow: ‘#0088ff’, solve: ‘#ffcc00’ },
            gold:      { bg: ‘#1a1000’, path: ‘#cca000’, wall: ‘#2a2000’, glow: ‘#ffaa00’, solve: ‘#9933ff’ }
        };

        let currentSettings = {
            shape: ‘knot’,
            theme: ‘cyberpunk’,
            difficulty: 25,
            showSolution: false
        };

        // Global State for Maze Data
        let mazeData = {
            grid: [],
            solution: [],
            cols: 0,
            rows: 0
        };

        // — 1. MAZE ALGORITHMS —
       
        // Generates the Grid and the Solution Path
        function generateMazeData(cols, rows) {
            // Ensure odd dimensions
            if (cols % 2 === 0) cols++;
            if (rows % 2 === 0) rows++;

            // Initialize Grid (1 = Wall, 0 = Path)
            let grid = Array(rows).fill().map(() => Array(cols).fill(1));

            // Recursive Backtracking to carve maze
            const directions = [[0,-2], [2,0], [0,2], [-2,0]];
           
            function carve(x, y) {
                grid[y][x] = 0;
                const dirs = […directions].sort(() => Math.random() – 0.5);

                for (let [dx, dy] of dirs) {
                    const nx = x + dx, ny = y + dy;
                    if (nx > 0 && nx < cols-1 && ny > 0 && ny < rows-1 && grid[ny][nx] === 1) {
                        grid[y + dy/2][x + dx/2] = 0;
                        carve(nx, ny);
                    }
                }
            }

            // Start carving from top-left
            carve(1, 1);

            // Force end point at bottom right
            grid[rows-2][cols-2] = 0;

            // Solve it immediately
            const solution = solveMaze(grid, cols, rows);

            return { grid, solution, cols, rows };
        }

        function solveMaze(grid, cols, rows) {
            const start = {x: 1, y: 1};
            const end = {x: cols-2, y: rows-2};
           
            let stack = [start];
            let visited = new Set();
            let parentMap = new Map();
            visited.add(`${start.x},${start.y}`);

            // DFS Solver
            while (stack.length > 0) {
                let current = stack.pop();
               
                if (current.x === end.x && current.y === end.y) {
                    // Reconstruct path
                    let path = [];
                    let currKey = `${end.x},${end.y}`;
                    while (currKey) {
                        const [cx, cy] = currKey.split(‘,’).map(Number);
                        path.push({x: cx, y: cy});
                        currKey = parentMap.get(currKey);
                    }
                    return path.reverse();
                }

                // Check neighbors (Up, Right, Down, Left)
                const neighbors = [
                    {x: current.x, y: current.y – 1},
                    {x: current.x + 1, y: current.y},
                    {x: current.x, y: current.y + 1},
                    {x: current.x – 1, y: current.y}
                ];

                for (let n of neighbors) {
                    if (n.x >= 0 && n.x < cols && n.y >= 0 && n.y < rows && grid[n.y][n.x] === 0) {
                        const key = `${n.x},${n.y}`;
                        if (!visited.has(key)) {
                            visited.add(key);
                            parentMap.set(key, `${current.x},${current.y}`);
                            stack.push(n);
                        }
                    }
                }
            }
            return []; // No path found
        }

        // Draws the current Maze Data to a Canvas
        function drawMazeToCanvas(size, themeName, showSol) {
            const { grid, solution, cols, rows } = mazeData;
            const theme = THEMES[themeName];
           
            const canvas = document.createElement(‘canvas’);
            canvas.width = size;
            canvas.height = size;
            const ctx = canvas.getContext(‘2d’);

            const cellW = size / cols;
            const cellH = size / rows;

            // 1. Draw Background (Walls)
            ctx.fillStyle = theme.wall;
            ctx.fillRect(0, 0, size, size);

            // 2. Draw Paths
            ctx.shadowBlur = 0; // Disable shadow for base path for crispness
            ctx.fillStyle = theme.path;

            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    if (grid[y][x] === 0) {
                        ctx.fillRect(x * cellW, y * cellH, cellW + 1, cellH + 1);
                    }
                }
            }
           
            // 3. Draw Glow Overlay (Optimization: Draw paths again with blur)
            ctx.shadowBlur = 15;
            ctx.shadowColor = theme.glow;
            ctx.fillStyle = theme.path;
            // Only draw a subset or simpler representation for glow if performance needed
            // But for textures, we can afford it once per generation.

            // 4. Draw Solution (if enabled)
            if (showSol && solution.length > 0) {
                ctx.beginPath();
                ctx.strokeStyle = theme.solve;
                ctx.lineWidth = cellW * 0.4; // 40% of cell width
                ctx.lineCap = ’round’;
                ctx.lineJoin = ’round’;
               
                // Override shadow for the solution line to make it pop
                ctx.shadowColor = theme.solve;
                ctx.shadowBlur = 10;

                // Move to start center
                const startX = solution[0].x * cellW + cellW/2;
                const startY = solution[0].y * cellH + cellH/2;
                ctx.moveTo(startX, startY);

                for (let i = 1; i < solution.length; i++) {
                    const px = solution[i].x * cellW + cellW/2;
                    const py = solution[i].y * cellH + cellH/2;
                    ctx.lineTo(px, py);
                }
                ctx.stroke();
            }

            // 5. Draw Border
            ctx.shadowBlur = 0;
            ctx.strokeStyle = theme.path;
            ctx.lineWidth = cellW;
            ctx.strokeRect(0, 0, size, size);

            return canvas;
        }

        // — 2. THREE.JS SCENE SETUP —

        const container = document.getElementById(‘canvas-container’);
        let scene, camera, renderer, mesh, material;
        let texture;
        let isDragging = false;
        let previousMousePosition = { x: 0, y: 0 };
        let autoRotate = true;
        let ambientLight, pointLight;

        function init() {
            scene = new THREE.Scene();
            scene.fog = new THREE.FogExp2(0x000000, 0.02);

            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.z = 3.5;

            renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            container.appendChild(renderer.domElement);

            // Lighting
            ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
            scene.add(ambientLight);

            pointLight = new THREE.PointLight(0xffffff, 1.5, 50);
            pointLight.position.set(5, 5, 10);
            scene.add(pointLight);

            // Input Listeners
            const canvas = renderer.domElement;
            canvas.addEventListener(‘mousedown’, startDrag);
            canvas.addEventListener(‘mousemove’, onDrag);
            canvas.addEventListener(‘mouseup’, endDrag);
            canvas.addEventListener(‘touchstart’, (e) => startDrag(e.touches[0]), {passive: false});
            canvas.addEventListener(‘touchmove’, (e) => { e.preventDefault(); onDrag(e.touches[0]); }, {passive: false});
            canvas.addEventListener(‘touchend’, endDrag);
           
            // Initial Maze Generation
            generateNewMaze();
            updateVisualization();
           
            window.addEventListener(‘resize’, onWindowResize);
            requestAnimationFrame(animate);
        }

        function createGeometry(type) {
            switch(type) {
                case ‘cube’: return new THREE.BoxGeometry(1.8, 1.8, 1.8);
                case ‘sphere’: return new THREE.SphereGeometry(1.4, 64, 64);
                case ‘cylinder’: return new THREE.CylinderGeometry(1, 1, 2.5, 32);
                case ‘cone’: return new THREE.ConeGeometry(1.2, 2.5, 32);
                case ‘torus’: return new THREE.TorusGeometry(1.2, 0.5, 30, 60);
                case ‘knot’: return new THREE.TorusKnotGeometry(1, 0.3, 128, 32);
                case ‘dodecahedron’: return new THREE.DodecahedronGeometry(1.3);
                default: return new THREE.BoxGeometry(1.5, 1.5, 1.5);
            }
        }

        // 1. Generate Logical Data
        function generateNewMaze() {
            const difficulty = parseInt(currentSettings.difficulty);
            mazeData = generateMazeData(difficulty, difficulty);
        }

        // 2. Render Texture & Mesh based on Data + Settings
        function updateVisualization() {
            const theme = THEMES[currentSettings.theme];
           
            // UI Styling
            document.body.style.backgroundColor = theme.bg;
            document.querySelector(‘h1’).style.textShadow = `0 0 15px ${theme.glow}`;
            document.querySelector(‘h1’).style.color = theme.path;
           
            const btns = document.querySelectorAll(‘button’);
            btns.forEach(b => {
                b.style.backgroundColor = theme.path;
                b.style.color = (theme.bg === ‘#000000’ || theme.bg === ‘#050505’) ? ‘black’ : ‘white’;
            });
           
            // Toggle Solve Button styling
            const solveBtn = document.getElementById(‘solveBtn’);
            if (currentSettings.showSolution) {
                solveBtn.style.backgroundColor = theme.solve;
                solveBtn.style.color = ‘black’;
                solveBtn.innerText = “Hide”;
            } else {
                solveBtn.innerText = “Solve”;
            }

            scene.fog.color.set(theme.bg);
            renderer.setClearColor(theme.bg);
            pointLight.color.set(theme.glow);

            // Cleanup
            if (mesh) {
                scene.remove(mesh);
                mesh.geometry.dispose();
                mesh.material.dispose();
                if(texture) texture.dispose();
            }

            // Draw Texture
            const mazeCanvas = drawMazeToCanvas(1024, currentSettings.theme, currentSettings.showSolution);
            texture = new THREE.CanvasTexture(mazeCanvas);
           
            texture.wrapS = THREE.RepeatWrapping;
            texture.wrapT = THREE.RepeatWrapping;
           
            if (currentSettings.shape === ‘torus’) texture.repeat.set(6, 2);
            else if (currentSettings.shape === ‘knot’) texture.repeat.set(8, 1);
            else if (currentSettings.shape === ‘cylinder’) texture.repeat.set(2, 1);
            else texture.repeat.set(1, 1);

            material = new THREE.MeshStandardMaterial({
                map: texture,
                emissive: 0xffffff,
                emissiveMap: texture,
                emissiveIntensity: 0.9,
                roughness: 0.3,
                metalness: 0.6,
                flatShading: currentSettings.shape === ‘dodecahedron’
            });

            // Adjust emissive color for solution contrast
            // We set the base emissive to white so the texture colors (which include the solution color) pop correctly
           
            mesh = new THREE.Mesh(createGeometry(currentSettings.shape), material);
            scene.add(mesh);
           
            if (!isDragging) autoRotate = true;
        }

        // — 3. EVENT HANDLERS —

        function startDrag(e) {
            isDragging = true;
            autoRotate = false;
            previousMousePosition = { x: e.clientX, y: e.clientY };
        }

        function onDrag(e) {
            if (!isDragging || !mesh) return;
            const delta = {
                x: e.clientX – previousMousePosition.x,
                y: e.clientY – previousMousePosition.y
            };
            mesh.rotation.y += delta.x * 0.005;
            mesh.rotation.x += delta.y * 0.005;
            previousMousePosition = { x: e.clientX, y: e.clientY };
        }

        function endDrag() {
            isDragging = false;
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function animate() {
            requestAnimationFrame(animate);
            if (mesh && autoRotate && !isDragging) {
                mesh.rotation.x += 0.0015;
                mesh.rotation.y += 0.0025;
            }
            renderer.render(scene, camera);
        }

        // Buttons
        document.getElementById(‘regenerateBtn’).addEventListener(‘click’, () => {
            currentSettings.showSolution = false; // Reset solver on new maze
            generateNewMaze();
            updateVisualization();
        });

        document.getElementById(‘solveBtn’).addEventListener(‘click’, () => {
            currentSettings.showSolution = !currentSettings.showSolution;
            updateVisualization(); // Re-render texture without regenerating grid
        });

        // Dropdowns
        document.getElementById(‘shapeSelect’).addEventListener(‘change’, (e) => {
            currentSettings.shape = e.target.value;
            updateVisualization();
        });

        document.getElementById(‘themeSelect’).addEventListener(‘change’, (e) => {
            currentSettings.theme = e.target.value;
            updateVisualization();
        });

        document.getElementById(‘difficultySelect’).addEventListener(‘change’, (e) => {
            currentSettings.difficulty = e.target.value;
            generateNewMaze(); // Difficulty requires new grid
            updateVisualization();
        });

        // Init
        init();

    </script>
</body>
</html>

Leave a Reply