import { CONFIG, COLORS, CANVAS } from './config.js'; let highScore = localStorage.getItem('highScore') || 0; const player = { x: 0, y: 0, health: CONFIG.PLAYER_HEALTH, score: 0, damage: CONFIG.PLAYER_BASE_DAMAGE, totalDamageDone: 0, totalDamageTaken: 0, cellsTraveled: 0, killCount: 0, itemsCollected: 0, }; const exit = { x: Math.floor(Math.random() * CONFIG.GRID_SIZE), y: Math.floor(Math.random() * CONFIG.GRID_SIZE) }; let walls = []; let enemies = []; let items = []; let combatDots = {}; function isValidMove(newX, newY) { return ( newX >= 0 && newX < CONFIG.GRID_SIZE && newY >= 0 && newY < CONFIG.GRID_SIZE && !walls.some(wall => wall.x === newX && wall.y === newY) ); } function generateExit() { let distance = 0; do { exit.x = Math.floor(Math.random() * CONFIG.GRID_SIZE); exit.y = Math.floor(Math.random() * CONFIG.GRID_SIZE); distance = Math.abs(exit.x - player.x) + Math.abs(exit.y - player.y); } while (distance < 4); } function generateEnemies() { enemies = []; // Generate between 0 and MAX_ENEMIES_ON_LEVEL enemies if the player's score is 4 or lower // Generate between MIN_ENEMIES_ON_LEVEL and MAX_ENEMIES_ON_LEVEL enemies if the player's score is 5 or higher const numEnemies = player.score > 4 ? Math.floor(Math.random() * (CONFIG.MAX_ENEMIES_ON_LEVEL - CONFIG.MIN_ENEMIES_ON_LEVEL + 1)) + CONFIG.MIN_ENEMIES_ON_LEVEL : Math.floor(Math.random() * (CONFIG.MAX_ENEMIES_ON_LEVEL + 1)); for (let i = 0; i < numEnemies;) { let enemyX, enemyY; do { enemyX = Math.floor(Math.random() * CONFIG.GRID_SIZE); enemyY = Math.floor(Math.random() * CONFIG.GRID_SIZE); } while ( (enemyX === player.x && enemyY === player.y) || (enemyX === exit.x && enemyY === exit.y) || walls.some(wall => wall.x === enemyX && wall.y === enemyY) || (Math.abs(enemyX - player.x) + Math.abs(enemyY - player.y) < 2) // Ensure enemy is at least 2 spaces away from player ); if (isReachable(player.x, player.y, enemyX, enemyY)) { enemies.push({ color: COLORS.enemy, x: enemyX, y: enemyY, health: Math.floor(Math.random() * (CONFIG.MAX_ENEMY_HEALTH - CONFIG.MIN_ENEMY_HEALTH + 1)) + CONFIG.MIN_ENEMY_HEALTH }); i++; // Only increment i if the enemy is reachable and actually placed on the board, this avoids levels with fewer enemies than MIN_ENEMIES_ON_LEVEL } } // Generate a boss enemy every ENEMY_BOSS_OCCURRENCE levels if (player.score % CONFIG.ENEMY_BOSS_OCCURRENCE === 0 && player.score > 0) { let bossX, bossY; do { bossX = exit.x; // Boss enemies always appear at the exit bossY = exit.y; // This ensures that they're not in little rooms that the player can't reach } while ( (Math.abs(bossX - player.x) + Math.abs(bossY - player.y) < 2) // Ensure boss is at least 2 spaces away from player ); if (isReachable(player.x, player.y, bossX, bossY)) { enemies.push({ isBoss: true, color: COLORS.boss, x: bossX, y: bossY, health: CONFIG.MAX_ENEMY_HEALTH + 2 }); } } } function generateWallsNaive() { walls = []; let numWalls = Math.floor(Math.random() * (CONFIG.WALLS_MAX - CONFIG.WALLS_MIN + 1)) + CONFIG.WALLS_MIN; while (walls.length < numWalls) { const wallX = Math.floor(Math.random() * CONFIG.GRID_SIZE); const wallY = Math.floor(Math.random() * CONFIG.GRID_SIZE); if ( (wallX === player.x && wallY === player.y) || // Check that a wall is not placed on the starting position (wallX === exit.x && wallY === exit.y) || // Check that a wall is not placed on the exit enemies.some(enemy => enemy.x === wallX && enemy.y === wallY) || // Check that a wall is not placed on any enemies items.some(item => item.x === wallX && item.y === wallY) // Check that a wall is not placed on any items ) continue; if (!walls.some(wall => wall.x === wallX && wall.y === wallY)) { walls.push({ x: wallX, y: wallY }); } } if (!isReachable(player.x, player.y, exit.x, exit.y)) { generateWallsNaive(); } } function generateWallsDrunkardsWalk() { walls = []; const numWalls = Math.floor(Math.random() * (CONFIG.WALLS_MAX - CONFIG.WALLS_MIN + 1)) + CONFIG.WALLS_MIN; let wallX = Math.floor(Math.random() * CONFIG.GRID_SIZE); let wallY = Math.floor(Math.random() * CONFIG.GRID_SIZE); while (walls.length < numWalls) { if ( (wallX !== player.x || wallY !== player.y) && // Check that a wall is not placed on the starting position (wallX !== exit.x || wallY !== exit.y) && // Check that a wall is not placed on the exit !enemies.some(enemy => enemy.x === wallX && enemy.y === wallY) && // Check that a wall is not placed on any enemies !items.some(item => item.x === wallX && item.y === wallY) && // Check that a wall is not placed on any items !walls.some(wall => wall.x === wallX && wall.y === wallY) // Check that a wall is not placed on an existing wall ) { walls.push({ x: wallX, y: wallY }); } // Randomly move to a neighboring cell const direction = Math.floor(Math.random() * 4); switch (direction) { case 0: wallX = Math.max(0, wallX - 1); break; // Move left case 1: wallX = Math.min(CONFIG.GRID_SIZE - 1, wallX + 1); break; // Move right case 2: wallY = Math.max(0, wallY - 1); break; // Move up case 3: wallY = Math.min(CONFIG.GRID_SIZE - 1, wallY + 1); break; // Move down } } if (!isReachable(player.x, player.y, exit.x, exit.y)) { generateWallsDrunkardsWalk(); } } function generateWallsCellularAutomata() { walls = []; const map = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(0)); // Initialize a map with random walls for (let x = 0; x < CONFIG.GRID_SIZE; x++) { for (let y = 0; y < CONFIG.GRID_SIZE; y++) { if (Math.random() < 0.4) { map[x][y] = 1; } } } for (let i = 0; i < 4; i++) { // Create a new map to store the next state of the cellular automata const newMap = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(0)); // Iterate over each cell in the grid for (let x = 0; x < CONFIG.GRID_SIZE; x++) { for (let y = 0; y < CONFIG.GRID_SIZE; y++) { // Count the number of neighboring walls const neighbors = countNeighbors(map, x, y); if (map[x][y] === 1) { // If the cell is a wall, it stays a wall if it has 4 or more neighbors newMap[x][y] = neighbors >= 4 ? 1 : 0; } else { // If the cell is empty, it turn into a wall if it has 5 or more neighbors newMap[x][y] = neighbors >= 5 ? 1 : 0; } } } // Update the original map with the new state map.forEach((row, x) => row.forEach((cell, y) => map[x][y] = newMap[x][y])); } // Convert map to walls array for (let x = 0; x < CONFIG.GRID_SIZE; x++) { for (let y = 0; y < CONFIG.GRID_SIZE; y++) { if (map[x][y] === 1 && (x !== player.x || y !== player.y) && (x !== exit.x || y !== exit.y) && !enemies.some(enemy => enemy.x === x && enemy.y === y) && !items.some(item => item.x === x && item.y === y)) { walls.push({ x, y }); } } } if (!isReachable(player.x, player.y, exit.x, exit.y)) { generateWallsCellularAutomata(); } } function countNeighbors(map, x, y) { let count = 0; for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < CONFIG.GRID_SIZE && ny >= 0 && ny < CONFIG.GRID_SIZE) { count += map[nx][ny]; } else { count++; // Consider out-of-bounds bits as walls } } } return count; } function generateWallsRSP() { walls = []; function splitNode(x, y, width, height) { const splitHorizontally = Math.random() > 0.5; const max = (splitHorizontally ? height : width) - CONFIG.MIN_ROOM_SIZE; if (max <= CONFIG.MIN_ROOM_SIZE) return [{ x, y, width, height }]; const split = Math.floor(Math.random() * (max - CONFIG.MIN_ROOM_SIZE)) + CONFIG.MIN_ROOM_SIZE; if (splitHorizontally) { return [ ...splitNode(x, y, width, split), ...splitNode(x, y + split, width, height - split) ]; } else { return [ ...splitNode(x, y, split, height), ...splitNode(x + split, y, width - split, height) ]; } } function createRoom(node) { const roomWidth = Math.floor(Math.random() * (node.width - 1)) + 1; const roomHeight = Math.floor(Math.random() * (node.height - 1)) + 1; const roomX = node.x + Math.floor(Math.random() * (node.width - roomWidth)); const roomY = node.y + Math.floor(Math.random() * (node.height - roomHeight)); return { x: roomX, y: roomY, width: roomWidth, height: roomHeight }; } const nodes = splitNode(0, 0, CONFIG.GRID_SIZE, CONFIG.GRID_SIZE); const rooms = nodes.map(createRoom); rooms.forEach(room => { for (let x = room.x; x < room.x + room.width; x++) { for (let y = room.y; y < room.y + room.height; y++) { if ( (x !== player.x || y !== player.y) && (x !== exit.x || y !== exit.y) && !enemies.some(enemy => enemy.x === x && enemy.y === y) && !items.some(item => item.x === x && item.y === y) ) { walls.push({ x, y }); } } }}); if (!isReachable(player.x, player.y, exit.x, exit.y)) { generateWallsRSP(); } } function generateWalls() { const wallGenerators = [ { name: 'RSP Tree', func: generateWallsRSP }, { name: 'Naive', func: generateWallsNaive }, { name: 'Cellular Automata', func: generateWallsCellularAutomata }, { name: 'Drunkard\'s Walk', func: generateWallsDrunkardsWalk } ]; const randomIndex = Math.floor(Math.random() * wallGenerators.length); const selectedGenerator = wallGenerators[randomIndex]; console.log(`Wall generator: ${selectedGenerator.name}`); selectedGenerator.func(); } function generateItems() { items = []; const numItems = Math.floor(Math.random() * (CONFIG.ITEMS_MAX - CONFIG.ITEMS_MIN + 1)) + CONFIG.ITEMS_MIN; for (let i = 0; i < numItems;) { let itemX, itemY; do { itemX = Math.floor(Math.random() * CONFIG.GRID_SIZE); itemY = Math.floor(Math.random() * CONFIG.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) || items.some(item => item.x === itemX && item.y === itemY) ); const itemType = Math.random() < 0.5 ? 'diamond' : 'pentagon'; // 50% chance for each type if (isReachable(player.x, player.y, itemX, itemY)) items.push({ x: itemX, y: itemY, type: itemType }); i++; // Only increment i if the item is reachable and actually placed on the board, this avoids levels with fewer items than ITEMS_MIN } } // Checks to see if there's a path between any two points on the level function isReachable(startX, startY, targetX, targetY) { const visited = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(false)); // Initialize a 2D array of false values function dfs(x, y) { if (x < 0 || x >= CONFIG.GRID_SIZE || y < 0 || y >= CONFIG.GRID_SIZE) return false; // Are the coordinates in bounds? if (visited[x][y]) return false; // Have we already visited this cell? if (walls.some(wall => wall.x === x && wall.y === y)) return false; // Is there a wall here? visited[x][y] = true; // Mark this cell as visited if (x === targetX && y === targetY) return true; // Have we reached the target? return dfs(x + 1, y) || dfs(x - 1, y) || dfs(x, y + 1) || dfs(x, y - 1); // Recursively check neighbors } return dfs(startX, startY); } function drawGrid() { CANVAS.ctx.clearRect(0, 0, CANVAS.canvas.width, CANVAS.canvas.height); CANVAS.ctx.lineWidth = 2; CANVAS.ctx.strokeStyle = COLORS.grid; for (let row = 0; row < CONFIG.GRID_SIZE; row++) { for (let col = 0; col < CONFIG.GRID_SIZE; col++) { CANVAS.ctx.strokeRect(col * CANVAS.tileSize, row * CANVAS.tileSize, CANVAS.tileSize, CANVAS.tileSize); } } } function drawExit() { const x = exit.x * CANVAS.tileSize + CANVAS.tileSize / 2; const y = exit.y * CANVAS.tileSize + CANVAS.tileSize / 2; CANVAS.ctx.beginPath(); CANVAS.ctx.moveTo(x, y - CANVAS.tileSize / 3); CANVAS.ctx.lineTo(x + CANVAS.tileSize / 3, y + CANVAS.tileSize / 3); CANVAS.ctx.lineTo(x - CANVAS.tileSize / 3, y + CANVAS.tileSize / 3); CANVAS.ctx.closePath(); CANVAS.ctx.fillStyle = COLORS.exit; CANVAS.ctx.fill(); } function drawWalls() { CANVAS.ctx.fillStyle = COLORS.walls; walls.forEach(wall => { CANVAS.ctx.fillRect(wall.x * CANVAS.tileSize, wall.y * CANVAS.tileSize, CANVAS.tileSize, CANVAS.tileSize); }); } function drawItems() { items.forEach(item => { const x = item.x * CANVAS.tileSize + CANVAS.tileSize / 2; const y = item.y * CANVAS.tileSize + CANVAS.tileSize / 2; CANVAS.ctx.fillStyle = item.type === 'diamond' ? COLORS.diamond : COLORS.pentagon; CANVAS.ctx.beginPath(); if (item.type === 'diamond') { CANVAS.ctx.moveTo(x, y - CANVAS.tileSize / 4); CANVAS.ctx.lineTo(x + CANVAS.tileSize / 4, y); CANVAS.ctx.lineTo(x, y + CANVAS.tileSize / 4); CANVAS.ctx.lineTo(x - CANVAS.tileSize / 4, y); } else { const sides = 5; const radius = CANVAS.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) CANVAS.ctx.moveTo(pointX, pointY); else CANVAS.ctx.lineTo(pointX, pointY); } } CANVAS.ctx.closePath(); CANVAS.ctx.fill(); }); } function drawCharacterBorder(x, y, radius, damageTaken) { const dashLength = 5; const gapLength = Math.max(1, damageTaken * 2); // More damage, larger gaps CANVAS.ctx.lineWidth = 2; CANVAS.ctx.strokeStyle = '#2d2d2d'; CANVAS.ctx.setLineDash([dashLength, gapLength]); CANVAS.ctx.beginPath(); CANVAS.ctx.arc(x, y, radius, 0, 2 * Math.PI); CANVAS.ctx.stroke(); CANVAS.ctx.setLineDash([]); // Reset to a solid line } function drawEnemies() { enemies.forEach(enemy => { const x = enemy.x * CANVAS.tileSize + CANVAS.tileSize / 2; const y = enemy.y * CANVAS.tileSize + CANVAS.tileSize / 2; const opacity = enemy.health / CONFIG.MAX_ENEMY_HEALTH; // Opacity based on health const radius = CANVAS.tileSize / 3; const damageTaken = CONFIG.MAX_ENEMY_HEALTH - enemy.health; CANVAS.ctx.beginPath(); CANVAS.ctx.arc(x, y, radius, 0, 2 * Math.PI); CANVAS.ctx.fillStyle = `${enemy.color}${opacity})`; CANVAS.ctx.fill(); drawCharacterBorder(x, y, radius, damageTaken); }); } function drawPlayer() { const x = player.x * CANVAS.tileSize + CANVAS.tileSize / 2; const y = player.y * CANVAS.tileSize + CANVAS.tileSize / 2; const radius = CANVAS.tileSize / 3; const playerOpacity = player.health / CONFIG.PLAYER_HEALTH; // Opacity based on health CANVAS.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) { CANVAS.ctx.moveTo(hexX, hexY); } else { CANVAS.ctx.lineTo(hexX, hexY); } } CANVAS.ctx.closePath(); CANVAS.ctx.fillStyle = `${COLORS.player}${playerOpacity})`; CANVAS.ctx.fill(); CANVAS.ctx.lineWidth = 2; CANVAS.ctx.strokeStyle = '#2d2d2d'; CANVAS.ctx.stroke(); } function drawCombatDots() { for (const key in combatDots) { const [cellX, cellY] = key.split(',').map(Number); const dots = combatDots[key]; dots.forEach(dot => { CANVAS.ctx.beginPath(); CANVAS.ctx.arc(cellX * CANVAS.tileSize + dot.x, cellY * CANVAS.tileSize + dot.y, 2, 0, Math.PI * 2); CANVAS.ctx.fillStyle = dot.color; CANVAS.ctx.fill(); CANVAS.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, CONFIG.PLAYER_MAX_HEALTH); console.log("Collected pentagon! Healed " + healAmount + " health."); } items = items.filter(item => item !== collectedItem); // Remove collected item } } 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 < CONFIG.GRID_SIZE && cellY >= 0 && cellY < CONFIG.GRID_SIZE) { const key = `${cellX},${cellY}`; if (!combatDots[key]) { combatDots[key] = []; } for (let i = 0; i < CONFIG.DOTS_PER_HIT; i++) { combatDots[key].push({ x: Math.random() * CANVAS.tileSize, y: Math.random() * CANVAS.tileSize, color: color }); } } }); } function movePlayer(dx, dy) { const newX = player.x + dx; const newY = player.y + dy; if (isValidMove(newX, newY) && !enemies.some(enemy => enemy.x === newX && enemy.y === newY)) { if (newX !== player.x || newY !== player.y) player.cellsTraveled++; player.x = newX; player.y = newY; handleItemCollection(); // Did the player collect an item? checkPlayerAtExit(); // Did the player get to the exit after moving? } else { // If an enemy is in the target cell, player stays put and does combat const enemyInTargetCell = enemies.find(enemy => enemy.x === newX && enemy.y === newY); if (enemyInTargetCell) { handleDamage(player, enemyInTargetCell); } } moveEnemies(); render(); } function moveEnemies() { enemies.forEach(enemy => { const distanceToPlayer = Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y); const distanceToExit = Math.abs(enemy.x - exit.x) + Math.abs(enemy.y - exit.y); // FIXME: necessary? // If the enemy is closer to the exit than the player, move towards the exit // Bosses are more aggressive about chasing the player const target = distanceToPlayer <= (enemy.isBoss ? CONFIG.ENEMY_CHASE_DISTANCE + 2 : CONFIG.ENEMY_CHASE_DISTANCE) ? player : exit; const path = findPath(enemy, target); if (path.length > 1) { const nextStep = path[1]; const enemyInNextStep = enemies.find(e => e.x === nextStep.x && e.y === nextStep.y); // Is the next step occupied by an enemy? if (!enemyInNextStep && !(nextStep.x === player.x && nextStep.y === player.y)) { // Move to the next place enemy.x = nextStep.x; enemy.y = nextStep.y; } else if (nextStep.x === player.x && nextStep.y === player.y) { // If the player is in the next step, stay put and do combat handleDamage(player, enemy); } } }); } function findPath(start, goal) { const queue = [{ x: start.x, y: start.y, path: [] }]; const visited = new Set(); visited.add(`${start.x},${start.y}`); while (queue.length > 0) { const { x, y, path } = queue.shift(); const newPath = [...path, { x, y }]; if (x === goal.x && y === goal.y) { return newPath; } const directions = [ { dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 } ]; directions.forEach(({ dx, dy }) => { const newX = x + dx; const newY = y + dy; const key = `${newX},${newY}`; // Check if the new position is within the level and if it is passable if ( newX >= 0 && newX < CONFIG.GRID_SIZE && newY >= 0 && newY < CONFIG.GRID_SIZE && // Have we already been here? !visited.has(key) && // Is it a wall? !walls.some(wall => wall.x === newX && wall.y === newY) && // Is there an enemy already there? !enemies.some(enemy => enemy.x === newX && enemy.y === newY) ) { queue.push({ x: newX, y: newY, path: newPath }); visited.add(key); } }); } return []; } let combatAnimationEnabled = localStorage.getItem('combatAnimationEnabled'); if (combatAnimationEnabled === null) { combatAnimationEnabled = true; // default to on...is that a good idea? localStorage.setItem('combatAnimationEnabled', combatAnimationEnabled); } else { combatAnimationEnabled = combatAnimationEnabled === 'true'; } document.getElementById('toggleShake').textContent = combatAnimationEnabled ? 'Turn Shake Off' : 'Turn Shake On'; function toggleShake() { combatAnimationEnabled = !combatAnimationEnabled; localStorage.setItem('combatAnimationEnabled', combatAnimationEnabled); document.getElementById('toggleShake').textContent = combatAnimationEnabled ? 'Turn Shake Off' : 'Turn Shake On'; } function combatAnimation() { const canvas = document.getElementById('gameCanvas'); canvas.classList.add('shake'); canvas.addEventListener('animationend', () => { canvas.classList.remove('shake'); }, { once: true }); } function handleDamage(player, enemy) { const enemyMisses = Math.random() < 0.5; // 50% 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, enemy.isBoss ? COLORS.combatDotBoss : COLORS.combatDotEnemy); // Add dots for enemy damage console.log("Player hit! Enemy health: " + enemy.health); if (combatAnimationEnabled) { combatAnimation(); // Trigger the shake animation } if (enemy.health <= 0) { player.killCount++; enemies = enemies.filter(e => e !== enemy); if (enemy.isBoss) { // Defeating a boss restores 3 player health player.health = Math.min(player.health + 3, CONFIG.PLAYER_MAX_HEALTH); console.log("Defeated a boss! Healed " + 3 + " health. Player health " + player.health); } else if (Math.random() < 0.25) { player.health = Math.min(player.health + 1, CONFIG.PLAYER_MAX_HEALTH); } } if (player.health <= 0) { if (player.score > highScore) { highScore = player.score; localStorage.setItem('highScore', highScore); } alert(`Score: ${player.score}\nDistance Traveled: ${player.cellsTraveled}\nTotal Damage Dealt: ${player.totalDamageDone}\nTotal Damage Received: ${player.totalDamageTaken}\nCircles Vanquished: ${player.killCount}\n\nHigh Score: ${highScore}`); resetGame(); } } function resetGame() { const canvas = document.getElementById('gameCanvas'); if (canvas.classList.contains('shake')) { canvas.classList.remove('shake'); } player.health = CONFIG.PLAYER_HEALTH; player.damage = CONFIG.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(); generateWalls(); generateEnemies(); generateItems(); render(); if (AUTO_PLAY) autoPlay(); } function checkPlayerAtExit() { if (player.x === exit.x && player.y === exit.y) { player.score += 1; player.damage = CONFIG.PLAYER_BASE_DAMAGE; console.groupCollapsed("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.log("High Score: " + highScore); console.groupEnd(); combatDots = {}; generateExit(); generateWalls(); generateEnemies(); generateItems(); render(); if (AUTO_PLAY) autoPlay(); } } function render() { drawGrid(); drawPlayer(); drawExit(); drawItems(); drawEnemies(); 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.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); // Prevent scrolling on touchstart touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }); CANVAS.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.canvas.getBoundingClientRect(); CANVAS.canvas.width = rect.width; CANVAS.canvas.height = rect.height; CANVAS.tileSize = CANVAS.updateTileSize(); // Update tile size based on canvas dimensions render(); }; window.addEventListener('resize', resizeCanvas); resizeCanvas(); // Initial level setup generateExit(); generateWalls(); generateEnemies(); generateItems(); render(); 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) { stuckCounter++; } else { stuckCounter = 0; lastPosition = { x: player.x, y: player.y }; } return stuckCounter > 3; // Consider yourself stuck after 3 turns in the same position }; 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; } } return false; }; const hasAdjacentEnemy = (x, y) => { return enemies.some(enemy => Math.abs(enemy.x - x) + Math.abs(enemy.y - 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; const nearestItem = findNearestItem(); if (nearestItem && Math.abs(player.x - nearestItem.x) + Math.abs(player.y - nearestItem.y) < 5) { return nearestItem; } return exit; }; const moveTowardsTarget = (target) => { const path = findPath(player, target); if (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; } } } 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(); } // Attack if can't retreat const dx = adjacentEnemy.x - player.x; const dy = adjacentEnemy.y - player.y; movePlayer(dx, dy); return true; } return false; }; const play = () => { if (playerAtExit()) { 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(); moveTowardsTarget(target); } setTimeout(play, 400); // 400ms is about 1.5 moves per second because 1000ms was terribly slow. }; 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(); } } });