about summary refs log tree commit diff stats
path: root/html
diff options
context:
space:
mode:
Diffstat (limited to 'html')
-rw-r--r--html/bp-sand/index.html164
-rw-r--r--html/broughlike/about.html28
-rw-r--r--html/broughlike/index.html546
-rw-r--r--html/file-system/index.html297
-rw-r--r--html/playground/APL386.ttfbin0 -> 203668 bytes
-rw-r--r--html/playground/index.html243
-rw-r--r--html/playground/little-regex.html728
-rw-r--r--html/playground/regex.html477
-rw-r--r--html/playground/scheme.html533
-rw-r--r--html/schemer/index.html305
-rw-r--r--html/schemer/tls.pdfbin0 -> 2359953 bytes
-rw-r--r--html/squine.html1108
-rw-r--r--html/tuner/index.html193
13 files changed, 4622 insertions, 0 deletions
diff --git a/html/bp-sand/index.html b/html/bp-sand/index.html
new file mode 100644
index 0000000..e34e33c
--- /dev/null
+++ b/html/bp-sand/index.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>BP Sand</title>
+    <style>
+        body, html {
+            margin: 0;
+            padding: 0;
+            overflow: hidden;
+            display: flex;
+            flex-direction: column;
+            height: 100%;
+        }
+        canvas {
+            display: block;
+            width: 100%;
+            background-color: beige;
+        }
+        .controls {
+            display: flex;
+            justify-content: space-around;
+            padding: 10px;
+            background-color: white;
+            box-shadow: 0 -2px 5px rgba(0,0,0,0.1);
+        }
+        input, button {
+            font-size: 1.5rem;
+        }
+    </style>
+</head>
+<body>
+    <canvas id="sandCanvas"></canvas>
+    <div class="controls">
+        <input type="number" id="bpmInput" value="60" min="10" max="300">
+        <button id="toggleButton">Start</button>
+    </div>
+
+    <script>
+        const canvas = document.getElementById('sandCanvas');
+        const ctx = canvas.getContext('2d');
+        const bpmInput = document.getElementById('bpmInput');
+        const toggleButton = document.getElementById('toggleButton');
+        const controls = document.querySelector('.controls');
+
+        let bpm = 60;
+        let isRunning = false;
+        let sandParticles = [];
+        let intervalId;
+
+        // Set canvas size to avoid overlapping with controls
+        function resizeCanvas() {
+            canvas.width = window.innerWidth;
+            canvas.height = window.innerHeight - controls.offsetHeight;
+        }
+
+        window.addEventListener('resize', resizeCanvas);
+        resizeCanvas();  
+
+        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+
+        function playTone() {
+            const oscillator = audioCtx.createOscillator();
+            oscillator.type = 'sine';
+            oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); // 440 Hz tone
+            oscillator.connect(audioCtx.destination);
+            oscillator.start();
+            oscillator.stop(audioCtx.currentTime + 0.1); // Short beep
+        }
+
+        function createSandParticle() {
+            return {
+                x: Math.random() * canvas.width,
+                y: 0,
+                size: 5 + Math.random() * 5, // Vary size a bit
+                velocityY: 0,
+                atRest: false // Track if the particle is at rest
+            };
+        }
+
+        // Update sand particle positions
+        function updateSand() {
+            for (let particle of sandParticles) {
+                if (!particle.atRest) {
+                    particle.velocityY += 0.1;  // Accelerate! Gravity!
+                    particle.y += particle.velocityY;
+
+                    // Check if sand particle has hit the bottom
+                    if (particle.y + particle.size >= canvas.height) {
+                        particle.y = canvas.height - particle.size;
+                        particle.atRest = true; // Mark the particle as at rest
+                    }
+
+                    // Check if sand particle has landed on another particle
+                    for (let otherParticle of sandParticles) {
+                        if (otherParticle !== particle && otherParticle.atRest) {
+                            let distY = otherParticle.y - (particle.y + particle.size);
+                            let distX = Math.abs(otherParticle.x - particle.x);
+                            if (distY <= 0 && distX < particle.size) {
+                                particle.y = otherParticle.y - particle.size;
+                                particle.atRest = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // Draw sand particles on the canvas
+        function drawSand() {
+            ctx.clearRect(0, 0, canvas.width, canvas.height);
+            ctx.fillStyle = 'teal';
+
+            for (let particle of sandParticles) {
+                ctx.beginPath();
+                ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
+                ctx.fill();
+            }
+        }
+
+        // Main loop
+        function gameLoop() {
+            updateSand();
+            drawSand();
+        }
+
+        // Control the BPM and start/stop the loop
+        function toggleBPM() {
+            isRunning = !isRunning;
+            if (isRunning) {
+                let interval = (60 / bpm) * 1000; // Convert BPM to interval in milliseconds
+                intervalId = setInterval(() => {
+                    sandParticles.push(createSandParticle()); // Generate new sand
+                    playTone(); // Play the tone
+                }, interval);
+                toggleButton.textContent = 'Stop';
+            } else {
+                clearInterval(intervalId);
+                toggleButton.textContent = 'Start';
+            }
+        }
+
+        // Event Listeners
+        toggleButton.addEventListener('click', toggleBPM);
+
+        bpmInput.addEventListener('input', (e) => {
+            bpm = parseInt(e.target.value);
+            if (isRunning) {
+                clearInterval(intervalId);
+                toggleBPM();
+            }
+        });
+
+        function animate() {
+            gameLoop();
+            requestAnimationFrame(animate);
+        }
+
+        animate();
+    </script>
+</body>
+</html>
diff --git a/html/broughlike/about.html b/html/broughlike/about.html
new file mode 100644
index 0000000..0811f77
--- /dev/null
+++ b/html/broughlike/about.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>About Eli's Broughlike</title>
+    <meta description="A Broughlike, or something like Flatland.">
+    <style>
+        body {
+            background-color: #f0f0f0;
+            padding: 1em 2em;
+        }
+    </style>
+</head>
+<body>
+    <h1>How to play</h1>
+    <ul>
+        <li>You are the hexagon.</li>
+        <li>Your goal is to reach the triangle. When you reach the triangle you'll be brought to the next level.</li>
+        <li>Enemies are circles. Run into them to battle. Be warned, you'll also take damage.</li>
+        <li>Diamonds add 3 to the amount of damage you can do to an enemy for the level.</li>
+        <li>Pentagons heal you a little bit.</li>
+        <li>You can tell your or an enemy's health by the opacity of the shape. Darker means more health.</li>
+        <li>Arrow keys, WASD, or VIM movement keys to move on a thing with a keyboard, swipe up, down, left or right to move on something with a touchscreen.</li>
+    </ul>
+    <p><a href="index.html">Play the game!</a></p>
+</body>
+</html>
\ No newline at end of file
diff --git a/html/broughlike/index.html b/html/broughlike/index.html
new file mode 100644
index 0000000..e643527
--- /dev/null
+++ b/html/broughlike/index.html
@@ -0,0 +1,546 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Eli's Broughlike</title>
+    <meta description="A Broughlike, or something like Flatland.">
+    <style>
+        body {
+            background-color: #f0f0f0;
+        }
+        canvas {
+            width: 90vw; 
+            height: 90vw; 
+            max-width: 600px; 
+            max-height: 600px; 
+            border: 2px solid black;
+            display: block;
+            margin: 0 auto;
+            background-color: #f0f0f0;
+        }
+    </style>
+</head>
+<body>
+    <p><a href="about.html">About</a></p>
+    <canvas id="gameCanvas"></canvas>
+    <script>
+
+        const COLORS = {
+            grid: '#2d2d2d',
+            walls: '#2d2d2d',
+            exit: 'teal',
+            diamond: 'gold',
+            pentagon: 'blueviolet', 
+            player: 'rgba(0, 237, 209, ',
+            enemy: 'rgba(255, 155, 28, ',
+            combatDotPlayer: '#00edd1',
+            combatDotEnemy: '#ff731c'
+        };
+
+        const GRID_SIZE = 6;
+        const PLAYER_HEALTH = 10;
+        const PLAYER_MAX_HEALTH = 12;
+        const PLAYER_BASE_DAMAGE = 1;
+        const ENEMY_CHASE_DISTANCE = 4;
+        const MAX_ENEMIES_ON_LEVEL = 4;
+        const MAX_ENEMY_HEALTH = 7;
+        const MIN_ENEMY_HEALTH = 2;
+        const WALLS_MIN = 5;
+        const WALLS_MAX = 20;
+        const ITEMS_MIN = 0;
+        const ITEMS_MAX = 3;
+        const DOTS_PER_HIT = 5;
+
+        const canvas = document.getElementById('gameCanvas');
+        const ctx = canvas.getContext('2d');
+        let tileSize = canvas.width / GRID_SIZE;
+
+        const player = {
+            x: 0,
+            y: 0,
+            health: PLAYER_HEALTH,
+            score: 0,
+            damage: PLAYER_BASE_DAMAGE,
+            totalDamageDone: 0,
+            totalDamageTaken: 0,
+            cellsTraveled: 0,
+            killCount: 0,
+            itemsCollected: 0
+        };
+
+        const exit = { x: Math.floor(Math.random() * GRID_SIZE), y: Math.floor(Math.random() * GRID_SIZE) };
+        let walls = [];
+        let enemies = [];
+        let items = [];
+        let combatDots = {};
+
+        function isValidMove(newX, newY) {
+            return (
+                newX >= 0 && newX < GRID_SIZE &&
+                newY >= 0 && newY < GRID_SIZE &&
+                !walls.some(wall => wall.x === newX && wall.y === newY)
+            );
+        }
+
+        function generateExit() {
+            let distance = 0;
+            do {
+                exit.x = Math.floor(Math.random() * GRID_SIZE);
+                exit.y = Math.floor(Math.random() * GRID_SIZE);
+                distance = Math.abs(exit.x - player.x) + Math.abs(exit.y - player.y);
+            } while (distance < 4);
+        }
+
+        function generateEnemies() {
+            enemies = [];
+            const numEnemies = Math.floor(Math.random() * (MAX_ENEMIES_ON_LEVEL + 1)); // Between 0 and the constant value.
+            for (let i = 0; i < numEnemies; i++) {
+                let enemyX, enemyY;
+                do {
+                    enemyX = Math.floor(Math.random() * GRID_SIZE);
+                    enemyY = Math.floor(Math.random() * GRID_SIZE);
+                } while (
+                    (enemyX === player.x && enemyY === player.y) ||
+                    (enemyX === exit.x && enemyY === exit.y) ||
+                    walls.some(wall => wall.x === enemyX && wall.y === enemyY)
+                );
+                enemies.push({
+                    x: enemyX,
+                    y: enemyY,
+                    health: Math.floor(Math.random() * (MAX_ENEMY_HEALTH - MIN_ENEMY_HEALTH + 1)) + MIN_ENEMY_HEALTH
+                });
+            }
+        }
+
+        function generateWalls() {
+            walls = [];
+            let numWalls = Math.floor(Math.random() * (WALLS_MAX - WALLS_MIN + 1)) + WALLS_MIN;
+            while (walls.length < numWalls) {
+                const wallX = Math.floor(Math.random() * GRID_SIZE);
+                const wallY = Math.floor(Math.random() * GRID_SIZE);
+
+                if (
+                    (wallX === player.x && wallY === player.y) ||
+                    (wallX === exit.x && wallY === exit.y) ||
+                    enemies.some(enemy => enemy.x === wallX && enemy.y === wallY) ||
+                    (wallX === 0 && wallY === 0) || // Don't block the player spawn
+                    items.some(item => item.x === wallX && item.y === wallY)
+                ) continue;
+
+                if (!walls.some(wall => wall.x === wallX && wall.y === wallY)) {
+                    walls.push({ x: wallX, y: wallY });
+                }
+            }
+
+            if (!isPassable()) {
+                generateWalls();
+            }
+        }
+
+        function generateItems() {
+            items = [];
+            const numItems = Math.floor(Math.random() * (ITEMS_MAX - ITEMS_MIN + 1)) + ITEMS_MIN;
+            for (let i = 0; i < numItems; i++) {
+                let itemX, itemY;
+                do {
+                    itemX = Math.floor(Math.random() * GRID_SIZE);
+                    itemY = Math.floor(Math.random() * GRID_SIZE);
+                } while (
+                    (itemX === player.x && itemY === player.y) ||
+                    (itemX === exit.x && itemY === exit.y) ||
+                    walls.some(wall => wall.x === itemX && wall.y === itemY) ||
+                    enemies.some(enemy => enemy.x === itemX && enemy.y === itemY)
+                );
+                const itemType = Math.random() < 0.5 ? 'diamond' : 'pentagon'; // 50% chance for each type
+                items.push({ x: itemX, y: itemY, type: itemType });
+            }
+        }
+
+        function isPassable() {
+            const visited = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(false));
+
+            function dfs(x, y) {
+                if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return false;
+                if (visited[x][y]) return false;
+                if (walls.some(wall => wall.x === x && wall.y === y)) return false;
+                visited[x][y] = true;
+                if (x === exit.x && y === exit.y) return true;
+
+                return dfs(x + 1, y) || dfs(x - 1, y) || dfs(x, y + 1) || dfs(x, y - 1);
+            }
+
+            return dfs(player.x, player.y);
+        }
+
+        function drawGrid() {
+            ctx.clearRect(0, 0, canvas.width, canvas.height);
+            ctx.strokeStyle = COLORS.grid;
+            for (let row = 0; row < GRID_SIZE; row++) {
+                for (let col = 0; col < GRID_SIZE; col++) {
+                    ctx.strokeRect(col * tileSize, row * tileSize, tileSize, tileSize);
+                }
+            }
+        }
+
+        function drawExit() {
+            const x = exit.x * tileSize + tileSize / 2;
+            const y = exit.y * tileSize + tileSize / 2;
+            ctx.beginPath();
+            ctx.moveTo(x, y - tileSize / 3);
+            ctx.lineTo(x + tileSize / 3, y + tileSize / 3);
+            ctx.lineTo(x - tileSize / 3, y + tileSize / 3);
+            ctx.closePath();
+            ctx.fillStyle = COLORS.exit;
+            ctx.fill();
+        }
+
+        function drawWalls() {
+            ctx.fillStyle = COLORS.walls;
+            walls.forEach(wall => {
+                ctx.fillRect(wall.x * tileSize, wall.y * tileSize, tileSize, tileSize);
+            });
+        }
+
+        function drawItems() {
+            items.forEach(item => {
+                const x = item.x * tileSize + tileSize / 2;
+                const y = item.y * tileSize + tileSize / 2;
+                ctx.fillStyle = item.type === 'diamond' ? COLORS.diamond : COLORS.pentagon;
+                ctx.beginPath();
+                if (item.type === 'diamond') {
+                    ctx.moveTo(x, y - tileSize / 4);
+                    ctx.lineTo(x + tileSize / 4, y);
+                    ctx.lineTo(x, y + tileSize / 4);
+                    ctx.lineTo(x - tileSize / 4, y);
+                } else {
+                    const sides = 5;
+                    const radius = tileSize / 4;
+                    for (let i = 0; i < sides; i++) {
+                        const angle = (i * 2 * Math.PI) / sides - Math.PI / 2;
+                        const pointX = x + radius * Math.cos(angle);
+                        const pointY = y + radius * Math.sin(angle);
+                        if (i === 0) ctx.moveTo(pointX, pointY);
+                        else ctx.lineTo(pointX, pointY);
+                    }
+                }
+                ctx.closePath();
+                ctx.fill();
+            });
+        }
+
+        function drawCharacterBorder(x, y, radius, damageTaken) {
+            const dashLength = 5;
+            const gapLength = Math.max(1, damageTaken * 2); // More damage, larger gaps
+
+            ctx.lineWidth = 2;
+            ctx.strokeStyle = '#2d2d2d';
+            ctx.setLineDash([dashLength, gapLength]);
+            ctx.beginPath();
+            ctx.arc(x, y, radius, 0, 2 * Math.PI);
+            ctx.stroke();
+            ctx.setLineDash([]); // Reset to a solid line
+        }
+
+        function drawEnemies() {
+            enemies.forEach(enemy => {
+                const x = enemy.x * tileSize + tileSize / 2;
+                const y = enemy.y * tileSize + tileSize / 2;
+                const opacity = enemy.health / MAX_ENEMY_HEALTH; // Opacity based on health
+                const radius = tileSize / 3;
+                const damageTaken = MAX_ENEMY_HEALTH - enemy.health;
+
+                ctx.beginPath();
+                ctx.arc(x, y, radius, 0, 2 * Math.PI);
+                ctx.fillStyle = `${COLORS.enemy}${opacity})`;
+                ctx.fill();
+
+                drawCharacterBorder(x, y, radius, damageTaken);
+            });
+        }
+
+        function drawPlayer() {
+            const x = player.x * tileSize + tileSize / 2;
+            const y = player.y * tileSize + tileSize / 2;
+            const radius = tileSize / 3;
+            const playerOpacity = player.health / PLAYER_HEALTH; // Opacity based on health
+            ctx.beginPath();
+            for (let i = 0; i < 6; i++) {
+                const angle = (Math.PI / 3) * i;
+                const hexX = x + radius * Math.cos(angle);
+                const hexY = y + radius * Math.sin(angle);
+                if (i === 0) {
+                    ctx.moveTo(hexX, hexY);
+                } else {
+                    ctx.lineTo(hexX, hexY);
+                }
+            }
+            ctx.closePath();
+            ctx.fillStyle = `${COLORS.player}${playerOpacity})`;
+            ctx.fill();
+            ctx.lineWidth = 2;
+            ctx.strokeStyle = '#2d2d2d';
+            ctx.stroke();
+        }
+
+        function drawCombatDots() {
+            for (const key in combatDots) {
+                const [cellX, cellY] = key.split(',').map(Number);
+                const dots = combatDots[key];
+                dots.forEach(dot => {
+                    ctx.beginPath();
+                    ctx.arc(cellX * tileSize + dot.x, cellY * tileSize + dot.y, 2, 0, Math.PI * 2);
+                    ctx.fillStyle = dot.color;
+                    ctx.fill();
+                    ctx.closePath();
+                });
+            }
+        }
+
+        function handleItemCollection() {
+            const collectedItem = items.find(item => item.x === player.x && item.y === player.y);
+            if (collectedItem) {
+                player.itemsCollected++;
+                if (collectedItem.type === 'diamond') {
+                    player.damage += 3;
+                    console.log("Collected diamond! +3 damage on this level.");
+                } else if (collectedItem.type === 'pentagon') {
+                    const healAmount = Math.floor(Math.random() * 2) + 1;
+                    player.health = Math.min(player.health + healAmount, PLAYER_MAX_HEALTH);
+                    console.log("Collected pentagon! Healed " + healAmount + " health.");
+                }
+                items = items.filter(item => item !== collectedItem); // Remove collected item
+            }
+        }
+
+        // Function to add dots for damage in combat (including adjacent cells)
+        function addCombatDots(x, y, color) {
+            const cellsToFill = [
+                [x, y],       // Center
+                [x - 1, y],   // Left
+                [x + 1, y],   // Right
+                [x, y - 1],   // Up
+                [x, y + 1],   // Down
+                [x - 1, y - 1], // Top-left
+                [x + 1, y - 1], // Top-right
+                [x - 1, y + 1], // Bottom-left
+                [x + 1, y + 1]  // Bottom-right
+            ];
+
+            cellsToFill.forEach(([cellX, cellY]) => {
+                if (cellX >= 0 && cellX < GRID_SIZE && cellY >= 0 && cellY < GRID_SIZE) {
+                    const key = `${cellX},${cellY}`;
+                    if (!combatDots[key]) {
+                        combatDots[key] = [];
+                    }
+                    for (let i = 0; i < DOTS_PER_HIT; i++) {
+                        combatDots[key].push({
+                            x: Math.random() * tileSize,
+                            y: Math.random() * tileSize,
+                            color: color
+                        });
+                    }
+                }
+            });
+        }
+
+        function movePlayer(dx, dy) {
+            const newX = player.x + dx;
+            const newY = player.y + dy;
+            if (isValidMove(newX, newY)) {
+                const enemyInTargetCell = enemies.find(enemy => enemy.x === newX && enemy.y === newY);
+                if (!enemyInTargetCell) {
+                    if (newX !== player.x || newY !== player.y) player.cellsTraveled++;
+                    player.x = newX;
+                    player.y = newY;
+                    handleItemCollection(); // Check if the player collected an item
+                    checkPlayerAtExit(); // Check if player reached the exit after moving
+                } else {
+                    // Enemy in the target cell, stay in place and do combat
+                    handleDamage(player, enemyInTargetCell);
+                }
+            }
+            moveEnemies();
+            render();
+        }
+
+        // Chase logic (naive)
+        function moveEnemies() {
+            enemies.forEach(enemy => {
+                const distance = Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y);
+                if (distance <= ENEMY_CHASE_DISTANCE) {
+                    let dx = 0, dy = 0;
+                    if (enemy.x < player.x && isValidMove(enemy.x + 1, enemy.y)) dx = 1;
+                    else if (enemy.x > player.x && isValidMove(enemy.x - 1, enemy.y)) dx = -1;
+                    else if (enemy.y < player.y && isValidMove(enemy.x, enemy.y + 1)) dy = 1;
+                    else if (enemy.y > player.x && isValidMove(enemy.x, enemy.y - 1)) dy = -1;
+
+                    if (!enemies.some(e => e.x === enemy.x + dx && e.y === enemy.y)) {
+                        enemy.x += dx;
+                        enemy.y += dy;
+                    }
+                }
+            });
+        }
+
+        function handleDamage(player, enemy) {
+            const enemyMisses = Math.random() < 0.2; // 1 in 5 chance the enemy misses you
+            const cellX = player.x;
+            const cellY = player.y;
+
+            if (!enemyMisses) {
+                player.health--;
+                player.totalDamageTaken++;
+                addCombatDots(cellX, cellY, COLORS.combatDotPlayer); // Add dots for player damage
+                console.log("Enemy hit! Player health: " + player.health);
+            } else {                
+                console.log("Enemy missed!");
+            }
+
+            enemy.health = enemy.health - player.damage;
+            player.totalDamageDone++;
+            addCombatDots(cellX, cellY, COLORS.combatDotEnemy); // Add dots for enemy damage
+            console.log("Player hit! Enemy health: " + enemy.health);
+
+            if (enemy.health <= 0) {
+                player.killCount++;
+                enemies = enemies.filter(e => e !== enemy);
+            }
+
+            if (player.health <= 0) {
+                alert(`Dead\n\nScore: ${player.score}\nDistance Traveled: ${player.cellsTraveled}\nTotal Damage Dealt: ${player.totalDamageDone}\nTotal Damage Received: ${player.totalDamageTaken}\nCircles Vanquished: ${player.killCount}\nItems Collected: ${player.itemsCollected}`);
+                resetGame();
+            }
+        }
+
+        function resetGame() {
+            player.health = PLAYER_HEALTH;
+            player.damage = PLAYER_BASE_DAMAGE;
+            player.bonusDamageTurns = 0;
+            player.totalDamageDone = 0;
+            player.totalDamageTaken = 0;
+            player.cellsTraveled = 0;
+            player.score = 0;
+            player.killCount = 0;
+            player.itemsCollected = 0;
+            player.x = 0;
+            player.y = 0;
+            combatDots = {};
+            generateExit();
+            generateEnemies();
+            generateItems();
+            generateWalls();
+            render();
+        }
+
+        function checkPlayerAtExit() {
+            if (player.x === exit.x && player.y === exit.y) {
+                player.score += 1;
+                player.damage = PLAYER_BASE_DAMAGE;
+                console.group("Level complete! " + player.score);
+                console.log("Score: " + player.score);
+                console.log("Current health: " + player.health);
+                console.log("Distance Traveled: " + player.cellsTraveled);
+                console.log("Total Damage Dealt: " + player.totalDamageDone);
+                console.log("Total Damage Received: " + player.totalDamageTaken);
+                console.log("Circles Vanquished: " + player.killCount);
+                console.log("Items Collected: " + player.itemsCollected);
+                console.groupEnd();
+                combatDots = {};
+                generateExit();
+                generateEnemies();
+                generateItems();
+                generateWalls();
+                render();
+            }
+        }
+
+        function render() {
+            drawGrid();
+            drawExit();
+            drawItems();
+            drawEnemies();
+            drawPlayer();
+            drawWalls();
+            drawCombatDots();
+        }
+
+        const directionMap = {
+            ArrowUp: [0, -1],
+            ArrowDown: [0, 1],
+            ArrowLeft: [-1, 0],
+            ArrowRight: [1, 0],
+            w: [0, -1],
+            s: [0, 1],
+            a: [-1, 0],
+            d: [1, 0],
+            h: [-1, 0],
+            j: [0, 1],
+            k: [0, -1],
+            l: [1, 0]
+        };
+
+        document.addEventListener('keydown', (e) => {
+            const direction = directionMap[e.key];
+            if (direction) {
+                movePlayer(...direction);
+                checkPlayerAtExit();
+                render();
+            }
+        });
+
+        let touchStartX = 0;
+        let touchStartY = 0;
+
+        canvas.addEventListener('touchstart', (e) => {
+            e.preventDefault(); // Prevent scrolling on touchstart
+            touchStartX = e.touches[0].clientX;
+            touchStartY = e.touches[0].clientY;
+        });
+
+        canvas.addEventListener('touchend', (e) => {
+            e.preventDefault(); // Prevent scrolling on touchend
+            const touchEndX = e.changedTouches[0].clientX;
+            const touchEndY = e.changedTouches[0].clientY;
+            const dx = touchEndX - touchStartX;
+            const dy = touchEndY - touchStartY;
+
+            if (Math.abs(dx) > Math.abs(dy)) {
+                // Horizontal swipe
+                if (dx > 0) {
+                    movePlayer(1, 0); // Swipe right
+                } else {
+                    movePlayer(-1, 0); // Swipe left
+                }
+            } else {
+                // Vertical swipe
+                if (dy > 0) {
+                    movePlayer(0, 1); // Swipe down
+                } else {
+                    movePlayer(0, -1); // Swipe up
+                }
+            }
+
+            render();
+        }, { passive: false }); // TIL you can use passive set to false to help make preventDefault actually work? Feels like superstition
+
+        const resizeCanvas = () => {
+            const rect = canvas.getBoundingClientRect();
+            canvas.width = rect.width;
+            canvas.height = rect.height;
+            tileSize = canvas.width / GRID_SIZE; // Update tile size based on canvas dimensions
+            render();
+        };
+
+        window.addEventListener('resize', resizeCanvas);
+        resizeCanvas();
+
+        // Initial level setup
+        generateExit();
+        generateEnemies();
+        generateItems();
+        generateWalls();
+        render();
+    </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/html/file-system/index.html b/html/file-system/index.html
new file mode 100644
index 0000000..89e05b8
--- /dev/null
+++ b/html/file-system/index.html
@@ -0,0 +1,297 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Folders in Folders in Folders</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            background-color: beige;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+        }
+        #navigation {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: center;
+            margin: 0.625em 0;
+        }
+        #navigation button {
+            margin: 0.3125em;
+            padding: 0.625em 0.9375em;
+            font-size: 1em;
+            flex-grow: 1;
+            min-width: 7.5em;
+        }
+        #currentPath {
+            font-weight: bold;
+            margin: 0.625em 0;
+        }
+        ul {
+            list-style-type: none;
+            width: 100%;
+            max-width: 37.5em;
+        }
+        li {
+            margin: 0.3125em 0;
+            cursor: pointer;
+        }
+        .folder {
+            display: inline-block;
+            padding: 0.3125em 0;
+            cursor: pointer;
+            font-size: 1.125em;
+            color: black;
+        }
+        #editor {
+            margin-top: 1.25em;
+            width: 100%;
+            max-width: 37.5em;
+        }
+        #editor textarea {
+            width: 100%;
+            height: 9.375em;
+            font-size: 1em;
+            padding: 0.625em;
+            box-sizing: border-box;
+            resize: none;
+        }
+        h1 {
+            text-align: center;
+            font-size: 1.5em;
+            margin-top: 0.625em;
+        }
+        @media (max-width: 37.5em) {
+            h1 {
+                font-size: 1.25em;
+            }
+            #navigation button {
+                font-size: 0.875em;
+                padding: 0.5em 0.625em;
+            }
+            .folder {
+                font-size: 1em;
+            }
+            #editor textarea {
+                height: 7.5em;
+                font-size: 0.875em;
+            }
+        }
+    </style>
+</head>
+<body>
+
+<div id="navigation">
+    <button onclick="goToRoot()">Go to Root</button>
+    <button onclick="goUp()">Go Up One Level</button>
+    <button onclick="createFolderPrompt()">Create Folder</button>
+    <button onclick="copyItem()">Copy</button>
+    <button onclick="pasteItem()">Paste</button>
+    <button onclick="deleteItem()">Delete</button>
+</div>
+
+<h1 id="currentPath">Current Folder: /</h1>
+
+<ul id="fileSystemTree"></ul>
+
+<div id="editor">
+    <h2>Folder Content Editor</h2>
+    <textarea id="folderContent" placeholder="Select a folder to edit its contents"></textarea>
+</div>
+
+<script>
+const initialState = () => ({
+    root: { type: 'folder', children: {}, content: '' }
+});
+
+const loadFileSystem = () => JSON.parse(localStorage.getItem('fileSystem')) || initialState();
+
+const state = {
+    fileSystem: loadFileSystem(),
+    currentFolderPath: 'root',
+    copiedFolderData: null, // Pastebin for the copied folder
+    copiedItemPath: null
+};
+
+const clone = (obj) => JSON.parse(JSON.stringify(obj));
+
+const updateFileSystem = (newFileSystem) => {
+    state.fileSystem = newFileSystem;
+    saveFileSystem(newFileSystem);
+};
+
+const getFolder = (path, fileSystem = state.fileSystem) => {
+    const parts = path.split('/').filter(part => part !== 'root');
+    return parts.reduce((current, part) => {
+        if (!current || !current.children[part]) {
+            return null;
+        }
+        return current.children[part];
+    }, fileSystem.root);
+};
+
+const updateFolder = (fileSystem, path, updateFn) => {
+    const rootCopy = clone(fileSystem);
+    const folderToUpdate = getFolder(path, rootCopy);
+    if (!folderToUpdate) {
+        alert(`Folder not found for path: ${path}`);
+        return null;
+    }
+
+    updateFn(folderToUpdate);
+    return rootCopy;
+};
+
+const addFolder = (path, name, folderData, fileSystem) => {
+    return updateFolder(fileSystem, path, (folder) => {
+        if (folder.children[name]) {
+            alert(`Folder with name "${name}" already exists.`);
+            return;
+        }
+        folder.children[name] = folderData || { type: 'folder', children: {}, content: '' };
+    });
+};
+
+const deleteFolder = (path, fileSystem) => {
+    const [parentPath, folderName] = path.split(/\/([^\/]+)$/);
+    return updateFolder(fileSystem, parentPath, (folder) => {
+        delete folder.children[folderName];
+    });
+};
+
+const saveFileSystem = (fileSystem) => {
+    localStorage.setItem('fileSystem', JSON.stringify(fileSystem));
+};
+
+const goToRoot = () => {
+    state.currentFolderPath = 'root';
+    render();
+};
+
+const goUp = () => {
+    const parts = state.currentFolderPath.split('/');
+    if (parts.length > 1) {
+        parts.pop();
+        state.currentFolderPath = parts.join('/');
+        render();
+    }
+};
+
+const createFolder = (path, name) => {
+    const updatedFileSystem = addFolder(path, name, null, state.fileSystem);
+    if (updatedFileSystem) {
+        updateFileSystem(updatedFileSystem);
+        render();
+    } else {
+        console.error('Error creating folder', path, name);
+        alert('Error creating folder');
+    }
+};
+
+const deleteItem = () => {
+    if (state.currentFolderPath === 'root') return alert("Can't delete the root!");
+
+    const folderToDelete = getFolder(state.currentFolderPath);
+    if (!folderToDelete) return alert("Folder not found!");
+
+    const hasNestedFolders = Object.keys(folderToDelete.children).length > 0;
+
+    if (hasNestedFolders) {
+        const confirmDelete = confirm(`The folder contains nested folders. Do you really wanna delete them too?`);
+        if (!confirmDelete) {
+            return;
+        }
+    }
+
+    const updatedFileSystem = deleteFolder(state.currentFolderPath, state.fileSystem);
+    if (updatedFileSystem) {
+        const parts = state.currentFolderPath.split('/');
+        parts.pop();
+        state.currentFolderPath = parts.join('/') || 'root';
+        updateFileSystem(updatedFileSystem);
+        render();
+    }
+};
+
+const copyItem = () => {
+    const folder = getFolder(state.currentFolderPath);
+    if (folder) {
+        state.copiedFolderData = clone(folder); // Store a deep copy of the folder
+        state.copiedItemPath = state.currentFolderPath; // Store the path so that we can remove later
+        // console.log('Folder copied:', state.copiedFolderData);
+    }
+};
+
+const pasteItem = () => {
+    if (!state.copiedItemPath || !state.copiedFolderData) return alert('No item to paste');
+
+    const [oldPath, folderName] = state.copiedItemPath.split(/\/([^\/]+)$/);
+    const updatedFileSystem = addFolder(state.currentFolderPath, folderName, state.copiedFolderData, state.fileSystem);
+
+    if (updatedFileSystem) {
+        updateFileSystem(deleteFolder(oldPath, updatedFileSystem)); // Remove original
+        state.copiedFolderData = null; // Clear the copied data
+        state.copiedItemPath = null;
+        render();
+    } else {
+        alert('Error pasting item');
+    }
+};
+
+const saveFolderContent = () => {
+    const updatedFileSystem = updateFolder(state.fileSystem, state.currentFolderPath, (folder) => {
+        folder.content = document.getElementById('folderContent').value;
+    });
+    if (updatedFileSystem) {
+        updateFileSystem(updatedFileSystem);
+    }
+};
+
+document.getElementById('folderContent').addEventListener('input', () => {
+    saveFolderContent();
+});
+
+const renderFolderTree = (folder, path = 'root') => {
+    const entries = Object.entries(folder.children);
+    return entries.length ? entries.map(([name, item]) => `
+        <li>
+            <span class="folder" onclick="selectFolder('${path}/${name}')">${name}</span>
+            <ul>${renderFolderTree(item, `${path}/${name}`)}</ul>
+        </li>
+    `).join('') : '';
+};
+
+const selectFolder = (path) => {
+    const folder = getFolder(path);
+    if (folder) {
+        state.currentFolderPath = path;
+        document.getElementById('folderContent').value = folder.content || '';
+        render();
+    } else {
+        console.error('Folder not found', path);
+        alert('Folder not found');
+    }
+};
+
+const render = () => {
+    if (!state.fileSystem.root) {
+        console.error('File system is not initialized correctly', state.fileSystem);
+        alert('File system is not initialized correctly');
+        return;
+    }
+    document.getElementById('currentPath').textContent = state.currentFolderPath.replace('root', '') || '/';
+    document.getElementById('fileSystemTree').innerHTML = renderFolderTree(state.fileSystem.root);
+};
+
+const createFolderPrompt = () => {
+    const name = prompt('Enter folder name:');
+    if (name) createFolder(state.currentFolderPath, name);
+};
+
+render();
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/html/playground/APL386.ttf b/html/playground/APL386.ttf
new file mode 100644
index 0000000..5e3a338
--- /dev/null
+++ b/html/playground/APL386.ttf
Binary files differdiff --git a/html/playground/index.html b/html/playground/index.html
new file mode 100644
index 0000000..680f022
--- /dev/null
+++ b/html/playground/index.html
@@ -0,0 +1,243 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>JavaScript Playground</title>
+    <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles.">
+    <style>
+        body {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            background-color: #ddd;
+            padding: 10px;
+            height: 100vh;
+            margin: 0;
+        }
+
+        textarea {
+            width: 100%;
+            height: 64%;
+            font-family: monospace;
+            background-color: #FFFEEC;
+            border: 2px solid #000;
+            scrollbar-width: none;            
+            font-size: 1rem;
+            margin-bottom: 10px;
+            padding: 10px;
+            box-sizing: border-box;
+            resize: none;
+            border-bottom: 12px solid teal;
+            -webkit-user-modify: read-write-plaintext-only;
+        }
+
+        textarea::-webkit-scrollbar {
+            display: none;
+        }
+
+        textarea::selection {
+            background-color: #EFECA7;
+        }
+
+        textarea:focus {
+            outline: none;
+        }
+
+        #console {
+            width: 100%;
+            height: 22%;
+            background-color: #000;
+            color: #0fc;
+            font-family: monospace;
+            font-size: 1rem;
+            overflow-y: auto;
+            padding: 10px;
+            box-sizing: border-box;
+            white-space: pre-wrap;
+        }
+
+        .button-container {
+            width: 100%;
+            display: flex;
+            justify-content: flex-end;
+            margin-bottom: 10px;
+        }
+
+        button {
+            padding: 10px 20px;
+            font-size: 1rem;
+            margin-left: 10px;
+            cursor: pointer;
+            border: none;
+            transition: background-color 0.3s ease;
+        }
+
+        button:hover, button:focus {
+            outline: none; 
+        }
+
+        button.run {
+            background-color: teal;
+            color: #FFFFFF;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="playground" id="playground">    
+        <div class="seesaw" id="seesaw"></div>
+        <div class="slide" id="slide"></div>
+    </div>
+
+    <div class="button-container">
+        <button onclick="clearEverything()">Clear</button>
+        <button onclick="downloadCodeAndEditor()">Download</button>
+        <button onclick="shareCode()">Share</button>
+        <button onclick="evaluateCode()" class="run">Run Code</button>
+    </div>
+    <textarea id="codeInput" placeholder="Enter JavaScript..." spellcheck="false"></textarea>
+    <div id="console"></div>
+
+    <script>
+        function evaluateCode() {
+            const code = document.getElementById('codeInput').value;
+            const consoleDiv = document.getElementById('console');
+            consoleDiv.innerHTML = '';
+
+            // Custom console.log function to output to the console div
+            const originalConsoleLog = console.log;
+            console.log = function(...args) {
+                args.forEach(arg => {
+                    const output = document.createElement('div');
+                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
+                    consoleDiv.appendChild(output);
+                });
+                originalConsoleLog.apply(console, args);
+            };
+
+            try {
+                eval(code);
+            } catch (error) {
+                const errorOutput = document.createElement('div');
+                errorOutput.textContent = error;
+                errorOutput.style.color = 'red';
+                consoleDiv.appendChild(errorOutput);
+            }
+
+            // Restore browser's console.log
+            console.log = originalConsoleLog;
+        }
+
+        function downloadCodeAndEditor() {
+            const codeInput = document.getElementById('codeInput').value;
+            const htmlContent = document.documentElement.outerHTML.replace(
+                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                `<textarea id="codeInput">${codeInput}</textarea>`
+            );
+
+            const blob = new Blob([htmlContent], { type: 'text/html' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'code_editor.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+        }
+
+        function shareCode() {
+            const code = document.getElementById('codeInput').value;
+            const encodedCode = btoa(encodeURIComponent(code));
+            window.location.hash = encodedCode;
+            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
+        }
+
+        function clearEverything() {
+            if (!confirm('Are you sure you want to reset the playground?')) {
+                return;
+            } else {               
+                window.location.hash = '';
+                window.location.reload();
+            }
+        }
+
+        function loadCodeFromHash() {
+            const hash = window.location.hash.substring(1);
+            if (hash) {
+                try {
+                    const decodedCode = decodeURIComponent(atob(hash));
+                    document.getElementById('codeInput').value = decodedCode;
+                } catch (error) {
+                    console.error('Failed to decode the URL hash:', error);
+                }
+            }
+        }
+
+        function help() {
+            const helpText = `
+            Welcome to the JavaScript Playground! Here are some tips to get you started:
+
+            1. Enter your JavaScript code in the textarea.
+            2. Click the "Run Code" button to execute your code.
+            3. The console output will be displayed below the textarea.
+            4. Click the "Clear" button to reset the playground.
+            5. Click the "Download" button to save your code and editor as an HTML file.
+            6. Click the "Share" button to generate a URL to share your code with others.
+            7. You can also press "Cmd + Enter" to run your code.
+            8. There's an empty div above the buttons with the id "playground"
+            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
+            10. You can use the "clear()" function to clear the content's of the console.
+
+            Go nuts! Share scrappy fiddles!
+            `;
+            console.log(helpText);
+        }
+
+        function clear() {
+            document.getElementById('console').innerHTML = '';
+        }
+
+        function mountHelp() {
+            console.log(`
+            The mount function is used to mount stuff to the playground div.
+            It takes a function as an argument, which in turn receives the playground div as an argument.
+            Before mounting, it clears the playground div.
+            Here's an example of how to use the mount function:
+
+            mount(playground => {
+                const h1 = document.createElement('h1');
+                h1.textContent = 'Hell is empty and all the devils are here.';
+                playground.appendChild(h1);
+            });
+
+            This will add an h1 element to the playground div.
+            `);
+        }
+
+        function mount(mountFunction) {
+            const playground = document.getElementById('playground');
+            if (!playground) {
+                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
+                return;
+            }
+
+            if (playground.innerHTML.trim() !== "") {
+                playground.innerHTML = "";
+            }
+            mountFunction(playground);
+        }
+
+
+        document.getElementById('codeInput').addEventListener('keydown', function(event) {
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                evaluateCode();
+            }
+        });
+
+        window.onload = loadCodeFromHash;
+    </script>
+</body>
+</html>
diff --git a/html/playground/little-regex.html b/html/playground/little-regex.html
new file mode 100644
index 0000000..c09139f
--- /dev/null
+++ b/html/playground/little-regex.html
@@ -0,0 +1,728 @@
+<html lang="en"><head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>JavaScript Playground</title>
+    <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles.">
+    <style>
+        body {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            background-color: #ddd;
+            padding: 10px;
+            height: 100vh;
+            margin: 0;
+        }
+
+        textarea {
+            width: 100%;
+            height: 64%;
+            font-family: monospace;
+            background-color: #FFFEEC;
+            border: 2px solid #000;
+            scrollbar-width: none;            
+            font-size: 1rem;
+            margin-bottom: 10px;
+            padding: 10px;
+            box-sizing: border-box;
+            resize: none;
+            border-bottom: 12px solid teal;
+            -webkit-user-modify: read-write-plaintext-only;
+        }
+
+        textarea::-webkit-scrollbar {
+            display: none;
+        }
+
+        textarea::selection {
+            background-color: #EFECA7;
+        }
+
+        textarea:focus {
+            outline: none;
+        }
+
+        #console {
+            width: 100%;
+            height: 22%;
+            background-color: #000;
+            color: #0fc;
+            font-family: monospace;
+            font-size: 1rem;
+            overflow-y: auto;
+            padding: 10px;
+            box-sizing: border-box;
+            white-space: pre-wrap;
+        }
+
+        .button-container {
+            width: 100%;
+            display: flex;
+            justify-content: flex-end;
+            margin-bottom: 10px;
+        }
+
+        button {
+            padding: 10px 20px;
+            font-size: 1rem;
+            margin-left: 10px;
+            cursor: pointer;
+            border: none;
+            transition: background-color 0.3s ease;
+        }
+
+        button:hover, button:focus {
+            outline: none; 
+        }
+
+        button.run {
+            background-color: teal;
+            color: #FFFFFF;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="playground" id="playground">    
+        <div class="seesaw" id="seesaw"></div>
+        <div class="slide" id="slide"></div>
+    </div>
+
+    <div class="button-container">
+        <button onclick="clearEverything()">Clear</button>
+        <button onclick="downloadCodeAndEditor()">Download</button>
+        <button onclick="shareCode()">Share</button>
+        <button onclick="evaluateCode()" class="run">Run Code</button>
+    </div>
+    <textarea id="codeInput">// Inspired by <https://www.cs.princeton.edu/courses/archive/spr09/cos333/beautiful.html>
+const matchHere = (pattern, text) => {
+    if (pattern.length === 0) return true;
+    
+    // If pattern ends with $, match end of string
+    if (pattern[0] === '
+    <div id="console"></div>
+
+    <script>
+        function evaluateCode() {
+            const code = document.getElementById('codeInput').value;
+            const consoleDiv = document.getElementById('console');
+            consoleDiv.innerHTML = '';
+
+            // Custom console.log function to output to the console div
+            const originalConsoleLog = console.log;
+            console.log = function(...args) {
+                args.forEach(arg => {
+                    const output = document.createElement('div');
+                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
+                    consoleDiv.appendChild(output);
+                });
+                originalConsoleLog.apply(console, args);
+            };
+
+            try {
+                eval(code);
+            } catch (error) {
+                const errorOutput = document.createElement('div');
+                errorOutput.textContent = error;
+                errorOutput.style.color = 'red';
+                consoleDiv.appendChild(errorOutput);
+            }
+
+            // Restore browser's console.log
+            console.log = originalConsoleLog;
+        }
+
+        function downloadCodeAndEditor() {
+            const codeInput = document.getElementById('codeInput').value;
+            const htmlContent = document.documentElement.outerHTML.replace(
+                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                `<textarea id="codeInput">${codeInput}</textarea>`
+            );
+
+            const blob = new Blob([htmlContent], { type: 'text/html' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'code_editor.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+        }
+
+        function shareCode() {
+            const code = document.getElementById('codeInput').value;
+            const encodedCode = btoa(encodeURIComponent(code));
+            window.location.hash = encodedCode;
+            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
+        }
+
+        function clearEverything() {
+            if (!confirm('Are you sure you want to reset the playground?')) {
+                return;
+            } else {               
+                window.location.hash = '';
+                window.location.reload();
+            }
+        }
+
+        function loadCodeFromHash() {
+            const hash = window.location.hash.substring(1);
+            if (hash) {
+                try {
+                    const decodedCode = decodeURIComponent(atob(hash));
+                    document.getElementById('codeInput').value = decodedCode;
+                } catch (error) {
+                    console.error('Failed to decode the URL hash:', error);
+                }
+            }
+        }
+
+        function help() {
+            const helpText = `
+            Welcome to the JavaScript Playground! Here are some tips to get you started:
+
+            1. Enter your JavaScript code in the textarea.
+            2. Click the "Run Code" button to execute your code.
+            3. The console output will be displayed below the textarea.
+            4. Click the "Clear" button to reset the playground.
+            5. Click the "Download" button to save your code and editor as an HTML file.
+            6. Click the "Share" button to generate a URL to share your code with others.
+            7. You can also press "Cmd + Enter" to run your code.
+            8. There's an empty div above the buttons with the id "playground"
+            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
+            10. You can use the "clear()" function to clear the content's of the console.
+
+            Go nuts! Share scrappy fiddles!
+            `;
+            console.log(helpText);
+        }
+
+        function clear() {
+            document.getElementById('console').innerHTML = '';
+        }
+
+        function mountHelp() {
+            console.log(`
+            The mount function is used to mount stuff to the playground div.
+            It takes a function as an argument, which in turn receives the playground div as an argument.
+            Before mounting, it clears the playground div.
+            Here's an example of how to use the mount function:
+
+            mount(playground => {
+                const h1 = document.createElement('h1');
+                h1.textContent = 'Hell is empty and all the devils are here.';
+                playground.appendChild(h1);
+            });
+
+            This will add an h1 element to the playground div.
+            `);
+        }
+
+        function mount(mountFunction) {
+            const playground = document.getElementById('playground');
+            if (!playground) {
+                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
+                return;
+            }
+
+            if (playground.innerHTML.trim() !== "") {
+                playground.innerHTML = "";
+            }
+            mountFunction(playground);
+        }
+
+
+        document.getElementById('codeInput').addEventListener('keydown', function(event) {
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                evaluateCode();
+            }
+        });
+
+        window.onload = loadCodeFromHash;
+    </script>
+
+
+</body></html> && pattern.length === 1) return text.length === 0;
+
+    // If next char is '*', match zero or more occurrences of prev char
+    if (pattern[1] === '*') return matchStar(pattern[0], pattern.slice(2), text);
+
+    // Match . or literal character
+    if (text.length !== 0 && (pattern[0] === '.' || pattern[0] === text[0])) {
+        return matchHere(pattern.slice(1), text.slice(1));
+    }
+
+    return false;
+};
+
+const matchStar = (prevChar, pattern, text) => {
+    // Try matching zero occurrences first
+    if (matchHere(pattern, text)) return true;
+
+    // Then, match one or more occurrences of prevChar
+    while (text.length > 0 && (text[0] === prevChar || prevChar === '.')) {
+        text = text.slice(1);
+        if (matchHere(pattern, text)) return true;
+    }
+
+    return false;
+};
+
+const match = (pattern, text) => {
+    // Handle ^ anchor at the beginning
+    if (pattern[0] === '^') {
+        return matchHere(pattern.slice(1), text);
+    }
+
+    // Otherwise, check the entire string
+    for (let i = 0; i <= text.length; i++) {
+        if (matchHere(pattern, text.slice(i))) return true;
+    }
+
+    return false;
+};
+
+const assertEqual = (actual, expected, testName) => 
+    console.log(actual === expected ? `Passed: ${testName}` : `Failed: ${testName}. Expected ${expected} but got ${actual}`);
+
+assertEqual(match("ab*c", "ac"), true, "Star match 'ab*c' vs 'ac' (zero occurrences)");
+assertEqual(match("ab*c", "abbbc"), true, "Star match 'ab*c' vs 'abbbc' (multiple occurrences)");
+assertEqual(match("^ab.*c$", "abc"), true, "Complex match '^ab.*c
+    <div id="console"></div>
+
+    <script>
+        function evaluateCode() {
+            const code = document.getElementById('codeInput').value;
+            const consoleDiv = document.getElementById('console');
+            consoleDiv.innerHTML = '';
+
+            // Custom console.log function to output to the console div
+            const originalConsoleLog = console.log;
+            console.log = function(...args) {
+                args.forEach(arg => {
+                    const output = document.createElement('div');
+                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
+                    consoleDiv.appendChild(output);
+                });
+                originalConsoleLog.apply(console, args);
+            };
+
+            try {
+                eval(code);
+            } catch (error) {
+                const errorOutput = document.createElement('div');
+                errorOutput.textContent = error;
+                errorOutput.style.color = 'red';
+                consoleDiv.appendChild(errorOutput);
+            }
+
+            // Restore browser's console.log
+            console.log = originalConsoleLog;
+        }
+
+        function downloadCodeAndEditor() {
+            const codeInput = document.getElementById('codeInput').value;
+            const htmlContent = document.documentElement.outerHTML.replace(
+                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                `<textarea id="codeInput">${codeInput}</textarea>`
+            );
+
+            const blob = new Blob([htmlContent], { type: 'text/html' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'code_editor.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+        }
+
+        function shareCode() {
+            const code = document.getElementById('codeInput').value;
+            const encodedCode = btoa(encodeURIComponent(code));
+            window.location.hash = encodedCode;
+            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
+        }
+
+        function clearEverything() {
+            if (!confirm('Are you sure you want to reset the playground?')) {
+                return;
+            } else {               
+                window.location.hash = '';
+                window.location.reload();
+            }
+        }
+
+        function loadCodeFromHash() {
+            const hash = window.location.hash.substring(1);
+            if (hash) {
+                try {
+                    const decodedCode = decodeURIComponent(atob(hash));
+                    document.getElementById('codeInput').value = decodedCode;
+                } catch (error) {
+                    console.error('Failed to decode the URL hash:', error);
+                }
+            }
+        }
+
+        function help() {
+            const helpText = `
+            Welcome to the JavaScript Playground! Here are some tips to get you started:
+
+            1. Enter your JavaScript code in the textarea.
+            2. Click the "Run Code" button to execute your code.
+            3. The console output will be displayed below the textarea.
+            4. Click the "Clear" button to reset the playground.
+            5. Click the "Download" button to save your code and editor as an HTML file.
+            6. Click the "Share" button to generate a URL to share your code with others.
+            7. You can also press "Cmd + Enter" to run your code.
+            8. There's an empty div above the buttons with the id "playground"
+            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
+            10. You can use the "clear()" function to clear the content's of the console.
+
+            Go nuts! Share scrappy fiddles!
+            `;
+            console.log(helpText);
+        }
+
+        function clear() {
+            document.getElementById('console').innerHTML = '';
+        }
+
+        function mountHelp() {
+            console.log(`
+            The mount function is used to mount stuff to the playground div.
+            It takes a function as an argument, which in turn receives the playground div as an argument.
+            Before mounting, it clears the playground div.
+            Here's an example of how to use the mount function:
+
+            mount(playground => {
+                const h1 = document.createElement('h1');
+                h1.textContent = 'Hell is empty and all the devils are here.';
+                playground.appendChild(h1);
+            });
+
+            This will add an h1 element to the playground div.
+            `);
+        }
+
+        function mount(mountFunction) {
+            const playground = document.getElementById('playground');
+            if (!playground) {
+                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
+                return;
+            }
+
+            if (playground.innerHTML.trim() !== "") {
+                playground.innerHTML = "";
+            }
+            mountFunction(playground);
+        }
+
+
+        document.getElementById('codeInput').addEventListener('keydown', function(event) {
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                evaluateCode();
+            }
+        });
+
+        window.onload = loadCodeFromHash;
+    </script>
+
+
+</body></html> vs 'abc'");
+assertEqual(match("^ab.*c$", "abcd"), false, "Complex mismatch '^ab.*c
+    <div id="console"></div>
+
+    <script>
+        function evaluateCode() {
+            const code = document.getElementById('codeInput').value;
+            const consoleDiv = document.getElementById('console');
+            consoleDiv.innerHTML = '';
+
+            // Custom console.log function to output to the console div
+            const originalConsoleLog = console.log;
+            console.log = function(...args) {
+                args.forEach(arg => {
+                    const output = document.createElement('div');
+                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
+                    consoleDiv.appendChild(output);
+                });
+                originalConsoleLog.apply(console, args);
+            };
+
+            try {
+                eval(code);
+            } catch (error) {
+                const errorOutput = document.createElement('div');
+                errorOutput.textContent = error;
+                errorOutput.style.color = 'red';
+                consoleDiv.appendChild(errorOutput);
+            }
+
+            // Restore browser's console.log
+            console.log = originalConsoleLog;
+        }
+
+        function downloadCodeAndEditor() {
+            const codeInput = document.getElementById('codeInput').value;
+            const htmlContent = document.documentElement.outerHTML.replace(
+                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                `<textarea id="codeInput">${codeInput}</textarea>`
+            );
+
+            const blob = new Blob([htmlContent], { type: 'text/html' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'code_editor.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+        }
+
+        function shareCode() {
+            const code = document.getElementById('codeInput').value;
+            const encodedCode = btoa(encodeURIComponent(code));
+            window.location.hash = encodedCode;
+            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
+        }
+
+        function clearEverything() {
+            if (!confirm('Are you sure you want to reset the playground?')) {
+                return;
+            } else {               
+                window.location.hash = '';
+                window.location.reload();
+            }
+        }
+
+        function loadCodeFromHash() {
+            const hash = window.location.hash.substring(1);
+            if (hash) {
+                try {
+                    const decodedCode = decodeURIComponent(atob(hash));
+                    document.getElementById('codeInput').value = decodedCode;
+                } catch (error) {
+                    console.error('Failed to decode the URL hash:', error);
+                }
+            }
+        }
+
+        function help() {
+            const helpText = `
+            Welcome to the JavaScript Playground! Here are some tips to get you started:
+
+            1. Enter your JavaScript code in the textarea.
+            2. Click the "Run Code" button to execute your code.
+            3. The console output will be displayed below the textarea.
+            4. Click the "Clear" button to reset the playground.
+            5. Click the "Download" button to save your code and editor as an HTML file.
+            6. Click the "Share" button to generate a URL to share your code with others.
+            7. You can also press "Cmd + Enter" to run your code.
+            8. There's an empty div above the buttons with the id "playground"
+            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
+            10. You can use the "clear()" function to clear the content's of the console.
+
+            Go nuts! Share scrappy fiddles!
+            `;
+            console.log(helpText);
+        }
+
+        function clear() {
+            document.getElementById('console').innerHTML = '';
+        }
+
+        function mountHelp() {
+            console.log(`
+            The mount function is used to mount stuff to the playground div.
+            It takes a function as an argument, which in turn receives the playground div as an argument.
+            Before mounting, it clears the playground div.
+            Here's an example of how to use the mount function:
+
+            mount(playground => {
+                const h1 = document.createElement('h1');
+                h1.textContent = 'Hell is empty and all the devils are here.';
+                playground.appendChild(h1);
+            });
+
+            This will add an h1 element to the playground div.
+            `);
+        }
+
+        function mount(mountFunction) {
+            const playground = document.getElementById('playground');
+            if (!playground) {
+                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
+                return;
+            }
+
+            if (playground.innerHTML.trim() !== "") {
+                playground.innerHTML = "";
+            }
+            mountFunction(playground);
+        }
+
+
+        document.getElementById('codeInput').addEventListener('keydown', function(event) {
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                evaluateCode();
+            }
+        });
+
+        window.onload = loadCodeFromHash;
+    </script>
+
+
+</body></html> vs 'abcd'");</textarea>
+    <div id="console"></div>
+
+    <script>
+        function evaluateCode() {
+            const code = document.getElementById('codeInput').value;
+            const consoleDiv = document.getElementById('console');
+            consoleDiv.innerHTML = '';
+
+            // Custom console.log function to output to the console div
+            const originalConsoleLog = console.log;
+            console.log = function(...args) {
+                args.forEach(arg => {
+                    const output = document.createElement('div');
+                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
+                    consoleDiv.appendChild(output);
+                });
+                originalConsoleLog.apply(console, args);
+            };
+
+            try {
+                eval(code);
+            } catch (error) {
+                const errorOutput = document.createElement('div');
+                errorOutput.textContent = error;
+                errorOutput.style.color = 'red';
+                consoleDiv.appendChild(errorOutput);
+            }
+
+            // Restore browser's console.log
+            console.log = originalConsoleLog;
+        }
+
+        function downloadCodeAndEditor() {
+            const codeInput = document.getElementById('codeInput').value;
+            const htmlContent = document.documentElement.outerHTML.replace(
+                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                `<textarea id="codeInput">${codeInput}</textarea>`
+            );
+
+            const blob = new Blob([htmlContent], { type: 'text/html' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'code_editor.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+        }
+
+        function shareCode() {
+            const code = document.getElementById('codeInput').value;
+            const encodedCode = btoa(encodeURIComponent(code));
+            window.location.hash = encodedCode;
+            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
+        }
+
+        function clearEverything() {
+            if (!confirm('Are you sure you want to reset the playground?')) {
+                return;
+            } else {               
+                window.location.hash = '';
+                window.location.reload();
+            }
+        }
+
+        function loadCodeFromHash() {
+            const hash = window.location.hash.substring(1);
+            if (hash) {
+                try {
+                    const decodedCode = decodeURIComponent(atob(hash));
+                    document.getElementById('codeInput').value = decodedCode;
+                } catch (error) {
+                    console.error('Failed to decode the URL hash:', error);
+                }
+            }
+        }
+
+        function help() {
+            const helpText = `
+            Welcome to the JavaScript Playground! Here are some tips to get you started:
+
+            1. Enter your JavaScript code in the textarea.
+            2. Click the "Run Code" button to execute your code.
+            3. The console output will be displayed below the textarea.
+            4. Click the "Clear" button to reset the playground.
+            5. Click the "Download" button to save your code and editor as an HTML file.
+            6. Click the "Share" button to generate a URL to share your code with others.
+            7. You can also press "Cmd + Enter" to run your code.
+            8. There's an empty div above the buttons with the id "playground"
+            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
+            10. You can use the "clear()" function to clear the content's of the console.
+
+            Go nuts! Share scrappy fiddles!
+            `;
+            console.log(helpText);
+        }
+
+        function clear() {
+            document.getElementById('console').innerHTML = '';
+        }
+
+        function mountHelp() {
+            console.log(`
+            The mount function is used to mount stuff to the playground div.
+            It takes a function as an argument, which in turn receives the playground div as an argument.
+            Before mounting, it clears the playground div.
+            Here's an example of how to use the mount function:
+
+            mount(playground => {
+                const h1 = document.createElement('h1');
+                h1.textContent = 'Hell is empty and all the devils are here.';
+                playground.appendChild(h1);
+            });
+
+            This will add an h1 element to the playground div.
+            `);
+        }
+
+        function mount(mountFunction) {
+            const playground = document.getElementById('playground');
+            if (!playground) {
+                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
+                return;
+            }
+
+            if (playground.innerHTML.trim() !== "") {
+                playground.innerHTML = "";
+            }
+            mountFunction(playground);
+        }
+
+
+        document.getElementById('codeInput').addEventListener('keydown', function(event) {
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                evaluateCode();
+            }
+        });
+
+        window.onload = loadCodeFromHash;
+    </script>
+
+
+</body></html>
\ No newline at end of file
diff --git a/html/playground/regex.html b/html/playground/regex.html
new file mode 100644
index 0000000..41f50e9
--- /dev/null
+++ b/html/playground/regex.html
@@ -0,0 +1,477 @@
+<html lang="en"><head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>JavaScript Playground</title>
+    <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles.">
+    <style>
+        body {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            background-color: #ddd;
+            padding: 10px;
+            height: 100vh;
+            margin: 0;
+        }
+
+        textarea {
+            width: 100%;
+            height: 64%;
+            font-family: monospace;
+            background-color: #FFFEEC;
+            border: 2px solid #000;
+            scrollbar-width: none;            
+            font-size: 1rem;
+            margin-bottom: 10px;
+            padding: 10px;
+            box-sizing: border-box;
+            resize: none;
+            border-bottom: 12px solid teal;
+            -webkit-user-modify: read-write-plaintext-only;
+        }
+
+        textarea::-webkit-scrollbar {
+            display: none;
+        }
+
+        textarea::selection {
+            background-color: #EFECA7;
+        }
+
+        textarea:focus {
+            outline: none;
+        }
+
+        #console {
+            width: 100%;
+            height: 22%;
+            background-color: #000;
+            color: #0fc;
+            font-family: monospace;
+            font-size: 1rem;
+            overflow-y: auto;
+            padding: 10px;
+            box-sizing: border-box;
+            white-space: pre-wrap;
+        }
+
+        .button-container {
+            width: 100%;
+            display: flex;
+            justify-content: flex-end;
+            margin-bottom: 10px;
+        }
+
+        button {
+            padding: 10px 20px;
+            font-size: 1rem;
+            margin-left: 10px;
+            cursor: pointer;
+            border: none;
+            transition: background-color 0.3s ease;
+        }
+
+        button:hover, button:focus {
+            outline: none; 
+        }
+
+        button.run {
+            background-color: teal;
+            color: #FFFFFF;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="playground" id="playground">    
+        <div class="seesaw" id="seesaw"></div>
+        <div class="slide" id="slide"></div>
+    </div>
+
+    <div class="button-container">
+        <button onclick="clearEverything()">Clear</button>
+        <button onclick="downloadCodeAndEditor()">Download</button>
+        <button onclick="shareCode()">Share</button>
+        <button onclick="evaluateCode()" class="run">Run Code</button>
+    </div>
+    <textarea id="codeInput">const tokenize = (pattern) => {
+    const tokens = [];
+    let i = 0;
+
+    while (i < pattern.length) {
+        const char = pattern[i];
+
+        if (char === '.' || char === '*' || char === '(' || char === ')' || char === '|') {
+            tokens.push({
+                type: char,
+                value: char
+            });
+        } else if (char === '\\') { // Handle escaped characters
+            i++;
+            tokens.push({
+                type: 'literal',
+                value: pattern[i]
+            });
+        } else if (char === '[') { // Handle character classes
+            let charClass = '';
+            i++;
+            while (pattern[i] !== ']' && i < pattern.length) {
+                charClass += pattern[i];
+                i++;
+            }
+            tokens.push({
+                type: 'charClass',
+                value: charClass
+            });
+        } else {
+            tokens.push({
+                type: 'literal',
+                value: char
+            });
+        }
+        i++;
+    }
+
+    return tokens;
+};
+
+
+
+
+const parse = (tokens) => {
+    let i = 0;
+
+    const parseSequenceExpression = () => {
+        const node = {
+            type: 'sequence',
+            elements: []
+        };
+
+        while (i < tokens.length) {
+            const token = tokens[i];
+
+            if (token.type === 'literal') {
+                node.elements.push({
+                    type: 'literal',
+                    value: token.value
+                });
+            } else if (token.type === '*') {
+                const lastElement = node.elements.pop();
+                node.elements.push({
+                    type: 'star',
+                    element: lastElement
+                });
+            } else if (token.type === '.') {
+                node.elements.push({
+                    type: 'dot'
+                });
+            } else if (token.type === 'charClass') {
+                node.elements.push({
+                    type: 'charClass',
+                    value: token.value
+                });
+            } else if (token.type === '|') {
+                i++;
+                const right = parseSequenceExpression();
+                return {
+                    type: 'alternation',
+                    left: node,
+                    right
+                };
+            } else if (token.type === '(') {
+                i++;
+                node.elements.push(parseSequenceExpression());
+            } else if (token.type === ')') {
+                break; // End of a grouping
+            }
+
+            i++;
+        }
+
+        return node;
+    };
+
+    return parseSequenceExpression();
+};
+
+
+
+const evaluateMatch = (node) => (input) => {
+    if (node.type === 'literal') {
+        return input[0] === node.value ? input.slice(1) : null;
+    }
+    if (node.type === 'dot') {
+        return input.length > 0 ? input.slice(1) : null;
+    }
+    if (node.type === 'star') {
+        let remainder = input;
+        while (remainder !== null) {
+            const next = evaluateMatch(node.element)(remainder);
+            if (next === null) {
+                break;
+            }
+            remainder = next;
+        }
+        return remainder;
+    }
+    if (node.type === 'charClass') {
+        return node.value.includes(input[0]) ? input.slice(1) : null;
+    }
+    if (node.type === 'alternation') {
+        const remainderLeft = evaluateMatch(node.left)(input);
+        const remainderRight = evaluateMatch(node.right)(input);
+        return remainderLeft !== null ? remainderLeft : remainderRight;
+    }
+    if (node.type === 'sequence') {
+        let remainder = input;
+        for (const element of node.elements) {
+            remainder = evaluateMatch(element)(remainder);
+            if (remainder === null) {
+                return null;
+            }
+        }
+        return remainder;
+    }
+};
+
+
+
+
+const assertEqual = (expected, actual, message) => {
+    if (expected !== actual) {
+        console.error(`FAIL: ${message}`);
+    } else {
+        console.log(`PASS: ${message}`);
+    }
+};
+
+
+
+const runTests = () => {
+    const tests = [{
+            pattern: "a.b*c",
+            input: "abbbc",
+            expected: true
+        },
+        {
+            pattern: "a.b*c",
+            input: "abc",
+            expected: true
+        },
+        {
+            pattern: "a.b*c",
+            input: "ac",
+            expected: true
+        },
+        {
+            pattern: "a.b*c",
+            input: "abbc",
+            expected: true
+        },
+        {
+            pattern: "a.b*c",
+            input: "axbc",
+            expected: false
+        },
+
+        // Character class tests
+        {
+            pattern: "[abc]x",
+            input: "bx",
+            expected: true
+        },
+        {
+            pattern: "[abc]x",
+            input: "dx",
+            expected: false
+        },
+
+        // Grouping and alternation tests
+        {
+            pattern: "(a|b)c",
+            input: "ac",
+            expected: true
+        },
+        {
+            pattern: "(a|b)c",
+            input: "bc",
+            expected: true
+        },
+        {
+            pattern: "(a|b)c",
+            input: "cc",
+            expected: false
+        },
+
+        // Escaped characters tests
+        {
+            pattern: "a\\.b",
+            input: "a.b",
+            expected: true
+        },
+        {
+            pattern: "a\\.b",
+            input: "a-b",
+            expected: false
+        },
+        {
+            pattern: "a\\*b",
+            input: "a*b",
+            expected: true
+        }
+    ];
+
+    tests.forEach(({ pattern, input, expected }, index) => {
+        const tokens = tokenize(pattern);
+        const ast = parse(tokens);
+        const result = evaluateMatch(ast)(input) !== null;
+        assertEqual(expected, result, `Test ${index + 1}`);
+    });
+};
+
+runTests();</textarea>
+    <div id="console"><div>PASS: Test 1</div><div>PASS: Test 2</div><div>PASS: Test 4</div><div>PASS: Test 6</div><div>PASS: Test 7</div><div>PASS: Test 8</div><div>PASS: Test 9</div><div>PASS: Test 10</div><div>PASS: Test 11</div><div>PASS: Test 12</div><div>PASS: Test 13</div></div>
+
+    <script>
+        function evaluateCode() {
+            const code = document.getElementById('codeInput').value;
+            const consoleDiv = document.getElementById('console');
+            consoleDiv.innerHTML = '';
+
+            // Custom console.log function to output to the console div
+            const originalConsoleLog = console.log;
+            console.log = function(...args) {
+                args.forEach(arg => {
+                    const output = document.createElement('div');
+                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
+                    consoleDiv.appendChild(output);
+                });
+                originalConsoleLog.apply(console, args);
+            };
+
+            try {
+                eval(code);
+            } catch (error) {
+                const errorOutput = document.createElement('div');
+                errorOutput.textContent = error;
+                errorOutput.style.color = 'red';
+                consoleDiv.appendChild(errorOutput);
+            }
+
+            // Restore browser's console.log
+            console.log = originalConsoleLog;
+        }
+
+        function downloadCodeAndEditor() {
+            const codeInput = document.getElementById('codeInput').value;
+            const htmlContent = document.documentElement.outerHTML.replace(
+                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                `<textarea id="codeInput">${codeInput}</textarea>`
+            );
+
+            const blob = new Blob([htmlContent], { type: 'text/html' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'code_editor.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+        }
+
+        function shareCode() {
+            const code = document.getElementById('codeInput').value;
+            const encodedCode = btoa(encodeURIComponent(code));
+            window.location.hash = encodedCode;
+            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
+        }
+
+        function clearEverything() {
+            if (!confirm('Are you sure you want to reset the playground?')) {
+                return;
+            } else {               
+                window.location.hash = '';
+                window.location.reload();
+            }
+        }
+
+        function loadCodeFromHash() {
+            const hash = window.location.hash.substring(1);
+            if (hash) {
+                try {
+                    const decodedCode = decodeURIComponent(atob(hash));
+                    document.getElementById('codeInput').value = decodedCode;
+                } catch (error) {
+                    console.error('Failed to decode the URL hash:', error);
+                }
+            }
+        }
+
+        function help() {
+            const helpText = `
+            Welcome to the JavaScript Playground! Here are some tips to get you started:
+
+            1. Enter your JavaScript code in the textarea.
+            2. Click the "Run Code" button to execute your code.
+            3. The console output will be displayed below the textarea.
+            4. Click the "Clear" button to reset the playground.
+            5. Click the "Download" button to save your code and editor as an HTML file.
+            6. Click the "Share" button to generate a URL to share your code with others.
+            7. You can also press "Cmd + Enter" to run your code.
+            8. There's an empty div above the buttons with the id "playground"
+            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
+            10. You can use the "clear()" function to clear the content's of the console.
+
+            Go nuts! Share scrappy fiddles!
+            `;
+            console.log(helpText);
+        }
+
+        function clear() {
+            document.getElementById('console').innerHTML = '';
+        }
+
+        function mountHelp() {
+            console.log(`
+            The mount function is used to mount stuff to the playground div.
+            It takes a function as an argument, which in turn receives the playground div as an argument.
+            Before mounting, it clears the playground div.
+            Here's an example of how to use the mount function:
+
+            mount(playground => {
+                const h1 = document.createElement('h1');
+                h1.textContent = 'Hell is empty and all the devils are here.';
+                playground.appendChild(h1);
+            });
+
+            This will add an h1 element to the playground div.
+            `);
+        }
+
+        function mount(mountFunction) {
+            const playground = document.getElementById('playground');
+            if (!playground) {
+                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
+                return;
+            }
+
+            if (playground.innerHTML.trim() !== "") {
+                playground.innerHTML = "";
+            }
+            mountFunction(playground);
+        }
+
+
+        document.getElementById('codeInput').addEventListener('keydown', function(event) {
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                evaluateCode();
+            }
+        });
+
+        window.onload = loadCodeFromHash;
+    </script>
+
+
+</body></html>
\ No newline at end of file
diff --git a/html/playground/scheme.html b/html/playground/scheme.html
new file mode 100644
index 0000000..b8ecd6f
--- /dev/null
+++ b/html/playground/scheme.html
@@ -0,0 +1,533 @@
+<html lang="en"><head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>JavaScript Playground</title>
+    <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles.">
+    <style>
+        body {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            background-color: #ddd;
+            padding: 10px;
+            height: 100vh;
+            margin: 0;
+        }
+
+        textarea {
+            width: 100%;
+            height: 64%;
+            font-family: monospace;
+            background-color: #FFFEEC;
+            border: 2px solid #000;
+            scrollbar-width: none;            
+            font-size: 1rem;
+            margin-bottom: 10px;
+            padding: 10px;
+            box-sizing: border-box;
+            resize: none;
+            border-bottom: 12px solid teal;
+            -webkit-user-modify: read-write-plaintext-only;
+        }
+
+        textarea::-webkit-scrollbar {
+            display: none;
+        }
+
+        textarea::selection {
+            background-color: #EFECA7;
+        }
+
+        textarea:focus {
+            outline: none;
+        }
+
+        #console {
+            width: 100%;
+            height: 22%;
+            background-color: #000;
+            color: #0fc;
+            font-family: monospace;
+            font-size: 1rem;
+            overflow-y: auto;
+            padding: 10px;
+            box-sizing: border-box;
+            white-space: pre-wrap;
+        }
+
+        .button-container {
+            width: 100%;
+            display: flex;
+            justify-content: flex-end;
+            margin-bottom: 10px;
+        }
+
+        button {
+            padding: 10px 20px;
+            font-size: 1rem;
+            margin-left: 10px;
+            cursor: pointer;
+            border: none;
+            transition: background-color 0.3s ease;
+        }
+
+        button:hover, button:focus {
+            outline: none; 
+        }
+
+        button.run {
+            background-color: teal;
+            color: #FFFFFF;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="playground" id="playground">    
+        <div class="seesaw" id="seesaw"></div>
+        <div class="slide" id="slide"></div>
+    </div>
+
+    <div class="button-container">
+        <button onclick="clearEverything()">Clear</button>
+        <button onclick="downloadCodeAndEditor()">Download</button>
+        <button onclick="shareCode()">Share</button>
+        <button onclick="evaluateCode()" class="run">Run Code</button>
+    </div>
+    <textarea id="codeInput">function tokenizeScheme(input) {
+    const tokens = [];
+    let current = 0;
+
+    const isWhitespace = (char) => /\s/.test(char);
+    const isDigit = (char) => /[0-9]/.test(char);
+    const isParen = (char) => char === '(' || char === ')';
+    // Symbols can include letters, numbers, and some punctuation like - _ ! ?
+    const isSymbolChar = (char) => /[a-zA-Z0-9\+\-\*\/\=\?\!\_]/.test(char);
+
+    while (current < input.length) {
+        let char = input[current];
+
+        if (isWhitespace(char)) {
+            current++;
+            continue;
+        }
+
+        if (isParen(char)) {
+            tokens.push({ type: 'paren', value: char });
+            current++;
+            continue;
+        }
+
+        if (isDigit(char) || (char === '-' && isDigit(input[current + 1]))) {
+            let number = '';
+            while (isDigit(char) || char === '-') {
+                number += char;
+                char = input[++current];
+            }
+            tokens.push({ type: 'number', value: number });
+            continue;
+        }
+
+        // Handle symbols, including letters, numbers, punctuation
+        if (isSymbolChar(char)) {
+            let symbol = '';
+            while (isSymbolChar(char)) {
+                symbol += char;
+                char = input[++current];
+            }
+            tokens.push({ type: 'symbol', value: symbol });
+            continue;
+        }
+
+        throw new Error(`Unexpected character: ${char}`);
+    }
+
+    return tokens;
+}
+
+
+function parseScheme(tokens) {
+    let current = 0;
+
+    function walk() {
+        let token = tokens[current];
+
+        if (token.type === 'number') {
+            current++;
+            return { type: 'NumberLiteral', value: Number(token.value) };
+        }
+
+        if (token.type === 'symbol') {
+            current++;
+            return { type: 'Symbol', value: token.value };
+        }
+
+        if (token.type === 'paren' && token.value === '(') {
+            current++;
+            const node = { type: 'List', value: [] };
+
+            while (!(tokens[current].type === 'paren' && tokens[current].value === ')')) {
+                node.value.push(walk());
+            }
+
+            current++; // Skip closing ')'
+            return node;
+        }
+
+        throw new Error(`Unexpected token: ${token.type}`);
+    }
+
+    return walk();
+}
+
+const globalEnv = {
+    '+': (...args) => args.reduce((acc, curr) => acc + curr),
+    '-': (...args) => args.reduce((acc, curr) => acc - curr),
+    '*': (...args) => args.reduce((acc, curr) => acc * curr),
+    '/': (a, b) => a / b, // Only two arguments for division
+    'eq?': (...args) => args.every((val, i, arr) => val === arr[0]),
+    'car': (list) => {
+        if (list.type !== 'List' || list.value.length === 0) {
+            throw new Error('car expects a non-empty list');
+        }
+        return list.value[0];
+    },
+    'cdr': (list) => {
+        if (list.type !== 'List' || list.value.length === 0) {
+            throw new Error('cdr expects a non-empty list');
+        }
+        return { type: 'List', value: list.value.slice(1) };
+    },
+    'cons': (a, b) => {
+        if (b.type !== 'List') {
+            throw new Error('cons expects second argument to be a list');
+        }
+        return { type: 'List', value: [a].concat(b.value) };
+    },
+    'null?': (list) => list.type === 'List' && list.value.length === 0,
+    'zero?': (n) => n === 0,
+    'atom?': (x) => typeof x !== 'object' || x === null,
+    'number?': (x) => typeof x === 'number',
+    'add1': (n) => n + 1,
+    'sub1': (n) => n - 1,
+    'quote': (x) => x,  // Simply return the quoted expression
+    'and': (...args) => args.every(Boolean),
+    'or': (...args) => args.some(Boolean),
+    'true': true,
+    'false': false
+};
+
+
+
+
+function evaluate(node, env = globalEnv) {
+    if (node.type === 'NumberLiteral') {
+        return node.value;
+    }
+
+    if (node.type === 'Symbol') {
+        if (env[node.value] !== undefined) {
+            return env[node.value];
+        }
+        throw new Error(`Undefined symbol: ${node.value}`);
+    }
+
+    if (node.type === 'List') {
+        const [first, ...rest] = node.value;
+
+        // Is the first element a symbol, like an operator or function name?
+        if (first.type === 'Symbol') {
+            const operator = first.value;
+
+            // Special case for define
+            if (operator === 'define') {
+                const [symbol, expr] = rest;
+                env[symbol.value] = evaluate(expr, env);
+                return;
+            }
+
+            // Special case for lambda
+            if (operator === 'lambda') {
+                const [params, body] = rest;
+
+                // Create a closure to return
+                return function (...args) {
+                    const lambdaEnv = { ...env };
+
+                    // Bind each argument to the corresponding parameter...
+                    params.value.forEach((param, i) => {
+                        lambdaEnv[param.value] = args[i];
+                    });
+
+                    // ...and then evaluate the body with the environment
+                    return evaluate(body, lambdaEnv);
+                };
+            }
+
+            // Special case for if
+            if (operator === 'if') {
+                const [test, consequent, alternate] = rest;
+                const condition = evaluate(test, env);
+                return condition ? evaluate(consequent, env) : evaluate(alternate, env);
+            }
+
+            // Special case for quote
+            if (operator === 'quote') {
+                return rest[0];  // Return the quoted expression without evaluating it
+            }
+
+            // Special case for cond
+            if (operator === 'cond') {
+                for (let clause of rest) {
+                    const [test, expr] = clause.value;
+                    if (evaluate(test, env)) {
+                        return evaluate(expr, env);
+                    }
+                }
+                return null; // No matching condition
+            }
+
+            // Special case for letrec (recursive let)
+            if (operator === 'letrec') {
+                const [bindings, body] = rest;
+                const letEnv = { ...env };
+
+                // Loop through bindings and evaluate each
+                bindings.value.forEach(binding => {
+                    const [name, expr] = binding.value;
+                    letEnv[name.value] = evaluate(expr, letEnv);
+                });
+
+                return evaluate(body, letEnv);
+            }
+        }
+
+        // Evaluate the first element
+        const func = evaluate(first, env);
+
+        if (typeof func !== 'function') {
+            throw new Error(`Expected a function but got: ${func}`);
+        }
+
+        const args = rest.map(arg => evaluate(arg, env));
+        return func(...args);
+    }
+
+    throw new Error(`Unexpected node type: ${node.type}`);
+}
+
+
+
+function evalScheme(input) {
+    const tokens = tokenizeScheme(input);
+    const ast = parseScheme(tokens);
+    return evaluate(ast);
+}
+
+
+
+
+
+
+
+function mountRepl(playground) {
+    // Create a REPL container
+    const replContainer = document.createElement('div');
+    replContainer.style.display = 'flex';
+    replContainer.style.flexDirection = 'column';
+    replContainer.style.width = '100%';
+
+    // Create an input field for the Scheme expressions
+    const input = document.createElement('textarea');
+    input.placeholder = "Scheme here...";
+    input.style.width = '100%';
+    input.style.height = '100px';
+    input.style.marginBottom = '10px';
+    input.style.fontFamily = 'monospace';
+
+    // Create a button to evaluate the expression
+    const evalButton = document.createElement('button');
+    evalButton.textContent = 'Evaluate';
+
+    // Create a container to display the results
+    const output = document.createElement('pre');
+    output.style.width = '100%';
+    output.style.height = '200px';
+    output.style.overflowY = 'auto';
+    output.style.backgroundColor = '#f0f0f0';
+    output.style.padding = '10px';
+    output.style.fontFamily = 'monospace';
+
+    // Add the input, button, and output to the REPL container
+    replContainer.appendChild(input);
+    replContainer.appendChild(evalButton);
+    replContainer.appendChild(output);
+
+    // Add the REPL container to the playground div
+    playground.appendChild(replContainer);
+
+    evalButton.addEventListener('click', () => {
+        const expression = input.value.trim();
+        if (expression) {
+            try {
+                // Evaluate the expression
+                const result = evalScheme(expression);
+                // Append the result to the output area
+                output.textContent += `> ${expression}\n${result}\n\n`;
+            } catch (error) {
+                // Error if the expression is invalid
+                output.textContent += `> ${expression}\nError: ${error.message}\n\n`;
+            }
+        }
+        // Clear input after evaluation
+        input.value = '';
+    });
+}
+
+
+mount(mountRepl);</textarea>
+    <div id="console"></div>
+
+    <script>
+        function evaluateCode() {
+            const code = document.getElementById('codeInput').value;
+            const consoleDiv = document.getElementById('console');
+            consoleDiv.innerHTML = '';
+
+            // Custom console.log function to output to the console div
+            const originalConsoleLog = console.log;
+            console.log = function(...args) {
+                args.forEach(arg => {
+                    const output = document.createElement('div');
+                    output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg;
+                    consoleDiv.appendChild(output);
+                });
+                originalConsoleLog.apply(console, args);
+            };
+
+            try {
+                eval(code);
+            } catch (error) {
+                const errorOutput = document.createElement('div');
+                errorOutput.textContent = error;
+                errorOutput.style.color = 'red';
+                consoleDiv.appendChild(errorOutput);
+            }
+
+            // Restore browser's console.log
+            console.log = originalConsoleLog;
+        }
+
+        function downloadCodeAndEditor() {
+            const codeInput = document.getElementById('codeInput').value;
+            const htmlContent = document.documentElement.outerHTML.replace(
+                /<textarea id="codeInput"[^>]*>.*<\/textarea>/,
+                `<textarea id="codeInput">${codeInput}</textarea>`
+            );
+
+            const blob = new Blob([htmlContent], { type: 'text/html' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'code_editor.html';
+            document.body.appendChild(a);
+            a.click();
+            document.body.removeChild(a);
+            URL.revokeObjectURL(url);
+        }
+
+        function shareCode() {
+            const code = document.getElementById('codeInput').value;
+            const encodedCode = btoa(encodeURIComponent(code));
+            window.location.hash = encodedCode;
+            window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href);
+        }
+
+        function clearEverything() {
+            if (!confirm('Are you sure you want to reset the playground?')) {
+                return;
+            } else {               
+                window.location.hash = '';
+                window.location.reload();
+            }
+        }
+
+        function loadCodeFromHash() {
+            const hash = window.location.hash.substring(1);
+            if (hash) {
+                try {
+                    const decodedCode = decodeURIComponent(atob(hash));
+                    document.getElementById('codeInput').value = decodedCode;
+                } catch (error) {
+                    console.error('Failed to decode the URL hash:', error);
+                }
+            }
+        }
+
+        function help() {
+            const helpText = `
+            Welcome to the JavaScript Playground! Here are some tips to get you started:
+
+            1. Enter your JavaScript code in the textarea.
+            2. Click the "Run Code" button to execute your code.
+            3. The console output will be displayed below the textarea.
+            4. Click the "Clear" button to reset the playground.
+            5. Click the "Download" button to save your code and editor as an HTML file.
+            6. Click the "Share" button to generate a URL to share your code with others.
+            7. You can also press "Cmd + Enter" to run your code.
+            8. There's an empty div above the buttons with the id "playground"
+            9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()"
+            10. You can use the "clear()" function to clear the content's of the console.
+
+            Go nuts! Share scrappy fiddles!
+            `;
+            console.log(helpText);
+        }
+
+        function clear() {
+            document.getElementById('console').innerHTML = '';
+        }
+
+        function mountHelp() {
+            console.log(`
+            The mount function is used to mount stuff to the playground div.
+            It takes a function as an argument, which in turn receives the playground div as an argument.
+            Before mounting, it clears the playground div.
+            Here's an example of how to use the mount function:
+
+            mount(playground => {
+                const h1 = document.createElement('h1');
+                h1.textContent = 'Hell is empty and all the devils are here.';
+                playground.appendChild(h1);
+            });
+
+            This will add an h1 element to the playground div.
+            `);
+        }
+
+        function mount(mountFunction) {
+            const playground = document.getElementById('playground');
+            if (!playground) {
+                console.error("Couldn't find a div with the id 'playground'! You may need to reload the page.");
+                return;
+            }
+
+            if (playground.innerHTML.trim() !== "") {
+                playground.innerHTML = "";
+            }
+            mountFunction(playground);
+        }
+
+
+        document.getElementById('codeInput').addEventListener('keydown', function(event) {
+            if (event.metaKey && event.key === 'Enter') {
+                event.preventDefault();
+                evaluateCode();
+            }
+        });
+
+        window.onload = loadCodeFromHash;
+    </script>
+
+
+</body></html>
\ No newline at end of file
diff --git a/html/schemer/index.html b/html/schemer/index.html
new file mode 100644
index 0000000..2220e57
--- /dev/null
+++ b/html/schemer/index.html
@@ -0,0 +1,305 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Scheme Interpreter with PDF</title>
+    <style>
+        body, html {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+            font-family: Arial, sans-serif;
+        }
+        #pdf-container {
+            width: 100%;
+            height: 67%;
+            overflow: hidden;
+        }
+        #repl-container {
+            width: 100%;
+            height: 33%;
+            display: flex;
+            flex-direction: column;
+            border-top: 1px solid #ccc;
+            padding: 10px;
+        }
+        textarea {
+            flex: 1;
+            width: 100%;
+            font-family: monospace;
+            font-size: 16px;
+            padding: 10px;
+        }
+        button {
+            margin-top: 10px;
+            padding: 10px;
+            font-size: 16px;
+        }
+        #scheme-output {
+            font-family: monospace;
+            background-color: #f0f0f0;
+            padding: 10px;
+            margin-top: 10px;
+            overflow-wrap: break-word;
+        }
+    </style>
+</head>
+<body>
+
+    <div id="pdf-container">
+        <embed src="tls.pdf" type="application/pdf" width="100%" height="100%">
+    </div>
+
+    <div id="repl-container">
+        <textarea id="scheme-input" placeholder="Scheme here..."></textarea>
+        <div id="scheme-output"></div>
+        <button onclick="evaluateScheme()">Run Code</button>
+    </div>
+
+    <script>
+        function tokenizeScheme(input) {
+            const tokens = [];
+            let current = 0;
+
+            const isWhitespace = (char) => /\s/.test(char);
+            const isDigit = (char) => /[0-9]/.test(char);
+            const isParen = (char) => char === '(' || char === ')';
+            // Symbols can include letters, numbers, and some punctuation like - _ ! ?
+            const isSymbolChar = (char) => /[a-zA-Z0-9\+\-\*\/\=\?\!\_]/.test(char);
+
+            while (current < input.length) {
+                let char = input[current];
+
+                if (isWhitespace(char)) {
+                    current++;
+                    continue;
+                }
+
+                if (isParen(char)) {
+                    tokens.push({ type: 'paren', value: char });
+                    current++;
+                    continue;
+                }
+
+                if (isDigit(char) || (char === '-' && isDigit(input[current + 1]))) {
+                    let number = '';
+                    while (isDigit(char) || char === '-') {
+                        number += char;
+                        char = input[++current];
+                    }
+                    tokens.push({ type: 'number', value: number });
+                    continue;
+                }
+
+                // Handle symbols, including letters, numbers, punctuation
+                if (isSymbolChar(char)) {
+                    let symbol = '';
+                    while (isSymbolChar(char)) {
+                        symbol += char;
+                        char = input[++current];
+                    }
+                    tokens.push({ type: 'symbol', value: symbol });
+                    continue;
+                }
+
+                throw new Error(`Unexpected character: ${char}`);
+            }
+
+            return tokens;
+        }
+
+
+        function parseScheme(tokens) {
+            let current = 0;
+
+            function walk() {
+                let token = tokens[current];
+
+                if (token.type === 'number') {
+                    current++;
+                    return { type: 'NumberLiteral', value: Number(token.value) };
+                }
+
+                if (token.type === 'symbol') {
+                    current++;
+                    return { type: 'Symbol', value: token.value };
+                }
+
+                if (token.type === 'paren' && token.value === '(') {
+                    current++;
+                    const node = { type: 'List', value: [] };
+
+                    while (!(tokens[current].type === 'paren' && tokens[current].value === ')')) {
+                        node.value.push(walk());
+                    }
+
+                    current++; // Skip closing ')'
+                    return node;
+                }
+
+                throw new Error(`Unexpected token: ${token.type}`);
+            }
+
+            return walk();
+        }
+
+        const globalEnv = {
+            '+': (...args) => args.reduce((acc, curr) => acc + curr),
+            '-': (...args) => args.reduce((acc, curr) => acc - curr),
+            '*': (...args) => args.reduce((acc, curr) => acc * curr),
+            '/': (a, b) => a / b, // Only two arguments for division
+            'eq?': (...args) => args.every((val, i, arr) => val === arr[0]),
+            'car': (list) => {
+                if (list.type !== 'List' || list.value.length === 0) {
+                    throw new Error('car expects a non-empty list');
+                }
+                return list.value[0];
+            },
+            'cdr': (list) => {
+                if (list.type !== 'List' || list.value.length === 0) {
+                    throw new Error('cdr expects a non-empty list');
+                }
+                return { type: 'List', value: list.value.slice(1) };
+            },
+            'cons': (a, b) => {
+                if (b.type !== 'List') {
+                    throw new Error('cons expects second argument to be a list');
+                }
+                return { type: 'List', value: [a].concat(b.value) };
+            },
+            'null?': (list) => list.type === 'List' && list.value.length === 0,
+            'zero?': (n) => n === 0,
+            'atom?': (x) => typeof x !== 'object' || x === null,
+            'number?': (x) => typeof x === 'number',
+            'add1': (n) => n + 1,
+            'sub1': (n) => n - 1,
+            'quote': (x) => x,  // Simply return the quoted expression
+            'and': (...args) => args.every(Boolean),
+            'or': (...args) => args.some(Boolean),
+            'true': true,
+            'false': false
+        };
+
+
+
+
+        function evaluate(node, env = globalEnv) {
+            if (node.type === 'NumberLiteral') {
+                return node.value;
+            }
+
+            if (node.type === 'Symbol') {
+                if (env[node.value] !== undefined) {
+                    return env[node.value];
+                }
+                throw new Error(`Undefined symbol: ${node.value}`);
+            }
+
+            if (node.type === 'List') {
+                const [first, ...rest] = node.value;
+
+                // Is the first element a symbol, like an operator or function name?
+                if (first.type === 'Symbol') {
+                    const operator = first.value;
+
+                    // Special case for define
+                    if (operator === 'define') {
+                        const [symbol, expr] = rest;
+                        env[symbol.value] = evaluate(expr, env);
+                        return;
+                    }
+
+                    // Special case for lambda
+                    if (operator === 'lambda') {
+                        const [params, body] = rest;
+
+                        // Create a closure to return
+                        return function (...args) {
+                            const lambdaEnv = { ...env };
+
+                            // Bind each argument to the corresponding parameter...
+                            params.value.forEach((param, i) => {
+                                lambdaEnv[param.value] = args[i];
+                            });
+
+                            // ...and then evaluate the body with the environment
+                            return evaluate(body, lambdaEnv);
+                        };
+                    }
+
+                    // Special case for if
+                    if (operator === 'if') {
+                        const [test, consequent, alternate] = rest;
+                        const condition = evaluate(test, env);
+                        return condition ? evaluate(consequent, env) : evaluate(alternate, env);
+                    }
+
+                    // Special case for quote
+                    if (operator === 'quote') {
+                        return rest[0];  // Return the quoted expression without evaluating it
+                    }
+
+                    // Special case for cond
+                    if (operator === 'cond') {
+                        for (let clause of rest) {
+                            const [test, expr] = clause.value;
+                            if (evaluate(test, env)) {
+                                return evaluate(expr, env);
+                            }
+                        }
+                        return null; // No matching condition
+                    }
+
+                    // Special case for letrec (recursive let)
+                    if (operator === 'letrec') {
+                        const [bindings, body] = rest;
+                        const letEnv = { ...env };
+
+                        // Loop through bindings and evaluate each
+                        bindings.value.forEach(binding => {
+                            const [name, expr] = binding.value;
+                            letEnv[name.value] = evaluate(expr, letEnv);
+                        });
+
+                        return evaluate(body, letEnv);
+                    }
+                }
+
+                // Evaluate the first element
+                const func = evaluate(first, env);
+
+                if (typeof func !== 'function') {
+                    throw new Error(`Expected a function but got: ${func}`);
+                }
+
+                const args = rest.map(arg => evaluate(arg, env));
+                return func(...args);
+            }
+
+            throw new Error(`Unexpected node type: ${node.type}`);
+        }
+
+
+
+        function evalScheme(input) {
+            const tokens = tokenizeScheme(input);
+            const ast = parseScheme(tokens);
+            return evaluate(ast);
+        }
+
+        // Function to evaluate the input in the REPL
+        function evaluateScheme() {
+            const input = document.getElementById('scheme-input').value;
+            let output;
+            try {
+                output = evalScheme(input);
+            } catch (error) {
+                output = `Error: ${error.message}`;
+            }
+            document.getElementById('scheme-output').innerText = JSON.stringify(output, null, 2);
+        }
+    </script>
+
+</body>
+</html>
diff --git a/html/schemer/tls.pdf b/html/schemer/tls.pdf
new file mode 100644
index 0000000..7e28a5f
--- /dev/null
+++ b/html/schemer/tls.pdf
Binary files differdiff --git a/html/squine.html b/html/squine.html
new file mode 100644
index 0000000..cd2c0e1
--- /dev/null
+++ b/html/squine.html
@@ -0,0 +1,1108 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <!--
+MIT License
+
+Copyright (c) m15o
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+-->
+    <meta charset="utf-8">
+    <title>squine</title>
+</head>
+<style>
+    html, body, #app, .column, textarea, table, td {
+        height: 100%;
+    }
+
+    body {
+        padding: 0;
+        margin: 0;
+        background: linear-gradient(to bottom, #e4d16f, #d5363d);
+    }
+
+    header {
+        display: flex;
+        border-bottom: 1px solid;
+        border-top: 1px solid;
+    }
+
+    header > * {
+        border-right: 1px solid;
+    }
+
+    .column {
+        display: flex;
+        flex-direction: column;
+        min-width: 10px;
+    }
+
+    article, section {
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+    }
+
+    .name {
+        box-sizing: border-box;
+        width: 100%;
+        padding: 2px 5px;
+        margin: 0;
+        outline: none;
+        border: 0;
+        border-right: 1px solid;
+        background-color: #fffad6;
+    }
+
+    #search {
+        display: none;
+    }
+
+    #search.searching {
+        display: block;
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        background-color: darkblue;
+        color: white;
+    }
+
+    textarea {
+        box-sizing: border-box;
+        padding: 5px;
+        margin: 0;
+        border: 0;
+        outline: none;
+        resize: none;
+        background-color: #fff9e9;
+    }
+
+    .action {
+        padding: 2px 5px;
+        cursor: pointer;
+    }
+
+    .back, .forward {
+        background-color: silver;
+    }
+
+    .hist {
+        background-color: inherit;
+    }
+
+    .folded article, .folded .h-resize, .maxed .h-resize {
+        display: none;
+    }
+
+    .maxed article {
+        display: flex;
+    }
+
+    .folded .maxed, .maxed article {
+        flex-grow: 1;
+    }
+
+    section:last-child, section:last-child article {
+        flex-grow: 1;
+    }
+
+    .folded section {
+        flex-grow: 0;
+    }
+
+    table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    td {
+        padding: 0;
+        border: 1px solid;
+    }
+
+    .menu > * {
+        padding: 0 4px;
+        cursor: pointer;
+    }
+
+    .menu > *:hover {
+        background-color: #dfad6a;
+    }
+
+    .active-p .content {
+        background-color: white;
+    }
+
+    .active {
+        background-color: #e5d575;
+    }
+
+    .col {
+        position: relative;
+    }
+
+    .h-resize:hover, .resize:hover {
+        background-color: rebeccapurple;
+    }
+
+    .resize {
+        width: 2px;
+        background-color: silver;
+        cursor: col-resize;
+        position: absolute;
+        top: 0;
+        right: 0;
+        bottom: 0;
+    }
+
+    #settings {
+        display: none;
+    }
+
+    #settings.visible {
+        display: table-row;
+    }
+
+    .dirty .save {
+        background-color: aquamarine;
+    }
+
+    .h-resize {
+        height: 2px;
+        background-color: darkgrey;
+        cursor: row-resize;
+    }
+
+    .parse {
+        background-color: #dfad6a;
+    }
+</style>
+<body>
+<div id="app"></div>
+<script>
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Helpers
+
+    function $(elt) {
+        return document.querySelector(elt);
+    }
+
+    function $$(elt) {
+        return document.querySelectorAll(elt);
+    }
+
+    function $$active(elt) {
+        return $panel => ($panel && $panel.querySelector(elt)) || $('.active-p ' + elt);
+    }
+
+    function $name($panel) {
+        return $$active('.name')($panel);
+    }
+
+    function $content($panel) {
+        return $$active('.content')($panel);
+    }
+
+    function t$(e) {
+        return e.target;
+    }
+
+    function closest$(e, sel) {
+        return t$(e).closest(sel);
+    }
+
+    function $search() {
+        return $('#search');
+    }
+
+    function $panel() {
+        return $('.active-p');
+    }
+
+    function hasCls($elt, cls) {
+        return $elt.classList.contains(cls);
+    }
+
+    function date() {
+        return (new Date()).toLocaleDateString('en-CA');
+    }
+
+    function makeElt(elt, cls, attrs, html, value) {
+        let e = document.createElement(elt);
+        cls.forEach(c => e.classList.add(c));
+        Object.keys(attrs).forEach(k => e.setAttribute(k, attrs[k]));
+        html && (e.innerHTML = html);
+        value && (e.value = value);
+        return e;
+    }
+
+    function togCls(cls, sel, $elt) {
+        $$(sel).forEach((e) => e.classList.remove(cls));
+        $elt.classList.add(cls);
+    }
+
+    function setActive($section) {
+        togCls('active-p', 'section', $section);
+    }
+
+    function closeSearch() {
+        $search().classList.remove('searching');
+        $content().focus();
+    }
+
+    function refPane(name) {
+        let p;
+        $$('section').forEach(e => e.querySelector('.name').value.startsWith('+ref') && (p = e));
+        if (!p) {
+            p = createPane();
+        }
+        setActive(p);
+        load(name);
+        return p;
+    }
+
+    function runPane(content) {
+        let p;
+        save();
+        let old = $panel();
+        $$('section').forEach(e => e.querySelector('.name').value.startsWith('+run') && (p = e));
+        let err, rv;
+        storeKeys().forEach(k => {
+            if (k === 'main') return;
+            try {
+                global[k] = eval(global, parse(storeGet(k)), true);
+            } catch (e) {
+                err = `in ${k}: ${e}`
+            }
+        });
+        if (!p) {
+            p = createPane();
+            load('+run');
+        }
+        setActive(p);
+        if (err) {
+            $content().value = err;
+            return;
+        }
+        try {
+            $content().value = asString(exec(storeGet('main')));
+        } catch (e) {
+            $content().value = e;
+        }
+        setActive(old);
+        $content().focus();
+    }
+
+
+    // function runPane(content) {
+    //     let p;
+    //     $$('section').forEach(e => e.querySelector('.name').value.startsWith('+run') && (p = e));
+    //     let err, rv;
+    //     try {
+    //         if ($name().value !== 'main') {
+    //             global[$name().value] = eval(global, parse(content), true);
+    //             rv = asString(global[$name().value]);
+    //         }
+    //     } catch (e) {
+    //         err = e;
+    //     }
+    //
+    //     if (!p) {
+    //         p = createPane();
+    //         load('+run');
+    //     }
+    //     setActive(p);
+    //     if (err) {
+    //         $content().value = err;
+    //         return;
+    //     }
+    //     if (rv) {
+    //         $content().value = rv;
+    //         return;
+    //     }
+    //     try {
+    //         $content().value = asString(exec(content));
+    //     } catch (e) {
+    //         $content().value = e;
+    //     }
+    // }
+
+    function isChordNav(e) {
+        return e.metaKey || e.ctrlKey;
+    }
+
+    function isChordNavBlank(e) {
+        return isChordNav(e) && e.altKey;
+    }
+
+    function navBlank(name) {
+        findOrCreatePane(name);
+    }
+
+    function debounce(fn, ms) {
+        let timeout;
+        return (...args) => {
+            clearTimeout(timeout);
+            timeout = setTimeout(() => fn(...args), ms);
+        }
+    }
+
+    function basename(path) {
+        const n = path.split('/').pop();
+        return n.split('.').shift();
+    }
+
+    function download(data, name, mime) {
+        const link = document.createElement('a');
+        link.download = name;
+        link.href = window.URL.createObjectURL(new Blob([data], {type: mime}));
+        document.body.appendChild(link);
+        link.click();
+        document.body.removeChild(link);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // storage
+
+    let store = {};
+
+    function storeSet(k, v) {
+        if (isStoreKey(k) && v === storeGet(k, v)) return;
+        $('#app').classList.add('dirty');
+        store[k] = v;
+    }
+
+    function storeGet(k) {
+        return store[k] || '';
+    }
+
+    function storeDel(k) {
+        delete store[k];
+    }
+
+    function storeKeys(sorted) {
+        let rv = Object.keys(store);
+        if (sorted) return rv.sort();
+        return rv;
+    }
+
+    function isStoreKey(k) {
+        return storeKeys().includes(k);
+    }
+
+    function storeClear() {
+        store = {};
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Core
+
+    function save() {
+        let name = $name().value;
+        let content = $content().value;
+        $panel().dataset['name'] = name;
+        if (!name || name === '+run') return;
+        storeSet(name, content);
+        refresh();
+    }
+
+    const saveD = debounce(save, 200);
+
+    function updateHistory(name) {
+        let cname = $panel().dataset['name'];
+        if (!name || name === cname) return;
+        hpush(hist().back, cname);
+        hist().forward = [];
+    }
+
+    function ls(prefix) {
+        return storeKeys(true).filter(k => !prefix || k.startsWith(prefix)).join('\n');
+    }
+
+    function orph() {
+        return storeKeys().map(k => {
+            const v = storeGet(k);
+            const m = [...v.matchAll(/\[\[([\w\.\-]+)\]\]/g)].map(match => match[1]);
+            const r = m.filter(e => !storeGet(e));
+            if (!r.length) return null;
+            return ['[[' + k + ']]', '----------', ...r.map(e => '[[' + e + ']]'), ''].join('\n');
+        }).filter(Boolean).join('\n');
+    }
+
+    function load(name, noHist) {
+        if (!name) {
+            $name().value = '';
+            $content().value = '';
+            return;
+        }
+        !noHist && updateHistory(name)
+        if (name === '+orph') {
+            $content().value = orph();
+        } else if (name.startsWith('+ls')) {
+            $content().value = ls(name.split(':')[1]);
+        } else if (name.startsWith('+search')) {
+            $content().value = name.split(':')[1] ? lookup(name.split(':')[1]) : '';
+        } else if (name.startsWith('+ref')) {
+            $content().value = name.split(':')[1] ? lookup('[[' + name.split(':')[1] + ']]') : '';
+        } else {
+            $content().value = storeGet(name);
+        }
+        $name().value = name;
+        $panel().dataset['name'] = name;
+        $panel().querySelector('.back').classList.toggle('hist', !!hist().back.length);
+        $panel().querySelector('.forward').classList.toggle('hist', !!hist().forward.length);
+    }
+
+    function refresh() {
+        let $elt = $('#files');
+        $elt.innerHTML = '';
+        storeKeys().forEach(k => $elt.appendChild(makeElt('option', [], {}, null, k)));
+        $elt = $('#globals');
+        $elt.innerHTML = '';
+        Object.keys(global).forEach(k => $elt.appendChild(makeElt('option', [], {}, null, k)));
+    }
+
+    let paneID = 0;
+
+    function createHistory(paneID) {
+        history[paneID] = {back: [], forward: []};
+    }
+
+    function removeHistory(paneID) {
+        delete history[paneID];
+    }
+
+    function deletePane($pane) {
+        removeHistory($pane.dataset['id']);
+        $pane.remove();
+    }
+
+    function createPane() {
+        const header = document.createElement('header');
+        header.append(
+            makeElt('span', ['back', 'action'], {}, '<'),
+            makeElt('span', ['forward', 'action'], {}, '>'),
+            makeElt('input', ['name'], {list: 'files', autocomplete: 'off'}),
+            makeElt('span', ['max', 'action'], {}, '+'),
+            makeElt('span', ['move', 'action'], {}, '~'),
+            makeElt('span', ['close', 'action'], {}, 'x')
+        );
+        const id = paneID++;
+        const section = document.createElement('section');
+        const article = document.createElement('article');
+        article.append(makeElt('textarea', ['content'], {spellcheck: false, onkeyup: 'saveD()'}));
+        section.setAttribute('data-id', '' + id);
+        section.append(
+            header,
+            makeElt('div', ['parse'], {}),
+            article,
+            makeElt('div', ['h-resize'], {})
+        );
+        createHistory(id);
+        $(".column.active").appendChild(section);
+        setActive(section);
+        $content().focus();
+        return section;
+    }
+
+    function lookup(str) {
+        return storeKeys(true).map(k => {
+            const v = storeGet(k);
+            const m = v.split('\n').filter(l => l.includes(str));
+            if (!m.length) return null;
+            return ['[[' + k + ']]', '----------', ...m, ''].join('\n');
+        }).filter(Boolean).join('\n');
+    }
+
+    function mv(before, after) {
+        return storeKeys().forEach(k => {
+            const v = storeGet(k);
+            storeSet(k, v.replaceAll('[[' + before + ']]', '[[' + after + ']]'))
+        })
+    }
+
+    function link(textarea) {
+        const text = textarea.value;
+        const pos = textarea.selectionStart;
+        let start, end;
+
+        for (start = pos - 1; start > -1 && /[^\s\(\)]/.test(text[start]); start--) {
+        }
+
+        for (end = pos; end < text.length && /[^\s\(\)]/.test(text[end]); end++) {
+        }
+
+        return text.substring(start + 1, end);
+    }
+
+    function insert(textarea, text) {
+        const start = textarea.selectionStart;
+        const end = textarea.selectionEnd;
+        const before = textarea.value.substring(0, start);
+        const after = textarea.value.substring(end, textarea.value.length);
+
+        textarea.value = before + text + after;
+        textarea.selectionStart = textarea.selectionEnd = start + text.length;
+    }
+
+    const write = text => document.execCommand('insertText', false, text);
+
+    const history = {};
+
+    function hist() {
+        return history[$panel().dataset['id']];
+    }
+
+    function hpush(h, name) {
+        if (h[h.length - 1] === name) return;
+        h.push(name);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Menu
+
+    function menuNew() {
+        createPane();
+    }
+
+    function menuLs() {
+        $name() || createPane();
+        load('+ls');
+    }
+
+    function menuReset() {
+        if (confirm('delete everything?')) {
+            storeClear();
+        }
+    }
+
+    function menuRun() {
+        runPane($content().value);
+    }
+
+    function menuMv() {
+        let prev = $panel().dataset['name'];
+        if (prev === $name().value) return;
+        mv(prev, $name().value);
+        save();
+        storeDel(prev);
+        $prevPanel = $panel();
+        $$('section').forEach($pane => {
+            setActive($pane);
+            load($name().value, false);
+        })
+        setActive($prevPanel);
+        refresh();
+    }
+
+    function quine() {
+        const regex = /let store = (.*)/;
+        return ['<!DOCTYPE html>',
+            `<head>${document.head.innerHTML}</head>`,
+            '<body>',
+            '<div id="app"></div>',
+            `<script>${$('script').innerHTML.replace(regex, "let store = " + JSON.stringify(store)
+                .replaceAll('</' + 'script', "' + '</' + 'script' + '") + ';') + '</'}script>`,
+            '</body>'].join('\n');
+    }
+
+    function menuSave() {
+        $('#app').classList.remove('dirty');
+        download(quine(), basename(window.location.href) + '.html', 'text/html');
+    }
+
+    function menuExport() {
+        download(JSON.stringify(store), basename(window.location.href) + '.json', 'text/json');
+    }
+
+    function menuDel() {
+        let name = $name().value;
+        if (name && confirm('delete ' + name + '?')) {
+            storeDel(name);
+            refresh();
+            deletePane($panel());
+        }
+    }
+
+    function menuSettings() {
+        $('#settings').classList.toggle('visible');
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Pane actions
+
+    function paneFold(e) {
+        closest$(e, 'section').classList.toggle('folded');
+    }
+
+    function paneMax(e) {
+        if (closest$(e, 'section').classList.contains('maxed')) {
+            closest$(e, 'section').classList.remove('maxed');
+            closest$(e, '.column').classList.remove('folded');
+        } else {
+            closest$(e, '.column').querySelectorAll('section').forEach(e => e.classList.remove("maxed"));
+            closest$(e, 'section').classList.add("maxed");
+            closest$(e, '.column').classList.add('folded');
+        }
+    }
+
+    function paneMove(e) {
+        let $c;
+        $$('.column').forEach(c => {
+            if (c !== closest$(e, '.column')) $c = c;
+        });
+        $c.appendChild(closest$(e, 'section'));
+    }
+
+    function paneClose(e) {
+        deletePane(closest$(e, 'section'));
+        if ($('section')) {
+            setActive($('section'));
+            $content().focus;
+        }
+    }
+
+    function paneHist(from, to) {
+        return function (e) {
+            if (!from.length) return;
+            let name = from.pop();
+            if (isChordNavBlank(e)) {
+                from.push(name);
+                createPane();
+            } else if (isChordNav(e)) {
+                from.push(name);
+                setActive($prevPanel);
+                load(name);
+                $content().focus();
+            } else {
+                hpush(to, $panel().dataset['name']);
+            }
+            load(name, true);
+        }
+    }
+
+    function findOrCreatePane(name) {
+        let p;
+        $$('section').forEach(e => e.querySelector('.name').value === name && (p = e));
+        if (!p) {
+            p = createPane();
+        }
+        setActive(p);
+        load(name);
+        return p;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Scheme interpreter
+
+    const global = {};
+    const arity = (fn, n) => (s, env) => {
+        if (collect(cdr(s)).length !== n) throw new Error(`${asString(car(s))}: Wrong number of arguments`);
+        return fn(s, env);
+    }
+    const primitive = {
+        define: arity((s, env) => {
+            return env[cadr(s)] = eval(env, cadr(cdr(s)));
+        }, 2),
+        quote: arity((s, env) => cadr(s), 1),
+        car: arity((s, env) => {
+            let rv = eval(env, cadr(s));
+            if (rv === null) throw new Error('You cannot ask for the car of the empty list');
+            else if (isAtom(rv)) throw new Error('You cannot ask for the car of an atom');
+            return car(rv)
+        }, 1),
+        cdr: arity((s, env) => {
+            let rv = eval(env, cadr(s));
+            if (rv === null) throw new Error('You cannot ask for the cdr of the empty list');
+            if (isAtom(rv)) throw new Error('You cannot ask for the cdr of an atom');
+            return cdr(rv)
+        }, 1),
+        cons: arity((s, env) => {
+            let r = eval(env, cadr(cdr(s)));
+            if (!isList(r)) throw new Error('Second argument of cons must be a list');
+            return cons(eval(env, cadr(s)), r);
+        }, 2),
+        'null?': arity((s, env) => {
+            let r = eval(env, cadr(s));
+            if (!isList(r)) throw new Error('null? is defined only for lists');
+            return r === null;
+        }, 1),
+        'zero?': arity((s, env) => {
+            let r = eval(env, cadr(s));
+            if (!isAtom(r)) throw new Error('zero? is defined only for atoms');
+            return r === 0;
+        }, 1),
+        'atom?': arity((s, env) => {
+            return isAtom(eval(env, cadr(s)));
+        }, 1),
+        'number?': arity((s, env) => {
+            return isNumber(eval(env, cadr(s)));
+        }, 1),
+        'eq?': arity((s, env) => {
+            let l = eval(env, cadr(s));
+            let r = eval(env, cadr(cdr(s)));
+            if (!isAtom(l) || isNumber(l) || !isAtom(r) || isNumber(r)) throw new Error('eq? takes two non-numeric atoms');
+            return l === r;
+        }, 2),
+        or: arity((s, env) => {
+            return eval(env, cadr(s)) || eval(env, cadr(cdr(s)));
+        }, 2),
+        and: arity((s, env) => {
+            return eval(env, cadr(s)) && eval(env, cadr(cdr(s)));
+        }, 2),
+        add1: arity((s, env) => eval(env, cadr(s)) + 1, 1),
+        sub1: arity((s, env) => eval(env, cadr(s)) - 1, 1),
+        lambda: arity((s, env) => {
+            return {
+                args: collect(cadr(s)), body: cadr(cdr(s)),
+                env: Object.fromEntries(Object.entries(env))
+            }
+        }, 2),
+        letrec: arity((s, env) => {
+            let f = eval(env, car(cdr(car(cadr(s)))));
+            f.env[car(car(car(cdr(s))))] = f;
+            return eval(f.env, car(cdr(cdr(s))));
+        }, 2),
+        cond: (s, env) => {
+            let b = collect(cdr(s));
+            for (let i = 0; i < b.length; i++) {
+                if (car(b[i]) === 'else' || eval(env, car(b[i])))
+                    return eval(env, cadr(b[i]));
+            }
+        }
+    }
+    const cons = (car, cdr) => [car, cdr];
+    const car = c => c[0];
+    const cdr = c => c[1];
+    const cadr = c => car(cdr(c));
+    const isAtom = s => s !== null && !Array.isArray(s);
+    const isList = s => Array.isArray(s) || s === null;
+    const isNumber = s => typeof s === 'number';
+    const parse = src => ast(src.replaceAll('(', ' ( ').replaceAll(')', ' ) ').replaceAll(/;.*$/gm, '').split(/\s/).filter(Boolean));
+    const exec = src => eval(global, parse(src), true);
+
+    function ast(tokens, d = {depth: 0}) {
+        if (!tokens.length) {
+            if (d.depth > 0) throw new Error('Unexpected EOF!');
+            else return null;
+        }
+        let t = tokens.shift();
+        if (t === ')') {
+            if (d.depth-- === 0) throw new Error('Unexpected closing parenthesis');
+            return null;
+        } else if (t === '(') {
+            d.depth++;
+            return cons(ast(tokens, d), ast(tokens, d));
+        } else if (t[0] === '#') {
+            if (tokens.length) return cons(t[1] === 't', ast(tokens, d));
+            return t[1] === 't';
+        } else if (isNaN(t)) {
+            if (tokens.length) return cons(t, ast(tokens, d));
+            return t;
+        }
+        if (tokens.length) return cons(+t, ast(tokens, d));
+        return +t;
+    }
+
+    function collect(s) {
+        if (!s) return [];
+        return [car(s), ...collect(cdr(s))];
+    }
+
+    function eval(env, s, isSrc) {
+        if (s === null) throw new Error('Evaluating empty list');
+        if (isAtom(s)) {
+            if (isNaN(s)) {
+                if (env[s] !== undefined) return env[s];
+                else if (Object.keys(primitive).includes(s)) return s;
+                throw new Error('Undefined variable ' + s);
+            }
+            return s;
+        } else if (isSrc) {
+            let rv = eval(env, car(s));
+            if (cdr(s)) return eval(env, cdr(s), isSrc);
+            return rv;
+        }
+        let proc = eval(env, car(s));
+        if (Object.keys(primitive).includes(proc || car(s))) {
+            return primitive[proc || car(s)](s, env)
+        }
+        try {
+            let args = {};
+            collect(cdr(s)).forEach((a, i) => {
+                args[proc.args[i]] = eval(env, a);
+            });
+            return arity((_, env) => eval(env, proc.body), proc.args.length)(s, Object.assign({}, env, proc.env, args));
+        } catch (e) {
+            throw new Error(`${asString(car(s))}: ${e}`);
+        }
+    }
+
+    function asString(s) {
+        function build(s, acc = [], nl = true) {
+            if (s === null) {
+                acc.push('()');
+                return acc;
+            } else if (s.body) {
+                acc.push('<proc (' + s.args.join(' ') + ')>');
+                return acc;
+            } else if (isAtom(s)) {
+                acc.push(s);
+                return acc;
+            } else {
+                nl && acc.push('(');
+                build(s[0], acc, true);
+                s[1] && build(s[1], acc, false);
+                nl && acc.push(')');
+            }
+            return acc;
+        }
+
+        return build(s).join(' ').replaceAll('( ', '(').replaceAll(' )', ')');
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Listeners
+
+    let $prevPanel;
+    let $resize;
+    let $hresize;
+
+    function handleSearch(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        if (isChordNavBlank(e)) {
+            navBlank(t$(e).value);
+            closeSearch();
+        } else if (isChordNav(e)) {
+            if (!$content()) createPane();
+            closeSearch();
+            load(t$(e).value);
+        } else {
+            if (!$content()) return;
+            closeSearch();
+            if (!isStoreKey(t$(e).value)) {
+                storeSet(t$(e).value, '');
+                refresh();
+            }
+            write(t$(e).value);
+        }
+    }
+
+    document.getElementById('app').innerHTML = `
+ <datalist id="files"></datalist>
+ <datalist id="globals"></datalist>
+<table>
+    <tr>
+        <td colspan="2" class="menu" style="height: 0;">
+            <span class="new">new</span><span class="ls">ls</span><span class="mv">mv</span><span class="del">del</span><span class="save">save</span><span class="run">run</span><span class="settings">import</span><span class="export">export</span><span class="reset">reset</span>
+        </td>
+    </tr>
+    <tr id="settings">
+        <td colspan="2" class="menu" style="height: 0;">
+            <input type="file" id="import" value="">
+        </td>
+    </tr>
+    <tr>
+        <td class="col">
+            <div class="active column"></div>
+            <div class="resize"></div>
+        </td>
+        <td>
+            <div class="column"></div>
+        </td>
+    </tr>
+</table>
+<input list="files" spellcheck="false" id="search" autocomplete="off"/>
+    `;
+
+    document.addEventListener('mousedown', function (e) {
+        if (hasCls(t$(e), 'resize')) {
+            e.preventDefault();
+            $resize = closest$(e, 'td');
+        } else if (hasCls(t$(e), 'h-resize')) {
+            e.preventDefault();
+            $hresize = closest$(e, 'section').querySelector('article');
+        }
+        $prevPanel = $panel() || $prevPanel;
+        let section = closest$(e, 'section');
+        section && setActive(section);
+        let column = closest$(e, '.column');
+        if (column && (!e.altKey && !e.metaKey && !e.ctrlKey)) {
+            togCls('active', '.column', column);
+        }
+    });
+
+    document.addEventListener('mousemove', function (e) {
+        if ($resize) $resize.width = e.clientX;
+        else if ($hresize) {
+            const $sib = $hresize.closest('section').nextSibling;
+            const rect = $hresize.getBoundingClientRect();
+            let h = e.y - rect.top < 0 ? 0 : e.y - rect.top;
+            let diff = h - rect.height;
+            if ($sib && $sib.nextSibling) {
+                const $sArt = $sib.querySelector('article');
+                const sRect = $sArt.getBoundingClientRect();
+                if (sRect.height - diff <= 0) {
+                    $hresize.style.height = `${+$hresize.style.height.split('px')[0] + sRect.height}px`;
+                    $sArt.style.height = `0px`;
+                    return;
+                }
+                $sArt.style.height = `${sRect.height - diff}px`;
+            }
+            $hresize.style.height = `${h}px`;
+        }
+    })
+
+    document.addEventListener('mouseup', () => {
+        $resize = null;
+        $hresize = null;
+    });
+
+    document.addEventListener('click', function (e) {
+        // @formatter:off
+        switch (t$(e).classList[0]) {
+            case 'new': menuNew(); return;
+            case 'ls': menuLs(); return;
+            case 'reset': menuReset(); return;
+            case 'save': menuSave(); return;
+            case 'mv': menuMv(); return;
+            case 'del': menuDel(); return;
+            case 'settings': menuSettings(); return;
+            case 'run': menuRun(); return;
+            case 'export': menuExport(); return;
+            case 'close': paneClose(e); return;
+            case 'max': paneMax(e); return;
+            case 'move': paneMove(e); return;
+            case 'back': paneHist(hist().back, hist().forward)(e); return;
+            case 'forward': paneHist(hist().forward, hist().back)(e); return;
+        }
+        // @formatter:on
+        if (isChordNavBlank(e) && e.button === 0) {
+            link($content()) && navBlank(link($content()));
+        } else if (isChordNav(e) && e.button === 0) {
+            let name = link($content());
+            if (name) {
+                setActive($prevPanel);
+                load(name);
+                $content().focus();
+            }
+        }
+    });
+
+    window.addEventListener('beforeunload', e => {
+        if ($('.dirty')) {
+            e.preventDefault();
+            e.returnValue = true;
+        }
+    })
+
+    function offset(text) {
+        const stack = [];
+        let o = 0;
+        text.split('').forEach((c, i) => {
+            if (c === '(') stack.push({spaces: i - o, total: i});
+            else if (c === ')') stack.pop();
+            else if (c === '\n') o = i + 1;
+        });
+        return stack.pop() || 0;
+    }
+
+    const move = (e, pos) => e.setSelectionRange(pos, pos);
+    const caret = e => e.value.substring(0, e.selectionStart);
+    let last;
+
+    document.addEventListener('keydown', function (e) {
+        if (e.key === 'Enter') {
+            if (t$(e).id === 'search') {
+                handleSearch(e);
+            } else if (isChordNav(e)) {
+                $search().value = '';
+                $search().classList.add('searching');
+                $search().focus();
+            } else if (hasCls(e.target, 'name')) {
+                load($name().value);
+            } else {
+                e.preventDefault();
+                let s = offset(caret(t$(e)));
+                if (s.spaces === undefined) write('\n')
+                else write('\n' + Array(s.spaces + 3).join(' '));
+            }
+        } else if (e.key === 'Escape' && e.target.id === 'search') {
+            $search().classList.remove('searching');
+            $content().focus();
+        } else if (t$(e).classList[0] === 'content') {
+            const textarea = t$(e);
+            if (e.key === '(') {
+                e.preventDefault();
+                write('()');
+                move(textarea, textarea.selectionStart - 1);
+            }
+            if (e.key === '\'') {
+                e.preventDefault();
+                write('(quote )');
+                move(textarea, textarea.selectionStart - 1);
+            } else if (e.key === 'Tab') {
+                e.preventDefault();
+                let c = caret(textarea);
+                if (c[c.length - 1] === '(') {
+                    return move(textarea, last);
+                }
+                let o = offset(c);
+                if (o.spaces === undefined) return;
+                last = c.length;
+                move(textarea, o.total + 1);
+            } else if (e.key === 'r' && (e.metaKey || e.ctrlKey)) {
+                runPane(textarea.value);
+            }
+        }
+    });
+
+    document.getElementById('import').addEventListener('change', function (e) {
+        const file = e.target.files[0];
+        const r = new FileReader();
+        r.onload = e => {
+            let data = JSON.parse('' + e.target.result);
+            Object.keys(data).forEach(k => storeSet(k, data[k]));
+            refresh();
+        }
+        r.readAsText(file);
+        refresh();
+    });
+
+    document.addEventListener('keyup', function (e) {
+        if (t$(e).className === 'content') {
+            let $parse = closest$(e, 'section').querySelector('.parse');
+            try {
+                parse($content().value);
+                $parse.innerHTML = '';
+            } catch (e) {
+                $parse.innerHTML = e;
+            }
+        }
+    })
+
+    refresh();
+    createPane();
+    load((location.hash && location.hash.substring(1)) || 'main');
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/html/tuner/index.html b/html/tuner/index.html
new file mode 100644
index 0000000..20bf125
--- /dev/null
+++ b/html/tuner/index.html
@@ -0,0 +1,193 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Tuner</title>
+    <style>
+        * {
+            box-sizing: border-box;
+            margin: 0;
+            padding: 0;
+        }
+        body {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            min-height: 100vh;
+            margin: 0;
+            font-family: Arial, sans-serif;
+            background-color: beige;
+        }
+        canvas {
+            border: none;
+            width: 100%;
+            max-width: 100%;
+            height: auto;
+        }
+        #note {
+            margin-top: 10px;
+            font-size: 18px;
+            font-weight: bold;
+            text-align: center;
+        }
+        #buttons {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: center;
+            margin-top: 10px;
+        }
+        button {
+            margin: 5px;
+            padding: 10px;
+            font-size: 16px;
+            flex: 1 1 25%;
+            max-width: 100px;
+        }
+        @media (max-width: 600px) {
+            button {
+                flex: 1 1 33.33%;
+            }
+        }
+    </style>
+</head>
+<body>
+
+    <canvas id="waveformCanvas"></canvas>
+    <div id="note">Note: N/A</div>
+
+    <div id="buttons"></div>
+
+    <script>
+        const canvas = document.getElementById('waveformCanvas');
+        const canvasCtx = canvas.getContext('2d');
+        const noteDisplay = document.getElementById('note');
+
+        let audioContext;
+        let analyzer;
+        let dataArray;
+        let bufferLength;
+
+        // Note frequencies in Hz for A4 = 440Hz
+        const notesFrequencies = {
+            'C': 261.63,
+            'C#': 277.18,
+            'D': 293.66,
+            'D#': 311.13,
+            'E': 329.63,
+            'F': 349.23,
+            'F#': 369.99,
+            'G': 392.00,
+            'G#': 415.30,
+            'A': 440.00,
+            'A#': 466.16,
+            'B': 493.88
+        };
+
+        navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
+            audioContext = new (window.AudioContext || window.webkitAudioContext)();
+            const source = audioContext.createMediaStreamSource(stream);
+
+            analyzer = audioContext.createAnalyser();
+            analyzer.fftSize = 2048;
+            bufferLength = analyzer.frequencyBinCount;
+            dataArray = new Uint8Array(bufferLength);
+
+            source.connect(analyzer);
+            drawWaveform();
+            setInterval(detectNote, 500);
+        }).catch(err => {
+            console.error('Error accessing the microphone:', err);
+        });
+
+        function resizeCanvas() {
+            canvas.width = window.innerWidth - 20;
+            canvas.height = window.innerHeight / 4;
+        }
+        window.addEventListener('resize', resizeCanvas);
+        resizeCanvas();
+
+        function drawWaveform() {
+            requestAnimationFrame(drawWaveform);
+
+            analyzer.getByteTimeDomainData(dataArray);
+
+            canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
+            canvasCtx.beginPath();
+
+            const sliceWidth = canvas.width / bufferLength;
+            let x = 0;
+
+            for (let i = 0; i < bufferLength; i++) {
+                const v = dataArray[i] / 128.0;
+                const y = v * canvas.height / 2;
+
+                if (i === 0) {
+                    canvasCtx.moveTo(x, y);
+                } else {
+                    canvasCtx.lineTo(x, y);
+                }
+
+                x += sliceWidth;
+            }
+
+            canvasCtx.lineTo(canvas.width, canvas.height / 2);
+            canvasCtx.stroke();
+        }
+
+        // Detect the dominant frequency and map it to a musical note
+        function detectNote() {
+            const freqArray = new Float32Array(analyzer.frequencyBinCount);
+            analyzer.getFloatFrequencyData(freqArray);
+
+            let maxAmp = -Infinity;
+            let maxIndex = 0;
+
+            for (let i = 0; i < freqArray.length; i++) {
+                if (freqArray[i] > maxAmp) {
+                    maxAmp = freqArray[i];
+                    maxIndex = i;
+                }
+            }
+
+            // Nyquist frequency is half the sample rate of a signal
+            const nyquist = audioContext.sampleRate / 2;
+            const frequency = maxIndex * nyquist / freqArray.length;
+
+            const note = getNoteFromFrequency(frequency);
+            noteDisplay.textContent = `Note: ${note}`;
+        }
+
+        // Convert frequency to musical note
+        function getNoteFromFrequency(frequency) {
+            const notes = [
+                'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'
+            ];
+            const A4 = 440;
+            const semitoneRatio = Math.pow(2, 1 / 12);
+            const noteIndex = Math.round(12 * Math.log2(frequency / A4));
+            const note = notes[(noteIndex % 12 + 12) % 12];
+
+            return frequency ? note : 'N/A';
+        }
+
+        const buttonsContainer = document.getElementById('buttons');
+        Object.keys(notesFrequencies).forEach(note => {
+            const button = document.createElement('button');
+            button.textContent = note;
+            button.onclick = () => playNote(notesFrequencies[note]);
+            buttonsContainer.appendChild(button);
+        });
+
+        function playNote(frequency) {
+            const osc = audioContext.createOscillator();
+            osc.type = 'sine'; // Valid values include 'sine', 'square', 'triangle', 'sawtooth'
+            osc.frequency.setValueAtTime(frequency, audioContext.currentTime); // Frequency in Hz
+            osc.connect(audioContext.destination);
+            osc.start();
+            osc.stop(audioContext.currentTime + 1); // Play the note for 1 second
+        }
+    </script>
+</body>
+</html>