about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--html/broughlike/broughlike.js204
-rw-r--r--html/mountain/game.js810
-rw-r--r--html/mountain/index.html11
-rw-r--r--html/plains/game.js486
-rw-r--r--html/plains/index.html26
-rw-r--r--rust/bf/.vscode/launch.json15
-rw-r--r--rust/bf/src/main.rs6
7 files changed, 1431 insertions, 127 deletions
diff --git a/html/broughlike/broughlike.js b/html/broughlike/broughlike.js
index 5e82ebb..7efa67d 100644
--- a/html/broughlike/broughlike.js
+++ b/html/broughlike/broughlike.js
@@ -671,8 +671,6 @@ function resetGame() {
     generateEnemies();
     generateItems();
     render();
-    if (AUTO_PLAY)
-        autoPlay();
 }
 
 function checkPlayerAtExit() {
@@ -695,8 +693,6 @@ function checkPlayerAtExit() {
         generateEnemies();
         generateItems();
         render();
-        if (AUTO_PLAY)
-            autoPlay();
     }
 }
 
@@ -792,14 +788,14 @@ render();
 
 
 
-
+let AUTO_PLAY = false;
+let autoPlayInterval = null;
 
 function autoPlay() {
     let lastPosition = { x: player.x, y: player.y };
     let stuckCounter = 0;
     
     const playerAtExit = () => player.x === exit.x && player.y === exit.y;
-    const playerCanMove = (dx, dy) => isValidMove(player.x + dx, player.y + dy);
     
     const checkIfStuck = () => {
         if (lastPosition.x === player.x && lastPosition.y === player.y) {
@@ -808,160 +804,120 @@ function autoPlay() {
             stuckCounter = 0;
             lastPosition = { x: player.x, y: player.y };
         }
-        return stuckCounter > 3; // Consider yourself stuck after 3 turns in the same position
+        return stuckCounter > 3;
     };
 
-    const desperateEscape = () => {
-        const allDirections = [
-            {dx: 1, dy: 0}, {dx: -1, dy: 0},
-            {dx: 0, dy: 1}, {dx: 0, dy: -1},
-            {dx: 1, dy: 1}, {dx: -1, dy: 1},
-            {dx: 1, dy: -1}, {dx: -1, dy: -1}
-        ];
-        
-        allDirections.sort(() => Math.random() - 0.5);
-        
-        for (const {dx, dy} of allDirections) {
-            if (playerCanMove(dx, dy)) {
-                movePlayer(dx, dy);
-                return true;
+    const findSafestPath = (target) => {
+        const path = findPath(player, target);
+        if (path.length <= 1) {
+            // If you can't find a path, find the nearest enemy
+            const nearestEnemy = enemies.reduce((closest, enemy) => {
+                const distToCurrent = Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y);
+                const distToClosest = closest ? Math.abs(closest.x - player.x) + Math.abs(closest.y - player.y) : Infinity;
+                return distToCurrent < distToClosest ? enemy : closest;
+            }, null);
+
+            if (nearestEnemy) {
+                return [{x: player.x, y: player.y}, {x: nearestEnemy.x, y: nearestEnemy.y}];
             }
+            return null;
         }
-        return false;
-    };
 
-    const hasAdjacentEnemy = (x, y) => {
-        return enemies.some(enemy => 
-            Math.abs(enemy.x - x) + Math.abs(enemy.y - y) === 1
+        const nextStep = path[1];
+        const adjacentEnemies = enemies.filter(enemy => 
+            Math.abs(enemy.x - nextStep.x) + Math.abs(enemy.y - nextStep.y) <= 1
         );
-    };
-
-    const findNearestItem = () => {
-        if (items.length === 0) return null;
-        return items.reduce((nearest, item) => {
-            const distToCurrent = Math.abs(player.x - item.x) + Math.abs(player.y - item.y);
-            const distToNearest = nearest ? Math.abs(player.x - nearest.x) + Math.abs(player.y - nearest.y) : Infinity;
-            return distToCurrent < distToNearest ? item : nearest;
-        }, null);
-    };
 
-    const decideNextTarget = () => {
-
-        const healingItem = items.find(item => 
-            item.type === 'pentagon' && 
-            player.health < CONFIG.PLAYER_HEALTH * 0.5
-        );
-        if (healingItem) return healingItem;
+        if (adjacentEnemies.length > 0 && player.health < 3) {
+            const alternativePaths = [
+                {dx: 1, dy: 0}, {dx: -1, dy: 0},
+                {dx: 0, dy: 1}, {dx: 0, dy: -1}
+            ].filter(({dx, dy}) => {
+                const newX = player.x + dx;
+                const newY = player.y + dy;
+                return isValidMove(newX, newY) && 
+                       !enemies.some(e => Math.abs(e.x - newX) + Math.abs(e.y - newY) <= 1);
+            });
 
-        const nearestItem = findNearestItem();
-        if (nearestItem && Math.abs(player.x - nearestItem.x) + Math.abs(player.y - nearestItem.y) < 5) {
-            return nearestItem;
+            if (alternativePaths.length > 0) {
+                const randomPath = alternativePaths[Math.floor(Math.random() * alternativePaths.length)];
+                return [{x: player.x, y: player.y}, {x: player.x + randomPath.dx, y: player.y + randomPath.dy}];
+            }
         }
 
-        return exit;
+        return path;
     };
 
     const moveTowardsTarget = (target) => {
-        const path = findPath(player, target);
-        if (path.length > 1) {
+        const path = findSafestPath(target);
+        if (path && path.length > 1) {
             const nextStep = path[1];
-            
-            if (Math.random() < 0.2) {
-                const alternateDirections = [
-                    {dx: 1, dy: 0}, {dx: -1, dy: 0},
-                    {dx: 0, dy: 1}, {dx: 0, dy: -1}
-                ].filter(({dx, dy}) => 
-                    playerCanMove(dx, dy) && 
-                    !hasAdjacentEnemy(player.x + dx, player.y + dy)
-                );
-                
-                if (alternateDirections.length > 0) {
-                    const randomDir = alternateDirections[Math.floor(Math.random() * alternateDirections.length)];
-                    movePlayer(randomDir.dx, randomDir.dy);
-                    return true;
-                }
-            }
-            
-            if (!hasAdjacentEnemy(nextStep.x, nextStep.y)) {
-                const dx = nextStep.x - player.x;
-                const dy = nextStep.y - player.y;
-                if (playerCanMove(dx, dy)) {
-                    movePlayer(dx, dy);
-                    return true;
-                }
-            }
+            const dx = nextStep.x - player.x;
+            const dy = nextStep.y - player.y;
+            movePlayer(dx, dy);
+            return true;
         }
         return false;
     };
 
-    const handleCombat = () => {
-        const adjacentEnemy = enemies.find(enemy => 
-            Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y) === 1
-        );
-        
-        if (adjacentEnemy) {
-            // Increase the chance of retreating when stuck
-            const retreatChance = checkIfStuck() ? 0.8 : 0.3;
-            
-            if (Math.random() < retreatChance) {
-                const retreatDirections = [
-                    {dx: 1, dy: 0}, {dx: -1, dy: 0},
-                    {dx: 0, dy: 1}, {dx: 0, dy: -1}
-                ];
-                
-                retreatDirections.sort(() => Math.random() - 0.5);
-                
-                for (const {dx, dy} of retreatDirections) {
-                    if (playerCanMove(dx, dy) && !hasAdjacentEnemy(player.x + dx, player.y + dy)) {
-                        movePlayer(dx, dy);
-                        return true;
-                    }
-                }
-            }
-            
-            // If stuck, try desperate escape
-            if (checkIfStuck()) {
-                return desperateEscape();
+    const findBestTarget = () => {
+        // If health is low, prioritize healing items
+        if (player.health < 3) {
+            const healingItem = items.find(item => item.type === 'pentagon');
+            if (healingItem && findPath(player, healingItem).length > 0) {
+                return healingItem;
             }
-            
-            // Attack if can't retreat
-            const dx = adjacentEnemy.x - player.x;
-            const dy = adjacentEnemy.y - player.y;
-            movePlayer(dx, dy);
-            return true;
         }
-        return false;
+
+        // If there's a nearby damage boost and we're healthy, grab it
+        const damageItem = items.find(item => 
+            item.type === 'diamond' && 
+            Math.abs(item.x - player.x) + Math.abs(item.y - player.y) < 5
+        );
+        if (damageItem && player.health > 2) {
+            return damageItem;
+        }
+
+        // Default to exit
+        return exit;
     };
 
     const play = () => {
-        if (playerAtExit()) {
+
+        if (!AUTO_PLAY) {
+            clearTimeout(autoPlayInterval);
+            autoPlayInterval = null;
             return;
         }
 
-        // If stuck, try desperate escape
-        if (checkIfStuck() && desperateEscape()) {
-            // Successfully escaped
-        } else if (!handleCombat()) {
-            // If no combat, move towards the next target
-            const target = decideNextTarget();
+        if (playerAtExit()) return;
+
+        if (checkIfStuck()) {
+            const directions = [{dx: 1, dy: 0}, {dx: -1, dy: 0}, {dx: 0, dy: 1}, {dx: 0, dy: -1}];
+            const validDirections = directions.filter(({dx, dy}) => isValidMove(player.x + dx, player.y + dy));
+            if (validDirections.length > 0) {
+                const {dx, dy} = validDirections[Math.floor(Math.random() * validDirections.length)];
+                movePlayer(dx, dy);
+            }
+        } else {
+            const target = findBestTarget();
             moveTowardsTarget(target);
         }
 
-        setTimeout(play, 400); // 400ms is about 1.5 moves per second because 1000ms was terribly slow.
+        autoPlayInterval = setTimeout(play, 400);
     };
 
     play();
 }
 
-let AUTO_PLAY = false;
-
 document.addEventListener('keydown', (e) => {
     if (e.key === 'v') {
-        // alert to confirm that the user wants to toggle auto play
-        if (confirm("Are you sure you want to toggle auto play? Once auto play is turned on, the only way to turn it off is to reload the page.")) {
-            AUTO_PLAY = !AUTO_PLAY;
-            if (AUTO_PLAY)
-                autoPlay();
+        AUTO_PLAY = !AUTO_PLAY;
+        if (AUTO_PLAY) {
+            console.log("Auto-play on");
+            autoPlay();
+        } else {
+            console.log("Auto-play off");
         }
     }
 });
\ No newline at end of file
diff --git a/html/mountain/game.js b/html/mountain/game.js
new file mode 100644
index 0000000..39fff3b
--- /dev/null
+++ b/html/mountain/game.js
@@ -0,0 +1,810 @@
+/* ================================
+
+45
+
+There's something quieter than sleep
+Within this inner room!
+It wears a sprig upon its breast—
+And will not tell its name.
+
+Some touch it, and some kiss it—
+Some chafe its idle hand—
+It has a simple gravity
+I do not understand!
+
+I would not weep if I were they—
+How rude in one to sob!
+Might scare the quiet fairy
+Back to her native wood!
+
+While simple-hearted neighbors
+Chat of the "Early dead"—
+We—prone to periphrasis
+Remark that Birds have fled!
+
+Emily Dickinson
+
+================================*/
+
+const canvas = document.createElement('canvas');
+const ctx = canvas.getContext('2d');
+document.body.appendChild(canvas);
+canvas.style.display = 'block';
+canvas.style.position = 'fixed';
+canvas.style.top = '0';
+canvas.style.left = '0';
+
+const GAME_STATE = {
+    PLAYING: 'playing',
+    GAME_OVER: 'game_over' // (ノಠ益ಠ)ノ
+};
+
+const PLATFORM_TYPE = {
+    NORMAL: 'normal',
+    DEADLY: 'deadly',
+    FALLING: 'falling'
+};
+
+const PLAYER_SIZE = 20;
+const GRAVITY = 0.5;
+const JUMP_FORCE = 12;
+const MOVE_SPEED = 7;
+
+const PLATFORM_HEIGHT = 20;
+const MIN_PLATFORM_WIDTH = 100;
+const MAX_PLATFORM_WIDTH = 300;
+const MIN_PARTITION_SIZE = MAX_PLATFORM_WIDTH + 20;
+const PARTITION_RATIO = 0.3;
+const MIN_PLATFORM_SPACING = 100;
+const DEADLY_BORDER_HEIGHT = 7;
+
+const ENEMY_SPEED = 2;
+
+const FALLING_PLATFORM_DELAY = 800;
+const FALLING_PLATFORM_GRAVITY = 0.5;
+
+const FPS = 60;
+const FRAME_TIME = 1000 / FPS;
+
+const PARTICLE_COUNT = 20;
+const PARTICLE_SPEED = 4;
+const PARTICLE_SIZE = 4;
+const PARTICLE_LIFETIME = 25;
+
+const DEATH_PARTICLE_COUNT = 60;
+const DEATH_PARTICLE_SPEED = 10;
+const DEATH_PARTICLE_SIZE = 4;
+const DEATH_PARTICLE_LIFETIME = 50;
+const DEATH_ANIMATION_DURATION = 55;
+
+const PLATFORM_PARTICLE_COUNT = 30;
+const PLATFORM_PARTICLE_SPEED = 8;
+const PLATFORM_PARTICLE_SIZE = 4;
+const PLATFORM_PARTICLE_LIFETIME = 40;
+
+const HARD_MODE_TIME_LIMIT = 7; // seconds
+const SUPER_HARD_MODE_TIME_LIMIT = 5;
+
+let gameState = GAME_STATE.PLAYING;
+let level = 1;
+let platforms = [];
+let enemies = [];
+let particles = [];
+let deathParticles = [];
+let deathAnimationTimer = 0;
+let frameCount = 0;
+let lastFpsUpdate = 0;
+let currentFps = 0;
+let lastFrameTime = 0;
+let accumulator = 0;
+
+let player = {
+    x: PLAYER_SIZE,
+    y: window.innerHeight - PLAYER_SIZE * 2,
+    velocityX: 0,
+    velocityY: 0,
+    isJumping: false,
+    jumpsLeft: 2,
+    gravityMultiplier: 1,
+    isDead: false
+};
+
+let exit = {
+    x: window.innerWidth - PLAYER_SIZE * 2,
+    y: PLAYER_SIZE * 2,
+    size: PLAYER_SIZE
+};
+
+const keys = {};
+window.addEventListener('keydown', e => keys[e.key] = true);
+window.addEventListener('keyup', e => keys[e.key] = false);
+
+const COLORS = {
+    BACKGROUND: '#E0E0E0',
+    PLATFORM: {
+        NORMAL: '#1A1A1A',
+        DEADLY: 'tomato',
+        FALLING: 'rgba(26, 26, 26, 0.5)'
+    },
+    PLAYER: {
+        NORMAL: '#1A1A1A',
+        INVERTED: '#4A90E2'
+    },
+    ENEMY: 'tomato',
+    EXIT: 'teal',
+    TEXT: '#1A1A1A',
+    GAME_OVER: {
+        OVERLAY: 'rgba(0, 0, 0, 0.7)',
+        TEXT: 'rgba(255, 255, 255, 0.75)'
+    },
+    DEADLY_BORDER: 'tomato'
+};
+
+function randomRange(min, max) {
+    return min + Math.random() * (max - min);
+}
+
+function createPartition(x, y, width, height) {
+    return {
+        x,
+        y,
+        width,
+        height,
+        platform: null,
+        left: null,
+        right: null
+    };
+}
+
+function splitPartition(partition, vertical) {
+    const splitPoint = randomRange(
+        vertical ? partition.width * PARTITION_RATIO : partition.height * PARTITION_RATIO,
+        vertical ? partition.width * (1 - PARTITION_RATIO) : partition.height * (1 - PARTITION_RATIO)
+    );
+
+    return {
+        left: vertical 
+            ? createPartition(partition.x, partition.y, splitPoint, partition.height)
+            : createPartition(partition.x, partition.y, partition.width, splitPoint),
+        right: vertical
+            ? createPartition(partition.x + splitPoint, partition.y, partition.width - splitPoint, partition.height)
+            : createPartition(partition.x, partition.y + splitPoint, partition.width, partition.height - splitPoint)
+    };
+}
+
+function platformsOverlap(platform1, platform2) {
+    const buffer = 5;
+    
+    return !(
+        platform1.x + platform1.width + buffer < platform2.x ||
+        platform1.x > platform2.x + platform2.width + buffer ||
+        platform1.y + platform1.height + buffer < platform2.y ||
+        platform1.y > platform2.y + platform2.height + buffer
+    );
+}
+
+function createParticle(x, y, velocityY) {
+    return {
+        // Randomly places particles around the player, within the player's width
+        x: x + PLAYER_SIZE / 2 + (Math.random() - 0.5) * PLAYER_SIZE,
+        
+        // Put particles to the top or bottom of the player depending on the direction
+        // the player is moving
+        y: y + (velocityY > 0 ? 0 : PLAYER_SIZE),
+        
+        // Zoot out to the left and right of the player
+        velocityX: (Math.random() - 0.5) * PARTICLE_SPEED * 2,
+        
+        // Zoot up or down, matching the player's direction
+        velocityY: (Math.random() * PARTICLE_SPEED) * Math.sign(velocityY),
+        
+        // Add some variety to the particle sizes
+        size: PARTICLE_SIZE + Math.random() * 2,
+        
+        life: PARTICLE_LIFETIME,        
+        initialOpacity: 0.3 + Math.random() * 0.7
+    };
+}
+
+function createDeathParticles(x, y) {
+    deathParticles = [];
+    for (let i = 0; i < DEATH_PARTICLE_COUNT; i++) {
+        const angle = (Math.PI * 2 * i) / DEATH_PARTICLE_COUNT;
+        deathParticles.push({
+            x: x + PLAYER_SIZE / 2,
+            y: y + PLAYER_SIZE / 2,
+            velocityX: Math.cos(angle) * DEATH_PARTICLE_SPEED * (0.5 + Math.random()),
+            velocityY: Math.sin(angle) * DEATH_PARTICLE_SPEED * (0.5 + Math.random()),
+            size: DEATH_PARTICLE_SIZE + Math.random() * 2,
+            life: DEATH_PARTICLE_LIFETIME,
+            initialOpacity: 0.6 + Math.random() * 0.4
+        });
+    }
+    deathAnimationTimer = DEATH_ANIMATION_DURATION;
+}
+
+function updateParticles() {
+    particles = particles.filter(particle => {
+        particle.x += particle.velocityX;
+        particle.y += particle.velocityY;
+        particle.life--;
+        return particle.life > 0;
+    });
+}
+
+function updateDeathParticles() {
+    if (deathAnimationTimer > 0) {
+        deathAnimationTimer--;
+    }
+    
+    deathParticles = deathParticles.filter(particle => {
+        particle.x += particle.velocityX;
+        particle.y += particle.velocityY;
+        particle.velocityY += GRAVITY * 0.5; // GRAVITY! Is working against me...
+        particle.life--;
+        return particle.life > 0;
+    });
+}
+
+function createEnemy(platform) {
+    const startsOnTop = Math.random() < 0.5;  // 50% chance to start on top
+    return {
+        x: platform.x,
+        y: startsOnTop ? 
+            platform.y - PLAYER_SIZE : 
+            platform.y + platform.height,
+        width: PLAYER_SIZE,
+        height: PLAYER_SIZE,
+        platform: platform,
+        direction: 1,
+        moveRight: true,
+        isOnTop: startsOnTop  // Track where things started
+    };
+}
+
+function updateEnemies() {
+    enemies.forEach(enemy => {
+        enemy.x += ENEMY_SPEED * (enemy.moveRight ? 1 : -1);
+
+        if (enemy.moveRight && enemy.x + enemy.width > enemy.platform.x + enemy.platform.width) {
+            enemy.moveRight = false;
+        } else if (!enemy.moveRight && enemy.x < enemy.platform.x) {
+            enemy.moveRight = true;
+        }
+
+        enemy.y = (player.gravityMultiplier > 0) === enemy.isOnTop ? 
+            enemy.platform.y - enemy.height :
+            enemy.platform.y + enemy.platform.height;
+    });
+}
+
+function generatePlatformsForPartition(partition) {
+    if (partition.width < MIN_PLATFORM_WIDTH || partition.height < PLATFORM_HEIGHT * 2) {
+        return [];
+    }
+
+    const platformCount = partition.width > MAX_PLATFORM_WIDTH * 1.5 
+        ? Math.floor(randomRange(1, 3)) 
+        : 1;
+
+    const newPlatforms = [];
+    const maxAttempts = 20;
+
+    for (let i = 0; i < platformCount; i++) {
+        let validPlatform = null;
+        let attempts = 0;
+
+        // Try to create a platform, but don't try too hard so that things get stuck
+        while (!validPlatform && attempts < maxAttempts) {
+            // Generate a random width for the platform, that is between a min and a max
+            const platformWidth = randomRange(
+                MIN_PLATFORM_WIDTH, 
+                Math.min(MAX_PLATFORM_WIDTH, partition.width * 0.8) 
+            );
+            
+            // Generate the minimum x position, which is either the partition's start
+            // or a position based on the platform's number being placed (i/platformCount)
+            // This spreads the platforms out more evenly across the partition
+            const minX = Math.max(
+                partition.x, 
+                partition.x + (partition.width * (i / platformCount))
+            );
+            
+            // Along the same lines, calculate a max x position that accounts for platform width
+            const maxX = Math.min(
+                partition.x + partition.width - platformWidth,
+                partition.x + (partition.width * ((i + 1) / platformCount))
+            );
+            
+            // Try to place a platform if there is a valid range
+            if (maxX > minX) {
+                // Create a candidate platform with a random position and properties
+                const candidatePlatform = {
+                    x: randomRange(minX, maxX),
+                    y: randomRange(
+                        partition.y + PLATFORM_HEIGHT,
+                        partition.y + partition.height - PLATFORM_HEIGHT * 2
+                    ),
+                    width: platformWidth,
+                    height: PLATFORM_HEIGHT,
+                    // After level 1, there is a chance that a platform will be deadly, or falling!
+                    type: (() => {
+                        if (level > 1 && Math.random() < 0.2) return PLATFORM_TYPE.DEADLY;
+                        if (level > 1 && Math.random() < 0.3) {
+                            return PLATFORM_TYPE.FALLING;
+                        }
+                        return PLATFORM_TYPE.NORMAL;
+                    })(),
+                    fallTimer: null,
+                    velocityY: 0
+                };
+
+                // Check if the platform overlaps with any existing platforms
+                let overlapping = false;
+                for (const existingPlatform of [...platforms, ...newPlatforms]) {
+                    if (platformsOverlap(candidatePlatform, existingPlatform)) {
+                        overlapping = true;
+                        break;
+                    }
+                }
+
+                // If there isn't an overlap the platform is valid!
+                // Place it!
+                if (!overlapping) {
+                    validPlatform = candidatePlatform;
+                }
+            }
+
+            attempts++;
+        }
+
+        if (validPlatform) {
+            newPlatforms.push(validPlatform);
+        }
+    }
+
+    newPlatforms.forEach(platform => {
+        if (platform.type === PLATFORM_TYPE.NORMAL && Math.random() < 0.2) {  // 20% chance
+            enemies.push(createEnemy(platform));
+        }
+    });
+
+    return newPlatforms;
+}
+
+function generateLevel() {
+    platforms = [];
+    enemies = [];
+    
+    platforms.push({
+        x: 0,
+        y: window.innerHeight - PLATFORM_HEIGHT,
+        width: MIN_PLATFORM_WIDTH,
+        height: PLATFORM_HEIGHT,
+        type: PLATFORM_TYPE.NORMAL
+    });
+
+    platforms.push({
+        x: window.innerWidth - MIN_PLATFORM_WIDTH,
+        y: PLAYER_SIZE * 3,
+        width: MIN_PLATFORM_WIDTH,
+        height: PLATFORM_HEIGHT,
+        type: PLATFORM_TYPE.NORMAL
+    });
+
+    const horizontalSections = 3;
+    const verticalSections = 2;
+    const sectionWidth = window.innerWidth / horizontalSections;
+    const sectionHeight = (window.innerHeight - PLATFORM_HEIGHT * 4) / verticalSections;
+
+    function subdivide(node, depth) {
+        if (depth === 0) {
+            return generatePlatformsForPartition(node);
+        }
+        
+        const vertical = Math.random() > 0.4;
+        if ((vertical && node.width > MIN_PARTITION_SIZE * 1.5) ||
+            (!vertical && node.height > MIN_PARTITION_SIZE * 1.5)) {
+            
+            const { left, right } = splitPartition(node, vertical);
+            return [
+                ...subdivide(left, depth - 1),
+                ...subdivide(right, depth - 1)
+            ];
+        }
+        
+        return generatePlatformsForPartition(node);
+    }
+
+    for (let i = 0; i < horizontalSections; i++) {
+        for (let j = 0; j < verticalSections; j++) {
+            const root = createPartition(
+                i * sectionWidth,
+                PLATFORM_HEIGHT * 2 + (j * sectionHeight),
+                sectionWidth,
+                sectionHeight
+            );
+
+            const newPlatforms = subdivide(root, Math.min(3 + Math.floor(level / 2), 5));
+            platforms.push(...newPlatforms);
+        }
+    }
+
+    levelStartTime = Date.now();
+}
+
+function resetPlayer() {
+    player.x = PLAYER_SIZE;
+    player.y = window.innerHeight - PLATFORM_HEIGHT - PLAYER_SIZE;
+    player.velocityX = 0;
+    player.velocityY = 0;
+    player.isJumping = false;
+    player.jumpsLeft = 2;
+    player.gravityMultiplier = 1;
+    player.isDead = false;
+    gameState = GAME_STATE.PLAYING;
+    level = 1;
+    generateLevel();
+    enemies = [];
+    levelStartTime = Date.now();
+}
+
+function killPlayer() {
+    if (!player.isDead) {
+        createDeathParticles(player.x, player.y);
+        player.isDead = true;
+        gameState = GAME_STATE.GAME_OVER;
+    }
+}
+
+// Try to compensate for varying viewport widths
+function calculateTimeLimit(isSuper) {
+    const baseLimit = isSuper ? SUPER_HARD_MODE_TIME_LIMIT : HARD_MODE_TIME_LIMIT;
+    
+    if (canvas.width <= 2000) return baseLimit;
+    
+    const extraWidth = canvas.width - 2000;
+    const extraSeconds = Math.floor(extraWidth / 1000) * (isSuper ? 0.5 : 1);
+    
+    return baseLimit + extraSeconds;
+}
+
+function updatePlayer() {
+    if (gameState === GAME_STATE.GAME_OVER) {
+        if (deathAnimationTimer <= 0 && keys['Enter']) {
+            resetPlayer();
+        }
+        return;
+    }
+
+    const timeLimit = superHardMode ? 
+        calculateTimeLimit(true) : 
+        calculateTimeLimit(false);
+        
+    if ((hardMode || superHardMode) && Date.now() - levelStartTime > timeLimit * 1000) {
+        killPlayer();
+        return;
+    }
+
+    if (keys['ArrowLeft']) player.velocityX = -MOVE_SPEED;
+    else if (keys['ArrowRight']) player.velocityX = MOVE_SPEED;
+    else player.velocityX = 0;
+    
+    if ((keys['g'] || keys[' ']) && !player.lastGravityKey) {
+        player.gravityMultiplier *= -1;
+        player.velocityY = 0;
+    }
+    player.lastGravityKey = keys['g'] || keys[' '];
+    
+    player.velocityY += GRAVITY * player.gravityMultiplier;
+    
+    if (keys['ArrowUp'] && keys['ArrowUp'] !== player.lastJumpKey && player.jumpsLeft > 0) {
+        player.velocityY = -JUMP_FORCE * player.gravityMultiplier;
+        player.jumpsLeft--;
+        player.isJumping = true;
+        
+        // Add particles for every jump
+        for (let i = 0; i < PARTICLE_COUNT; i++) {
+            particles.push(createParticle(player.x, player.y, player.velocityY));
+        }
+    }
+    player.lastJumpKey = keys['ArrowUp'];
+    
+    player.x += player.velocityX;
+    player.y += player.velocityY;
+    
+    player.isJumping = true;
+    for (let platform of platforms) {
+        if (player.x + PLAYER_SIZE > platform.x &&
+            player.x < platform.x + platform.width) {
+            
+            let collision = false;
+            
+            if (player.gravityMultiplier > 0) {
+                if (player.y + PLAYER_SIZE > platform.y &&
+                    player.y + PLAYER_SIZE < platform.y + platform.height + player.velocityY) {
+                    collision = true;
+                    player.y = platform.y - PLAYER_SIZE;
+                }
+            } else {
+                if (player.y < platform.y + platform.height &&
+                    player.y > platform.y + player.velocityY) {
+                    collision = true;
+                    player.y = platform.y + platform.height;
+                }
+            }
+
+            if (collision) {
+                if (platform.type === PLATFORM_TYPE.DEADLY) {
+                    killPlayer();
+                } else {
+                    if (platform.type === PLATFORM_TYPE.FALLING && !platform.fallTimer) {
+                        platform.fallTimer = setTimeout(() => {
+                            platform.isFalling = true;
+                        }, FALLING_PLATFORM_DELAY);
+                    }
+                    player.velocityY = 0;
+                    player.isJumping = false;
+                    player.jumpsLeft = 2;
+                }
+            }
+        }
+    }
+    
+    if (player.y <= DEADLY_BORDER_HEIGHT) {
+        if (player.x < canvas.width - exit.size - 300) {
+            killPlayer();
+        }
+    } else if (player.y + PLAYER_SIZE >= canvas.height - DEADLY_BORDER_HEIGHT) {
+        killPlayer();
+    }
+    
+    if (player.x < 0) player.x = 0;
+    if (player.x + PLAYER_SIZE > canvas.width) player.x = canvas.width - PLAYER_SIZE;
+    if (player.y < 0) {
+        player.y = 0;
+        player.velocityY = 0;
+    }
+    if (player.y + PLAYER_SIZE > canvas.height) {
+        player.y = canvas.height - PLAYER_SIZE;
+        player.velocityY = 0;
+    }
+    
+    if (player.x + PLAYER_SIZE > exit.x &&
+        player.x < exit.x + exit.size &&
+        player.y + PLAYER_SIZE > exit.y &&
+        player.y < exit.y + exit.size) {
+        level++;
+        generateLevel();
+        player.x = PLAYER_SIZE;
+        player.y = window.innerHeight - PLATFORM_HEIGHT - PLAYER_SIZE;
+        player.velocityY = 0;
+        player.velocityX = 0;
+        player.jumpsLeft = 2;
+        player.isJumping = false;
+    }
+
+    enemies.forEach(enemy => {
+        if (player.x < enemy.x + enemy.width &&
+            player.x + PLAYER_SIZE > enemy.x &&
+            player.y < enemy.y + enemy.height &&
+            player.y + PLAYER_SIZE > enemy.y) {
+            killPlayer();
+        }
+    });
+}
+
+function resizeCanvas() {
+    canvas.width = window.innerWidth;
+    canvas.height = window.innerHeight;
+}
+
+function draw(currentTime) {
+    if ((hardMode || superHardMode) && gameState === GAME_STATE.PLAYING) {
+        const timeLimit = superHardMode ? 
+            calculateTimeLimit(true) : 
+            calculateTimeLimit(false);
+            
+        const timeElapsed = Date.now() - levelStartTime;
+        const timeRemaining = Math.max(0, timeLimit * 1000 - timeElapsed);
+        const progressRatio = timeRemaining / (timeLimit * 1000);
+        
+        ctx.fillStyle = COLORS.BACKGROUND;
+        ctx.fillRect(0, 0, canvas.width, canvas.height);
+        
+        ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
+        const progressWidth = canvas.width * progressRatio;
+        ctx.fillRect(progressWidth, 0, canvas.width - progressWidth, canvas.height);
+    } else {
+        ctx.fillStyle = COLORS.BACKGROUND;
+        ctx.fillRect(0, 0, canvas.width, canvas.height);
+    }
+    
+    ctx.fillStyle = COLORS.DEADLY_BORDER;
+    ctx.fillRect(0, 0, canvas.width - exit.size - 300, DEADLY_BORDER_HEIGHT);
+    
+    ctx.fillRect(0, canvas.height - DEADLY_BORDER_HEIGHT, platforms[0].x, DEADLY_BORDER_HEIGHT);
+    ctx.fillRect(
+        platforms[0].x + platforms[0].width, 
+        canvas.height - DEADLY_BORDER_HEIGHT, 
+        canvas.width - (platforms[0].x + platforms[0].width), 
+        DEADLY_BORDER_HEIGHT
+    );
+    
+    for (let platform of platforms) {
+        ctx.fillStyle = platform.type === PLATFORM_TYPE.DEADLY ? COLORS.PLATFORM.DEADLY :
+                       platform.type === PLATFORM_TYPE.FALLING ? COLORS.PLATFORM.FALLING :
+                       COLORS.PLATFORM.NORMAL;
+        ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
+    }
+    
+    ctx.fillStyle = COLORS.EXIT;
+    ctx.fillRect(exit.x, exit.y, exit.size, exit.size);
+    
+    if (!player.isDead) {
+        ctx.fillStyle = player.gravityMultiplier > 0 ? COLORS.PLAYER.NORMAL : COLORS.PLAYER.INVERTED;
+        ctx.fillRect(player.x, player.y, PLAYER_SIZE, PLAYER_SIZE);
+    }
+    
+    ctx.fillStyle = COLORS.TEXT;
+    ctx.font = '20px Arial';
+    ctx.textAlign = 'left';
+    ctx.fillText(`Level: ${level}`, 10, 30);
+
+    // Particles can have different opacities
+    particles.forEach(particle => {
+        const alpha = (particle.life / PARTICLE_LIFETIME) * particle.initialOpacity;
+        ctx.fillStyle = `rgba(26, 26, 26, ${alpha})`;
+        ctx.fillRect(particle.x, particle.y, particle.size, particle.size);
+    });
+
+    enemies.forEach(enemy => {
+        ctx.fillStyle = COLORS.ENEMY;
+        ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
+    });
+
+    // Death particles x_x
+    deathParticles.forEach(particle => {
+        const alpha = (particle.life / DEATH_PARTICLE_LIFETIME) * particle.initialOpacity;
+        ctx.fillStyle = `rgba(26, 26, 26, ${alpha})`;
+        ctx.fillRect(particle.x, particle.y, particle.size, particle.size);
+    });
+
+    if (gameState === GAME_STATE.GAME_OVER && deathAnimationTimer <= 0) {
+        ctx.fillStyle = COLORS.GAME_OVER.OVERLAY;
+        ctx.fillRect(0, 0, canvas.width, canvas.height);
+        
+        ctx.fillStyle = COLORS.GAME_OVER.TEXT;
+        ctx.font = '100px Arial';
+        ctx.textAlign = 'center';
+        ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2);
+        
+        ctx.font = '24px Arial';
+        ctx.fillText('Press ENTER to restart', canvas.width / 2, canvas.height / 2 + 40);
+        
+        ctx.textAlign = 'left';
+    }
+
+    // Show some help text on the first level so that folks know what to do
+    if (level === 1 && gameState === GAME_STATE.PLAYING) {
+        ctx.fillStyle = COLORS.TEXT;
+        ctx.font = '24px Arial';
+        ctx.textAlign = 'center';
+        ctx.fillText('Arrow keys move the player.', canvas.width / 2, canvas.height / 2 - 20);
+        ctx.fillText('Space bar reverses gravity.', canvas.width / 2, canvas.height / 2 + 20);
+        ctx.textAlign = 'left';
+    }
+}
+
+function gameLoop(currentTime) {
+    if (lastFrameTime === 0) {
+        lastFrameTime = currentTime;
+        lastFpsUpdate = currentTime;
+    }
+
+    const deltaTime = currentTime - lastFrameTime;
+    lastFrameTime = currentTime;
+    
+    accumulator += deltaTime;
+    
+    while (accumulator >= FRAME_TIME) {
+        updatePlayer();
+        updateEnemies();
+        updateParticles();
+        updateDeathParticles();
+        updatePlatforms();
+        accumulator -= FRAME_TIME;
+    }
+    
+    draw(currentTime);
+    requestAnimationFrame(gameLoop);
+}
+
+function createPlatformParticles(platform) {
+    for (let i = 0; i < PLATFORM_PARTICLE_COUNT; i++) {
+        const angle = (Math.PI * 2 * i) / PLATFORM_PARTICLE_COUNT;
+        particles.push({
+            x: platform.x + platform.width / 2,
+            y: platform.y + platform.height / 2,
+            velocityX: Math.cos(angle) * PLATFORM_PARTICLE_SPEED * (0.5 + Math.random()),
+            velocityY: Math.sin(angle) * PLATFORM_PARTICLE_SPEED * (0.5 + Math.random()),
+            size: PLATFORM_PARTICLE_SIZE + Math.random() * 2,
+            life: PLATFORM_PARTICLE_LIFETIME,
+            initialOpacity: 0.6 + Math.random() * 0.4
+        });
+    }
+}
+
+function updatePlatforms() {
+    platforms.forEach(platform => {
+        if (platform.type === PLATFORM_TYPE.FALLING && platform.isFalling) {
+            platform.velocityY += FALLING_PLATFORM_GRAVITY * player.gravityMultiplier;
+            platform.y += platform.velocityY;
+            
+            // Create particles when platform goes off screen
+            if ((player.gravityMultiplier > 0 && platform.y > canvas.height + 50) || 
+                (player.gravityMultiplier < 0 && platform.y < -50)) {
+                createPlatformParticles(platform);
+            }
+        }
+    });
+    
+    // Remove platforms that have fallen off screen in either direction
+    platforms = platforms.filter(platform => 
+        platform.type !== PLATFORM_TYPE.FALLING || 
+        (player.gravityMultiplier > 0 ? platform.y < canvas.height + 100 : platform.y > -100)
+    );
+}
+
+let hardMode = false;
+let superHardMode = false;
+let levelStartTime = 0;
+
+const hardModeButton = document.createElement('button');
+hardModeButton.textContent = 'Hard Mode: OFF';
+hardModeButton.style.position = 'fixed';
+hardModeButton.style.left = '10px';
+hardModeButton.style.top = '40px';
+hardModeButton.style.padding = '5px 10px';
+hardModeButton.style.backgroundColor = COLORS.PLATFORM.NORMAL;
+hardModeButton.style.color = 'white';
+hardModeButton.style.border = 'none';
+hardModeButton.style.cursor = 'pointer';
+document.body.appendChild(hardModeButton);
+
+const superHardModeButton = document.createElement('button');
+superHardModeButton.textContent = 'Super Hard Mode: OFF';
+superHardModeButton.style.position = 'fixed';
+superHardModeButton.style.left = '10px';
+superHardModeButton.style.top = '70px';
+superHardModeButton.style.padding = '5px 10px';
+superHardModeButton.style.backgroundColor = COLORS.PLATFORM.NORMAL;
+superHardModeButton.style.color = 'white';
+superHardModeButton.style.border = 'none';
+superHardModeButton.style.cursor = 'pointer';
+document.body.appendChild(superHardModeButton);
+
+hardModeButton.addEventListener('click', () => {
+    hardMode = !hardMode;
+    superHardMode = false;
+    hardModeButton.textContent = `Hard Mode: ${hardMode ? 'ON' : 'OFF'}`;
+    superHardModeButton.textContent = 'Super Hard Mode: OFF';
+    resetPlayer();
+    hardModeButton.blur();
+});
+
+superHardModeButton.addEventListener('click', () => {
+    superHardMode = !superHardMode;
+    hardMode = false; 
+    superHardModeButton.textContent = `Super Hard Mode: ${superHardMode ? 'ON' : 'OFF'}`;
+    hardModeButton.textContent = 'Hard Mode: OFF'; 
+    resetPlayer();
+    superHardModeButton.blur();
+});
+
+document.body.style.margin = '0';
+document.body.style.overflow = 'hidden';
+resizeCanvas();
+window.addEventListener('resize', resizeCanvas);
+generateLevel();
+requestAnimationFrame(gameLoop);
diff --git a/html/mountain/index.html b/html/mountain/index.html
new file mode 100644
index 0000000..cac722a
--- /dev/null
+++ b/html/mountain/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Mountain</title>
+</head>
+<body>
+    <script src="game.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/html/plains/game.js b/html/plains/game.js
new file mode 100644
index 0000000..6054455
--- /dev/null
+++ b/html/plains/game.js
@@ -0,0 +1,486 @@
+const CONFIG = {
+    player: {
+        size: 30,
+        speed: 5,
+        sprintMultiplier: 2,
+        color: '#111'
+    },
+    sword: {
+        length: 60,
+        swingSpeed: 0.6,
+        colors: {
+            primary: '#4169E1',
+            secondary: '#1E90FF',
+            tertiary: '#0000CD',
+            glow: 'rgba(30, 144, 255, 0.3)'
+        }
+    },
+    defense: {
+        numLayers: 6,
+        maxRadiusMultiplier: 2,
+        baseAlpha: 0.15,
+        particleCount: 12,
+        orbitRadiusMultiplier: 0.8,
+        rotationSpeed: 1.5
+    },
+    particles: {
+        max: 100,
+        lifetime: 1.0,
+        speed: 1.5
+    },
+    footprints: {
+        lifetime: 1000,
+        spacing: 300,
+        size: 5
+    },
+    camera: {
+        deadzoneMultiplierX: 0.6,
+        deadzoneMultiplierY: 0.6,
+        ease: 0.08
+    },
+    grid: {
+        size: 100,
+        color: '#ddd'
+    },
+    fps: 60
+};
+
+let GAME_WIDTH = window.innerWidth;
+let GAME_HEIGHT = window.innerHeight;
+
+let lastFrameTime = 0;
+let animationTime = 0;
+
+const FRAME_TIME = 1000 / CONFIG.fps;
+const CAMERA_DEADZONE_X = GAME_WIDTH * CONFIG.camera.deadzoneMultiplierX;
+const CAMERA_DEADZONE_Y = GAME_HEIGHT * CONFIG.camera.deadzoneMultiplierY;
+
+const state = {
+    player: {
+        x: GAME_WIDTH / 2,
+        y: GAME_HEIGHT / 2,
+        isDefending: false,
+        direction: { x: 0, y: -1 },
+        swordAngle: 0,
+        isSwinging: false
+    },
+    particles: [],
+    footprints: [],
+    lastFootprintTime: 0,
+    camera: {
+        x: 0,
+        y: 0,
+        targetX: 0,
+        targetY: 0
+    }
+};
+
+const canvas = document.getElementById('gameCanvas');
+const ctx = canvas.getContext('2d');
+
+const keys = new Set();
+
+const handleKeyDown = (e) => {
+    keys.add(e.key);
+    
+    if (e.key === 'z' && !state.player.isSwinging && !state.player.isDefending) {
+        state.player.isSwinging = true;
+        state.player.swordAngle = Math.atan2(state.player.direction.y, state.player.direction.x) - Math.PI / 2;
+    }
+    
+    if (e.key === 'x') {
+        state.player.isDefending = true;
+    }
+};
+
+const handleKeyUp = (e) => {
+    keys.delete(e.key);
+    if (e.key === 'x') {
+        state.player.isDefending = false;
+    }
+};
+
+const updatePlayer = () => {
+    if (state.player.isDefending) {
+        return;
+    }
+    
+    let dx = 0;
+    let dy = 0;
+    
+    if (keys.has('ArrowLeft')) dx -= 1;
+    if (keys.has('ArrowRight')) dx += 1;
+    if (keys.has('ArrowUp')) dy -= 1;
+    if (keys.has('ArrowDown')) dy += 1;
+    
+    if (dx !== 0 || dy !== 0) {
+        const length = Math.sqrt(dx * dx + dy * dy);
+        state.player.direction = { x: dx / length, y: dy / length };
+        
+        const currentSpeed = keys.has('Shift') ? 
+            CONFIG.player.speed * CONFIG.player.sprintMultiplier : 
+            CONFIG.player.speed;
+        
+        state.player.x += (dx / length) * currentSpeed;
+        state.player.y += (dy / length) * currentSpeed;
+        
+        if (!state.player.isDefending) {
+            const timeSinceLastFootprint = animationTime - state.lastFootprintTime;
+            const currentSpacing = keys.has('Shift') ? 
+                CONFIG.footprints.spacing * CONFIG.player.sprintMultiplier : 
+                CONFIG.footprints.spacing;
+                
+            if (timeSinceLastFootprint > currentSpacing / currentSpeed) {
+                const offset = (Math.random() - 0.5) * 6;
+                const perpX = -state.player.direction.y * offset;
+                const perpY = state.player.direction.x * offset;
+                
+                state.footprints.push(createFootprint(
+                    state.player.x + perpX,
+                    state.player.y + perpY,
+                    Math.atan2(dy, dx)
+                ));
+                state.lastFootprintTime = animationTime;
+            }
+        }
+    }
+    
+    state.footprints = state.footprints.filter(footprint => {
+        return (animationTime - footprint.createdAt) < CONFIG.footprints.lifetime;
+    });
+    
+    if (state.player.isSwinging) {
+        state.player.swordAngle += CONFIG.sword.swingSpeed;
+        
+        if (Math.random() < 0.3) {
+            const tipX = state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length;
+            const tipY = state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length;
+            state.particles.push(createParticle(tipX, tipY, state.player.swordAngle));
+        }
+        
+        if (state.player.swordAngle > Math.atan2(state.player.direction.y, state.player.direction.x) + Math.PI / 2) {
+            state.player.isSwinging = false;
+        }
+    }
+    
+    state.particles = state.particles.filter(particle => {
+        particle.lifetime -= 1/60;
+        if (particle.lifetime <= 0) return false;
+        
+        particle.x += Math.cos(particle.angle) * particle.speed;
+        particle.y += Math.sin(particle.angle) * particle.speed;
+        return true;
+    });
+    
+    if (state.particles.length > CONFIG.particles.max) {
+        state.particles.splice(0, state.particles.length - CONFIG.particles.max);
+    }
+};
+
+const createParticle = (x, y, angle) => ({
+    x,
+    y,
+    angle,
+    lifetime: CONFIG.particles.lifetime,
+    speed: CONFIG.particles.speed * (0.5 + Math.random() * 0.5),
+    size: 2 + Math.random() * 2
+});
+
+const createFootprint = (x, y, direction) => ({
+    x,
+    y,
+    direction,
+    createdAt: animationTime,
+    size: CONFIG.footprints.size * (0.8 + Math.random() * 0.4),
+    offset: (Math.random() - 0.5) * 5
+});
+
+const renderPlayer = () => {
+    ctx.save();
+    
+    if (state.player.isSwinging) {
+        const blurSteps = 12;
+        const blurSpread = 0.2;
+        
+        for (let i = 0; i < blurSteps; i++) {
+            const alpha = 0.35 - (i * 0.02);
+            const angleOffset = -blurSpread * i;
+            
+            ctx.strokeStyle = `rgba(30, 144, 255, ${alpha})`;
+            ctx.lineWidth = 4 + (blurSteps - i);
+            ctx.beginPath();
+            ctx.moveTo(state.player.x, state.player.y);
+            ctx.lineTo(
+                state.player.x + Math.cos(state.player.swordAngle + angleOffset) * CONFIG.sword.length,
+                state.player.y + Math.sin(state.player.swordAngle + angleOffset) * CONFIG.sword.length
+            );
+            ctx.stroke();
+        }
+        
+        state.particles.forEach(particle => {
+            const alpha = (particle.lifetime / CONFIG.particles.lifetime) * 0.8;
+            ctx.fillStyle = `rgba(135, 206, 250, ${alpha})`;
+            ctx.beginPath();
+            ctx.arc(particle.x, particle.y, particle.size * 1.5, 0, Math.PI * 2);
+            ctx.fill();
+            
+            ctx.fillStyle = `rgba(30, 144, 255, ${alpha * 0.3})`;
+            ctx.beginPath();
+            ctx.arc(particle.x, particle.y, particle.size * 2.5, 0, Math.PI * 2);
+            ctx.fill();
+        });
+        
+        const gradient = ctx.createLinearGradient(
+            state.player.x,
+            state.player.y,
+            state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length,
+            state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length
+        );
+        gradient.addColorStop(0, CONFIG.sword.colors.primary);
+        gradient.addColorStop(0.6, CONFIG.sword.colors.secondary);
+        gradient.addColorStop(1, CONFIG.sword.colors.tertiary);
+        
+        ctx.strokeStyle = CONFIG.sword.colors.glow;
+        ctx.lineWidth = 10;
+        ctx.beginPath();
+        ctx.moveTo(state.player.x, state.player.y);
+        ctx.lineTo(
+            state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length,
+            state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length
+        );
+        ctx.stroke();
+        
+        ctx.strokeStyle = gradient;
+        ctx.lineWidth = 6;
+        ctx.stroke();
+    }
+    
+    if (state.player.isDefending) {
+        const numLayers = CONFIG.defense.numLayers;
+        const maxRadius = CONFIG.player.size * CONFIG.defense.maxRadiusMultiplier;
+        const baseAlpha = CONFIG.defense.baseAlpha;
+        
+        for (let i = numLayers - 1; i >= 0; i--) {
+            const radius = CONFIG.player.size / 2 + (maxRadius - CONFIG.player.size / 2) * (i / numLayers);
+            const alpha = baseAlpha * (1 - i / numLayers);
+            
+            const pulseOffset = Math.sin(Date.now() / 500) * 3;
+            
+            const glowGradient = ctx.createRadialGradient(
+                state.player.x, state.player.y, radius - 5,
+                state.player.x, state.player.y, radius + pulseOffset
+            );
+            glowGradient.addColorStop(0, `rgba(30, 144, 255, ${alpha})`);
+            glowGradient.addColorStop(1, 'rgba(30, 144, 255, 0)');
+            
+            ctx.beginPath();
+            ctx.arc(state.player.x, state.player.y, radius + pulseOffset, 0, Math.PI * 2);
+            ctx.fillStyle = glowGradient;
+            ctx.fill();
+        }
+        
+        const mainAuraGradient = ctx.createRadialGradient(
+            state.player.x, state.player.y, CONFIG.player.size / 2 - 2,
+            state.player.x, state.player.y, CONFIG.player.size / 2 + 8
+        );
+        mainAuraGradient.addColorStop(0, 'rgba(30, 144, 255, 0.3)');
+        mainAuraGradient.addColorStop(0.5, 'rgba(30, 144, 255, 0.2)');
+        mainAuraGradient.addColorStop(1, 'rgba(30, 144, 255, 0)');
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2 + 8, 0, Math.PI * 2);
+        ctx.fillStyle = mainAuraGradient;
+        ctx.fill();
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2, 0, Math.PI * 2);
+        ctx.fillStyle = '#111';
+        ctx.fill();
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2, 0, Math.PI * 2);
+        ctx.strokeStyle = CONFIG.sword.colors.secondary;
+        ctx.lineWidth = 3;
+        ctx.stroke();
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2 - 3, 0, Math.PI * 2);
+        ctx.strokeStyle = 'rgba(30, 144, 255, 0.3)';
+        ctx.lineWidth = 2;
+        ctx.stroke();
+        
+        const numParticles = CONFIG.defense.particleCount;
+        const baseOrbitRadius = CONFIG.player.size * CONFIG.defense.orbitRadiusMultiplier;
+        
+        const rotationSpeed = CONFIG.defense.rotationSpeed;
+        
+        for (let i = 0; i < numParticles; i++) {
+            const radialOffset = Math.sin(animationTime * 0.002 + i * 0.5) * 4;
+            const orbitRadius = baseOrbitRadius + radialOffset;
+            
+            const angle = (i / numParticles) * Math.PI * 2 + animationTime * rotationSpeed * 0.001;
+            const x = state.player.x + Math.cos(angle) * orbitRadius;
+            const y = state.player.y + Math.sin(angle) * orbitRadius;
+            
+            const size = 2 + Math.sin(animationTime * 0.003 + i * 0.8) * 1.5;
+            const baseAlpha = 0.6 + Math.sin(animationTime * 0.002 + i) * 0.2;
+            
+            ctx.beginPath();
+            ctx.arc(x, y, size * 2, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(30, 144, 255, ${baseAlpha * 0.3})`;
+            ctx.fill();
+            
+            ctx.beginPath();
+            ctx.arc(x, y, size, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(135, 206, 250, ${baseAlpha})`;
+            ctx.fill();
+            
+            if (i > 0) {
+                const prevAngle = ((i - 1) / numParticles) * Math.PI * 2 + animationTime * rotationSpeed * 0.001;
+                const prevX = state.player.x + Math.cos(prevAngle) * orbitRadius;
+                const prevY = state.player.y + Math.sin(prevAngle) * orbitRadius;
+                
+                ctx.beginPath();
+                ctx.moveTo(prevX, prevY);
+                ctx.lineTo(x, y);
+                ctx.strokeStyle = `rgba(30, 144, 255, ${baseAlpha * 0.2})`;
+                ctx.lineWidth = 1;
+                ctx.stroke();
+            }
+        }
+    } else {
+        ctx.fillStyle = CONFIG.player.color;
+        ctx.fillRect(
+            state.player.x - CONFIG.player.size / 2,
+            state.player.y - CONFIG.player.size / 2,
+            CONFIG.player.size,
+            CONFIG.player.size
+        );
+    }
+    
+    ctx.restore();
+};
+
+const lerp = (start, end, t) => {
+    return start * (1 - t) + end * t;
+};
+
+const render = () => {
+    ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
+    
+    const screenCenterX = -state.camera.x + GAME_WIDTH / 2;
+    const screenCenterY = -state.camera.y + GAME_HEIGHT / 2;
+    
+    const distX = state.player.x - screenCenterX;
+    const distY = state.player.y - screenCenterY;
+    
+    if (Math.abs(distX) > CAMERA_DEADZONE_X / 2) {
+        const bufferX = (GAME_WIDTH - CAMERA_DEADZONE_X) / 2;
+        const targetOffsetX = distX > 0 ? GAME_WIDTH - bufferX : bufferX;
+        state.camera.targetX = -(state.player.x - targetOffsetX);
+    }
+    if (Math.abs(distY) > CAMERA_DEADZONE_Y / 2) {
+        const bufferY = (GAME_HEIGHT - CAMERA_DEADZONE_Y) / 2;
+        const targetOffsetY = distY > 0 ? GAME_HEIGHT - bufferY : bufferY;
+        state.camera.targetY = -(state.player.y - targetOffsetY);
+    }
+    
+    state.camera.x = lerp(state.camera.x, state.camera.targetX, CONFIG.camera.ease);
+    state.camera.y = lerp(state.camera.y, state.camera.targetY, CONFIG.camera.ease);
+    
+    ctx.save();
+    ctx.translate(state.camera.x, state.camera.y);
+    
+    const gridSize = CONFIG.grid.size;
+    ctx.strokeStyle = CONFIG.grid.color;
+    ctx.lineWidth = 1;
+    
+    const startX = Math.floor((-state.camera.x) / gridSize) * gridSize;
+    const startY = Math.floor((-state.camera.y) / gridSize) * gridSize;
+    const endX = startX + GAME_WIDTH + gridSize;
+    const endY = startY + GAME_HEIGHT + gridSize;
+    
+    for (let x = startX; x < endX; x += gridSize) {
+        ctx.beginPath();
+        ctx.moveTo(x, startY);
+        ctx.lineTo(x, endY);
+        ctx.stroke();
+    }
+    
+    for (let y = startY; y < endY; y += gridSize) {
+        ctx.beginPath();
+        ctx.moveTo(startX, y);
+        ctx.lineTo(endX, y);
+        ctx.stroke();
+    }
+    
+    state.footprints.forEach(footprint => {
+        const age = (animationTime - footprint.createdAt) / CONFIG.footprints.lifetime;
+        if (age >= 1) return;
+        
+        const alpha = Math.max(0, 1 - age * age);
+        
+        ctx.save();
+        ctx.translate(footprint.x + footprint.offset, footprint.y + footprint.offset);
+        
+        const radius = Math.max(0.1, footprint.size * (1 - age * 0.5));
+        
+        if (radius > 0) {
+            ctx.beginPath();
+            ctx.arc(0, 0, radius * 2, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(17, 17, 17, ${alpha * 0.1})`;
+            ctx.fill();
+            
+            ctx.beginPath();
+            ctx.arc(0, 0, radius, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(17, 17, 17, ${alpha * 0.3})`;
+            ctx.fill();
+        }
+        
+        ctx.restore();
+    });
+    
+    renderPlayer();
+    
+    ctx.restore();
+};
+
+const gameLoop = (currentTime) => {
+    if (!lastFrameTime) {
+        lastFrameTime = currentTime;
+        animationTime = 0;
+    }
+
+    const deltaTime = currentTime - lastFrameTime;
+    
+    if (deltaTime >= FRAME_TIME) {
+        animationTime += FRAME_TIME;
+        
+        updatePlayer();
+        render();
+        
+        lastFrameTime = currentTime;
+    }
+    
+    requestAnimationFrame(gameLoop);
+};
+
+window.addEventListener('keydown', handleKeyDown);
+window.addEventListener('keyup', handleKeyUp);
+
+const resizeCanvas = () => {
+    GAME_WIDTH = window.innerWidth;
+    GAME_HEIGHT = window.innerHeight;
+    canvas.width = GAME_WIDTH;
+    canvas.height = GAME_HEIGHT;
+    if (!state.player.x) {
+        state.player.x = GAME_WIDTH / 2;
+        state.player.y = GAME_HEIGHT / 2;
+    }
+};
+
+window.addEventListener('resize', resizeCanvas);
+
+resizeCanvas();
+
+requestAnimationFrame(gameLoop);
\ No newline at end of file
diff --git a/html/plains/index.html b/html/plains/index.html
new file mode 100644
index 0000000..508e7e0
--- /dev/null
+++ b/html/plains/index.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>Top-down Adventure</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        body {
+            background: #f0f0f0;
+            overflow: hidden;
+        }
+        canvas {
+            display: block;
+            width: 100vw;
+            height: 100vh;
+        }
+    </style>
+</head>
+<body>
+    <canvas id="gameCanvas"></canvas>
+    <script src="game.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/rust/bf/.vscode/launch.json b/rust/bf/.vscode/launch.json
new file mode 100644
index 0000000..33bcb32
--- /dev/null
+++ b/rust/bf/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "lldb",
+            "request": "launch",
+            "name": "Debug executable",
+            "cargo": {
+                "args": ["build"]
+            },
+            "args": ["src/hw.bf"],
+            "cwd": "${workspaceFolder}"
+        }
+    ]
+} 
\ No newline at end of file
diff --git a/rust/bf/src/main.rs b/rust/bf/src/main.rs
index 9257ae2..e511e69 100644
--- a/rust/bf/src/main.rs
+++ b/rust/bf/src/main.rs
@@ -16,12 +16,12 @@ fn interpret_brainfuck(code: &str) {
             '>' => {
                 pointer += 1;
                 if pointer >= memory.len() {
-                    panic!("Pointer out of bounds");
+                    panic!("Pointer out of bounds (positive)");
                 }
             }
             '<' => {
                 if pointer == 0 {
-                    panic!("Pointer out of bounds");
+                    panic!("Pointer out of bounds (negative)");
                 }
                 pointer -= 1;
             }
@@ -36,7 +36,7 @@ fn interpret_brainfuck(code: &str) {
             }
             ',' => {
                 let mut input = [0u8];
-                io::stdin().read_exact(&mut input).expect("Failed to read input");
+                io::stdin().read_exact(&mut input).expect("Failed to read input. Unable to read a single byte from stdin");
                 memory[pointer] = input[0];
             }
             '[' => {