// Set up canvas const canvas = document.createElement('canvas'); canvas.style.display = 'block'; canvas.style.position = 'fixed'; canvas.style.top = '0'; canvas.style.left = '0'; document.body.appendChild(canvas); const ctx = canvas.getContext('2d'); // Add CSS to body document.body.style.margin = '0'; document.body.style.overflow = 'hidden'; // Make canvas fullscreen function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Add these at the top of the file, after the canvas setup but before the game constants const GAME_STATE = { PLAYING: 'playing', GAME_OVER: 'game_over' }; const PLATFORM_TYPE = { NORMAL: 'normal', DEADLY: 'deadly' }; // Game constants const PLAYER_SIZE = 20; const PLATFORM_HEIGHT = 20; const MIN_PLATFORM_WIDTH = 100; const MAX_PLATFORM_WIDTH = 300; const GRAVITY = 0.5; const JUMP_FORCE = 12; const MOVE_SPEED = 7; // Add these constants for level generation const MIN_PARTITION_SIZE = MAX_PLATFORM_WIDTH + 20; // Reduced from 50 to allow more splits const PARTITION_RATIO = 0.3; // Reduced from 0.4 to allow more uneven splits // Add this constant for minimum platform spacing const MIN_PLATFORM_SPACING = 100; // Minimum distance between platforms // Add these helper functions for the RSP tree function randomRange(min, max) { return min + Math.random() * (max - min); } // Core partition data structure (plain object) function createPartition(x, y, width, height) { return { x, y, width, height, platform: null, left: null, right: null }; } // Split function that returns new partitions instead of mutating 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) }; } // Replace the platformsOverlap function with this new version function platformsOverlap(platform1, platform2) { // Add a small buffer space around platforms (5 pixels) const buffer = 5; // Check if the rectangles (with buffer) overlap 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 ); } // Modify the generatePlatformsForPartition function 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; // Increased max attempts for better placement for (let i = 0; i < platformCount; i++) { let validPlatform = null; let attempts = 0; while (!validPlatform && attempts < maxAttempts) { const platformWidth = randomRange( MIN_PLATFORM_WIDTH, Math.min(MAX_PLATFORM_WIDTH, partition.width * 0.8) ); // Calculate valid range for platform placement const minX = Math.max( partition.x, partition.x + (partition.width * (i / platformCount)) ); const maxX = Math.min( partition.x + partition.width - platformWidth, partition.x + (partition.width * ((i + 1) / platformCount)) ); // Only proceed if we have valid space if (maxX > minX) { const candidatePlatform = { x: randomRange(minX, maxX), y: randomRange( partition.y + PLATFORM_HEIGHT, partition.y + partition.height - PLATFORM_HEIGHT * 2 ), width: platformWidth, height: PLATFORM_HEIGHT, // 20% chance for a platform to be deadly after level 1 type: (level > 1 && Math.random() < 0.2) ? PLATFORM_TYPE.DEADLY : PLATFORM_TYPE.NORMAL }; // Check overlap with all existing platforms let overlapping = false; for (const existingPlatform of [...platforms, ...newPlatforms]) { if (platformsOverlap(candidatePlatform, existingPlatform)) { overlapping = true; break; } } if (!overlapping) { validPlatform = candidatePlatform; } } attempts++; } if (validPlatform) { newPlatforms.push(validPlatform); } } return newPlatforms; } // Game state let gameState = GAME_STATE.PLAYING; let level = 1; let platforms = []; 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 }; // Add reset player function function resetPlayer() { player.x = PLAYER_SIZE; player.y = window.innerHeight - PLAYER_SIZE * 2; 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(); } // Restore the original generateLevel function function generateLevel() { platforms = []; // Add starting platform platforms.push({ x: 0, y: window.innerHeight - PLATFORM_HEIGHT, width: MIN_PLATFORM_WIDTH, height: PLATFORM_HEIGHT, type: PLATFORM_TYPE.NORMAL }); // Add end platform 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); } // Create sections and generate platforms 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); } } } // Handle player input const keys = {}; window.addEventListener('keydown', e => keys[e.key] = true); window.addEventListener('keyup', e => keys[e.key] = false); function updatePlayer() { if (gameState === GAME_STATE.GAME_OVER) { if (keys[' ']) { // Space key to restart when game over resetPlayer(); } return; } // Horizontal movement if (keys['ArrowLeft']) player.velocityX = -MOVE_SPEED; else if (keys['ArrowRight']) player.velocityX = MOVE_SPEED; else player.velocityX = 0; // Toggle gravity when 'g' or spacebar is pressed if ((keys['g'] || keys[' ']) && !player.lastGravityKey) { player.gravityMultiplier *= -1; player.velocityY = 0; // Reset vertical velocity when flipping gravity } player.lastGravityKey = keys['g'] || keys[' ']; // Apply gravity (now with direction) player.velocityY += GRAVITY * player.gravityMultiplier; // Jump (in the direction opposite to gravity) if (keys['ArrowUp'] && keys['ArrowUp'] !== player.lastJumpKey && player.jumpsLeft > 0) { player.velocityY = -JUMP_FORCE * player.gravityMultiplier; player.jumpsLeft--; player.isJumping = true; } player.lastJumpKey = keys['ArrowUp']; // Update position player.x += player.velocityX; player.y += player.velocityY; // Check platform collisions (modified for deadly platforms) 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) { // Normal gravity collision 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 { // Reversed gravity collision 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) { player.isDead = true; gameState = GAME_STATE.GAME_OVER; } else { player.velocityY = 0; player.isJumping = false; player.jumpsLeft = 2; } } } } // Check for deadly border collisions (modified for gap) if (player.y <= DEADLY_BORDER_HEIGHT) { // Only die if touching the actual drawn deadly border if (player.x < canvas.width - exit.size - 300) { player.isDead = true; gameState = GAME_STATE.GAME_OVER; } } else if (player.y + PLAYER_SIZE >= canvas.height - DEADLY_BORDER_HEIGHT) { player.isDead = true; gameState = GAME_STATE.GAME_OVER; } // Keep player in bounds (modified to account for deadly borders) 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; } // Check if player reached exit 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++; player.x = PLAYER_SIZE; player.y = window.innerHeight - PLAYER_SIZE; generateLevel(); } } // Add these variables at the top with other game state let frameCount = 0; let lastFpsUpdate = 0; let currentFps = 0; // Add this constant at the top with other constants const DEADLY_BORDER_HEIGHT = 7; // Update the draw function to include the deadly borders function draw(currentTime) { // Clear canvas with light grey ctx.fillStyle = '#E0E0E0'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw deadly borders with gap for exit ctx.fillStyle = '#FF0000'; // Top border (with gap) ctx.fillRect(0, 0, canvas.width - exit.size - 300, DEADLY_BORDER_HEIGHT); // Left section stops before exit // Bottom border (full width) ctx.fillRect(0, canvas.height - DEADLY_BORDER_HEIGHT, canvas.width, DEADLY_BORDER_HEIGHT); // Draw platforms (almost black for normal platforms, red for deadly) for (let platform of platforms) { ctx.fillStyle = platform.type === PLATFORM_TYPE.DEADLY ? '#FF0000' : // Red for deadly platforms '#1A1A1A'; // Almost black for normal platforms ctx.fillRect(platform.x, platform.y, platform.width, platform.height); } // Draw exit (keeping green for now) ctx.fillStyle = '#32CD32'; ctx.fillRect(exit.x, exit.y, exit.size, exit.size); // Draw player in almost black if (!player.isDead) { ctx.fillStyle = '#1A1A1A'; ctx.fillRect(player.x, player.y, PLAYER_SIZE, PLAYER_SIZE); } // Draw level number only ctx.fillStyle = '#000000'; ctx.font = '20px Arial'; ctx.textAlign = 'left'; ctx.fillText(`Level: ${level}`, 10, 30); // Draw game over screen if (gameState === GAME_STATE.GAME_OVER) { ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#FFFFFF'; ctx.font = '48px Arial'; ctx.textAlign = 'center'; ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2); ctx.font = '24px Arial'; ctx.fillText('Press SPACE to restart', canvas.width / 2, canvas.height / 2 + 40); ctx.textAlign = 'left'; // Reset text align } } // Add these constants at the top with other game constants const FPS = 60; const FRAME_TIME = 1000 / FPS; // Replace the simple game loop with this new version let lastFrameTime = 0; let accumulator = 0; function gameLoop(currentTime) { if (lastFrameTime === 0) { lastFrameTime = currentTime; lastFpsUpdate = currentTime; } // Calculate delta time const deltaTime = currentTime - lastFrameTime; lastFrameTime = currentTime; // Accumulate time to process accumulator += deltaTime; // Update physics at a fixed time step while (accumulator >= FRAME_TIME) { updatePlayer(); accumulator -= FRAME_TIME; } // Render at whatever frame rate the browser can handle draw(currentTime); requestAnimationFrame(gameLoop); } // Initialize the first level before starting the game loop generateLevel(); // Start the game loop lastFrameTime = 0; accumulator = 0; requestAnimationFrame(gameLoop);