diff --git a/.gitignore b/.gitignore index ceaea36..8be53ef 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ dist .yarn/install-state.gz .pnp.* +highscores.json diff --git a/src/game.js b/src/game.js index be13e21..7d60940 100644 --- a/src/game.js +++ b/src/game.js @@ -1452,23 +1452,123 @@ function adjacentCrate(x, y) { return false; } -function enemyInBlastPotential(player) { +function countAdjacentCrates(x, y) { + let count = 0; + for (const dir of DIRECTIONS) { + const nx = x + dir.dx; + const ny = y + dir.dy; + if (inBounds(nx, ny) && state.map[ny][nx].type === "crate") { + count += 1; + } + } + return count; +} + +function threatenedEnemiesFromCell(player, x, y) { + let count = 0; for (const other of state.players) { if (!other.alive || other.id === player.id) { continue; } - if (player.x !== other.x && player.y !== other.y) { + if (x !== other.x && y !== other.y) { continue; } - const distance = Math.abs(player.x - other.x) + Math.abs(player.y - other.y); + const distance = Math.abs(x - other.x) + Math.abs(y - other.y); if (distance > player.flameRange) { continue; } - if (!hasHardBlockBetween(player.x, player.y, other.x, other.y)) { - return true; + if (!hasHardBlockBetween(x, y, other.x, other.y)) { + count += 1; } } - return false; + return count; +} + +function nearestEnemyDistance(x, y, playerId) { + let best = Infinity; + for (const other of state.players) { + if (!other.alive || other.id === playerId) { + continue; + } + const distance = Math.abs(x - other.x) + Math.abs(y - other.y); + if (distance < best) { + best = distance; + } + } + return best; +} + +function botFindDirection(player, evaluator, maxDepth = 8) { + const visited = new Set([tileKey(player.x, player.y)]); + const queue = []; + + for (const dir of DIRECTIONS) { + const nx = player.x + dir.dx; + const ny = player.y + dir.dy; + if (!canEnterTile(nx, ny, player)) { + continue; + } + visited.add(tileKey(nx, ny)); + queue.push({ x: nx, y: ny, depth: 1, firstDir: dir.key }); + } + + let best = null; + + while (queue.length > 0) { + const node = queue.shift(); + const score = evaluator(node.x, node.y, node.depth); + if (Number.isFinite(score)) { + if (!best || score > best.score || (score === best.score && node.depth < best.depth)) { + best = { score, depth: node.depth, dir: node.firstDir }; + } + } + + if (node.depth >= maxDepth) { + continue; + } + + for (const dir of DIRECTIONS) { + const nx = node.x + dir.dx; + const ny = node.y + dir.dy; + const key = tileKey(nx, ny); + if (visited.has(key)) { + continue; + } + if (!canEnterTile(nx, ny, player)) { + continue; + } + visited.add(key); + queue.push({ x: nx, y: ny, depth: node.depth + 1, firstDir: node.firstDir }); + } + } + + return best ? best.dir : null; +} + +function pickEscapeDirection(player) { + return botFindDirection( + player, + (x, y, depth) => { + const danger = estimateDangerAt(x, y); + let score = -danger * 3 - depth * 0.55; + if (danger < 0.6) { + score += 3.5; + } + if (!state.fireLookup.has(tileKey(x, y))) { + score += 0.6; + } + const tile = state.map[y][x]; + if (tile && tile.powerup) { + score += 0.8; + } + return score; + }, + 9, + ); +} + +function enemyInBlastPotential(player) { + return threatenedEnemiesFromCell(player, player.x, player.y) > 0; } function updateBot(player, dt) { @@ -1481,63 +1581,112 @@ function updateBot(player, dt) { if (player.ai.thinkTimer > 0) { return; } - player.ai.thinkTimer = 0.13 + Math.random() * 0.2; + player.ai.thinkTimer = 0.08 + Math.random() * 0.12; const currentDanger = estimateDangerAt(player.x, player.y); - const liveEnemies = state.players.filter((p) => p.alive && p.id !== player.id); - const targetEnemy = liveEnemies.sort( - (a, b) => - Math.abs(a.x - player.x) + Math.abs(a.y - player.y) - - (Math.abs(b.x - player.x) + Math.abs(b.y - player.y)), - )[0]; + const currentKey = tileKey(player.x, player.y); + const targetEnemy = state.players + .filter((p) => p.alive && p.id !== player.id) + .sort( + (a, b) => + Math.abs(a.x - player.x) + Math.abs(a.y - player.y) - + (Math.abs(b.x - player.x) + Math.abs(b.y - player.y)), + )[0]; + + if (state.fireLookup.has(currentKey) || currentDanger >= 1.2) { + player.ai.desiredDir = pickEscapeDirection(player); + return; + } + + const cratePressure = countAdjacentCrates(player.x, player.y); + const enemyPressure = threatenedEnemiesFromCell(player, player.x, player.y); if ( player.bombsPlaced < player.bombCapacity && player.ai.bombCooldown <= 0 && - currentDanger < 0.9 && - (adjacentCrate(player.x, player.y) || enemyInBlastPotential(player)) && - hasEscapeRouteAfterBomb(player) + currentDanger < 0.8 && + hasEscapeRouteAfterBomb(player) && + (enemyPressure > 0 || cratePressure >= 2 || (cratePressure >= 1 && Math.random() < 0.45)) ) { if (dropBomb(player)) { - player.ai.bombCooldown = 1.4 + Math.random() * 0.8; + player.ai.bombCooldown = enemyPressure > 0 ? 0.8 + Math.random() * 0.45 : 1.1 + Math.random() * 0.55; } } - const options = []; - const dirs = shuffle([...DIRECTIONS]); - for (const dir of dirs) { - const nx = player.x + dir.dx; - const ny = player.y + dir.dy; - if (!canEnterTile(nx, ny, player)) { - continue; - } - const danger = estimateDangerAt(nx, ny); - let score = Math.random() * 0.35 - danger * 1.65; + let desiredDir = botFindDirection( + player, + (x, y, depth) => { + const tile = state.map[y][x]; + if (!tile || !tile.powerup) { + return Number.NEGATIVE_INFINITY; + } + return 8 - depth - estimateDangerAt(x, y) * 1.8; + }, + 7, + ); - if (danger < currentDanger) { - score += 2.5; - } - - const tile = state.map[ny][nx]; - if (tile.powerup) { - score += 1.6; - } - - if (adjacentCrate(nx, ny)) { - score += 0.3; - } - - if (targetEnemy) { - const nowDist = Math.abs(targetEnemy.x - player.x) + Math.abs(targetEnemy.y - player.y); - const newDist = Math.abs(targetEnemy.x - nx) + Math.abs(targetEnemy.y - ny); - score += (nowDist - newDist) * 0.45; - } - - options.push({ dir: dir.key, score }); + if (!desiredDir && targetEnemy) { + desiredDir = botFindDirection( + player, + (x, y, depth) => { + const danger = estimateDangerAt(x, y); + if (danger > 4.2) { + return Number.NEGATIVE_INFINITY; + } + const dist = Math.abs(targetEnemy.x - x) + Math.abs(targetEnemy.y - y); + let score = 6 - dist * 1.1 - depth * 0.4 - danger * 1.3; + if (countAdjacentCrates(x, y) > 0) { + score += 0.35; + } + return score; + }, + 8, + ); } - options.sort((a, b) => b.score - a.score); - player.ai.desiredDir = options.length > 0 ? options[0].dir : null; + if (!desiredDir) { + desiredDir = botFindDirection( + player, + (x, y, depth) => { + const crates = countAdjacentCrates(x, y); + if (crates <= 0) { + return Number.NEGATIVE_INFINITY; + } + return crates * 2.2 - depth * 0.55 - estimateDangerAt(x, y) * 1.4; + }, + 8, + ); + } + + if (!desiredDir) { + const options = []; + for (const dir of shuffle([...DIRECTIONS])) { + const nx = player.x + dir.dx; + const ny = player.y + dir.dy; + if (!canEnterTile(nx, ny, player)) { + continue; + } + const danger = estimateDangerAt(nx, ny); + const nearestEnemy = nearestEnemyDistance(nx, ny, player.id); + let score = Math.random() * 0.18 - danger * 1.8 - nearestEnemy * 0.16; + if (danger < currentDanger) { + score += 1.6; + } + if (adjacentCrate(nx, ny)) { + score += 0.25; + } + const tile = state.map[ny][nx]; + if (tile.powerup) { + score += 1.4; + } + options.push({ dir: dir.key, score }); + } + + options.sort((a, b) => b.score - a.score); + desiredDir = options.length > 0 ? options[0].dir : null; + } + + player.ai.desiredDir = desiredDir; } function registerMultiplayerKill(killerPlayerId, victimPlayerId) {