AI improvements
This commit is contained in:
193
bomberman.lua
193
bomberman.lua
@@ -43,7 +43,6 @@ local PLAYER_RED = 257
|
|||||||
local BOMB_SPRITE = 258
|
local BOMB_SPRITE = 258
|
||||||
local BREAKABLE_WALL_SPRITE = 259
|
local BREAKABLE_WALL_SPRITE = 259
|
||||||
local SOLID_WALL_SPRITE = 260
|
local SOLID_WALL_SPRITE = 260
|
||||||
local FLOOR_SPRITE = 261
|
|
||||||
|
|
||||||
-- Colors
|
-- Colors
|
||||||
local COLOR_BLACK = 0
|
local COLOR_BLACK = 0
|
||||||
@@ -651,6 +650,7 @@ end
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
function AI.is_dangerous(gridX, gridY)
|
function AI.is_dangerous(gridX, gridY)
|
||||||
|
-- Check active explosions
|
||||||
for _, expl in ipairs(State.explosions) do
|
for _, expl in ipairs(State.explosions) do
|
||||||
local explGridX = math.floor(expl.x / TILE_SIZE) + 1
|
local explGridX = math.floor(expl.x / TILE_SIZE) + 1
|
||||||
local explGridY = math.floor(expl.y / TILE_SIZE) + 1
|
local explGridY = math.floor(expl.y / TILE_SIZE) + 1
|
||||||
@@ -659,32 +659,49 @@ function AI.is_dangerous(gridX, gridY)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Check bombs about to explode (timer < 30) - need to escape!
|
||||||
for _, bomb in ipairs(State.bombs) do
|
for _, bomb in ipairs(State.bombs) do
|
||||||
local bombGridX = math.floor(bomb.x / TILE_SIZE) + 1
|
local bombGridX = math.floor(bomb.x / TILE_SIZE) + 1
|
||||||
local bombGridY = math.floor(bomb.y / TILE_SIZE) + 1
|
local bombGridY = math.floor(bomb.y / TILE_SIZE) + 1
|
||||||
|
local power = bomb.power or 1
|
||||||
|
|
||||||
|
-- Only urgent if bomb is about to explode
|
||||||
|
if bomb.timer < 30 then
|
||||||
if gridX == bombGridX and gridY == bombGridY then
|
if gridX == bombGridX and gridY == bombGridY then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
if gridY == bombGridY then
|
-- Check blast radius only for soon-to-explode bombs
|
||||||
if math.abs(gridX - bombGridX) <= 1 then
|
if gridY == bombGridY and math.abs(gridX - bombGridX) <= power then
|
||||||
if gridX < bombGridX then
|
local blocked = false
|
||||||
if State.map[gridY][gridX + 1] ~= SOLID_WALL then return true end
|
local minX = math.min(gridX, bombGridX)
|
||||||
elseif gridX > bombGridX then
|
local maxX = math.max(gridX, bombGridX)
|
||||||
if State.map[gridY][gridX - 1] ~= SOLID_WALL then return true end
|
for x = minX + 1, maxX - 1 do
|
||||||
|
if State.map[gridY][x] == SOLID_WALL then
|
||||||
|
blocked = true
|
||||||
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if not blocked then return true end
|
||||||
end
|
end
|
||||||
|
|
||||||
if gridX == bombGridX then
|
if gridX == bombGridX and math.abs(gridY - bombGridY) <= power then
|
||||||
if math.abs(gridY - bombGridY) <= 1 then
|
local blocked = false
|
||||||
if gridY < bombGridY then
|
local minY = math.min(gridY, bombGridY)
|
||||||
if State.map[gridY + 1][gridX] ~= SOLID_WALL then return true end
|
local maxY = math.max(gridY, bombGridY)
|
||||||
elseif gridY > bombGridY then
|
for y = minY + 1, maxY - 1 do
|
||||||
if State.map[gridY - 1][gridX] ~= SOLID_WALL then return true end
|
if State.map[y][gridX] == SOLID_WALL then
|
||||||
|
blocked = true
|
||||||
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if not blocked then return true end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- For bombs with more time, just avoid the bomb cell itself
|
||||||
|
if gridX == bombGridX and gridY == bombGridY then
|
||||||
|
return true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -734,32 +751,35 @@ function AI.has_adjacent_breakable_wall(gridX, gridY)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function AI.has_escape_route(gridX, gridY, player)
|
function AI.find_nearest_powerup(gridX, gridY)
|
||||||
local dirs = {
|
local nearest = nil
|
||||||
{0, -1},
|
local nearestDist = 9999
|
||||||
{0, 1},
|
for _, pw in ipairs(State.powerups) do
|
||||||
{-1, 0},
|
if State.map[pw.gridY][pw.gridX] == EMPTY then
|
||||||
{1, 0}
|
local dist = math.abs(pw.gridX - gridX) + math.abs(pw.gridY - gridY)
|
||||||
}
|
if dist < nearestDist then
|
||||||
for _, dir in ipairs(dirs) do
|
nearestDist = dist
|
||||||
local newX = gridX + dir[1]
|
nearest = pw
|
||||||
local newY = gridY + dir[2]
|
end
|
||||||
if Map.can_move_to(newX, newY, player) and not AI.is_dangerous(newX, newY) then
|
end
|
||||||
for _, dir2 in ipairs(dirs) do
|
end
|
||||||
local safeX = newX + dir2[1]
|
return nearest
|
||||||
local safeY = newY + dir2[2]
|
end
|
||||||
if Map.can_move_to(safeX, safeY, player) then
|
|
||||||
|
function AI.is_in_blast_line(cellX, cellY, bombX, bombY, power)
|
||||||
|
-- Check if cell is in same row or column as bomb and within power range
|
||||||
|
if cellY == bombY and math.abs(cellX - bombX) <= power then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
if cellX == bombX and math.abs(cellY - bombY) <= power then
|
||||||
end
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function AI.escape_from_bomb(player)
|
function AI.find_safe_cell(gridX, gridY, player)
|
||||||
local bombGridX = player.gridX
|
-- Find a cell to escape to that's OUTSIDE the bomb's blast line
|
||||||
local bombGridY = player.gridY
|
local power = player.bombPower
|
||||||
local dirs = {
|
local dirs = {
|
||||||
{0, -1},
|
{0, -1},
|
||||||
{0, 1},
|
{0, 1},
|
||||||
@@ -767,61 +787,72 @@ function AI.escape_from_bomb(player)
|
|||||||
{1, 0}
|
{1, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
local best_dir = nil
|
-- First try: find a path that gets us completely out of blast line
|
||||||
local best_score = -999
|
|
||||||
|
|
||||||
for _, dir in ipairs(dirs) do
|
for _, dir in ipairs(dirs) do
|
||||||
local newX = player.gridX + dir[1]
|
local newX = gridX + dir[1]
|
||||||
local newY = player.gridY + dir[2]
|
local newY = gridY + dir[2]
|
||||||
|
if Map.can_move_to(newX, newY, player) and not AI.is_dangerous(newX, newY) then
|
||||||
if Map.can_move_to(newX, newY, player) then
|
-- Check if this first step gets us out of blast line
|
||||||
local sc = 0
|
if not AI.is_in_blast_line(newX, newY, gridX, gridY, power) then
|
||||||
|
return {newX, newY}
|
||||||
if not AI.in_blast_zone(newX, newY, bombGridX, bombGridY) then
|
|
||||||
sc = sc + 100
|
|
||||||
end
|
end
|
||||||
|
-- If not, check if we can turn corner to get out
|
||||||
for _, dir2 in ipairs(dirs) do
|
for _, dir2 in ipairs(dirs) do
|
||||||
local checkX = newX + dir2[1]
|
local safeX = newX + dir2[1]
|
||||||
local checkY = newY + dir2[2]
|
local safeY = newY + dir2[2]
|
||||||
if not (checkX == bombGridX and checkY == bombGridY) then
|
if Map.can_move_to(safeX, safeY, player) and not AI.is_dangerous(safeX, safeY) then
|
||||||
if Map.can_move_to(checkX, checkY, player) then
|
if not AI.is_in_blast_line(safeX, safeY, gridX, gridY, power) then
|
||||||
sc = sc + 10
|
return {newX, newY}
|
||||||
if not AI.in_blast_zone(checkX, checkY, bombGridX, bombGridY) then
|
|
||||||
sc = sc + 20
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
if sc > best_score then
|
function AI.has_escape_route(gridX, gridY, player)
|
||||||
best_score = sc
|
return AI.find_safe_cell(gridX, gridY, player) ~= nil
|
||||||
best_dir = dir
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if best_dir then
|
function AI.escape_from_bomb(player)
|
||||||
player.gridX = player.gridX + best_dir[1]
|
local safe = AI.find_safe_cell(player.gridX, player.gridY, player)
|
||||||
player.gridY = player.gridY + best_dir[2]
|
if safe then
|
||||||
|
player.gridX = safe[1]
|
||||||
|
player.gridY = safe[2]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function AI.move_and_bomb(player, target)
|
function AI.move_and_bomb(player, target)
|
||||||
if not target then return end
|
if not target then return end
|
||||||
|
|
||||||
local dx = target.gridX - player.gridX
|
-- Check for nearby powerup first
|
||||||
local dy = target.gridY - player.gridY
|
local powerup = AI.find_nearest_powerup(player.gridX, player.gridY)
|
||||||
|
local actualTarget = target
|
||||||
|
|
||||||
|
-- If powerup is closer than target, go for powerup
|
||||||
|
if powerup then
|
||||||
|
local pwDist = math.abs(powerup.gridX - player.gridX) + math.abs(powerup.gridY - player.gridY)
|
||||||
|
local targetDist = math.abs(target.gridX - player.gridX) + math.abs(target.gridY - player.gridY)
|
||||||
|
if pwDist < targetDist or pwDist <= 5 then
|
||||||
|
actualTarget = {gridX = powerup.gridX, gridY = powerup.gridY}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local dx = actualTarget.gridX - player.gridX
|
||||||
|
local dy = actualTarget.gridY - player.gridY
|
||||||
local dist = math.abs(dx) + math.abs(dy)
|
local dist = math.abs(dx) + math.abs(dy)
|
||||||
|
|
||||||
local should_bomb = false
|
local should_bomb = false
|
||||||
if dist <= 2 then should_bomb = true end
|
if dist <= 2 and actualTarget == target then should_bomb = true end
|
||||||
if AI.has_adjacent_breakable_wall(player.gridX, player.gridY) then
|
if AI.has_adjacent_breakable_wall(player.gridX, player.gridY) then
|
||||||
should_bomb = true
|
should_bomb = true
|
||||||
end
|
end
|
||||||
|
|
||||||
if should_bomb and player.activeBombs < player.maxBombs and player.bombCooldown <= 0 then
|
if should_bomb and player.activeBombs < player.maxBombs and player.bombCooldown <= 0 then
|
||||||
if AI.has_escape_route(player.gridX, player.gridY, player) then
|
if AI.has_escape_route(player.gridX, player.gridY, player) then
|
||||||
|
player.lastGridX = player.gridX
|
||||||
|
player.lastGridY = player.gridY
|
||||||
Bomb.place(player)
|
Bomb.place(player)
|
||||||
player.bombCooldown = AI_BOMB_COOLDOWN
|
player.bombCooldown = AI_BOMB_COOLDOWN
|
||||||
AI.escape_from_bomb(player)
|
AI.escape_from_bomb(player)
|
||||||
@@ -829,6 +860,7 @@ function AI.move_and_bomb(player, target)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Build direction list
|
||||||
local dirs = {}
|
local dirs = {}
|
||||||
if dx > 0 then table.insert(dirs, {1, 0})
|
if dx > 0 then table.insert(dirs, {1, 0})
|
||||||
elseif dx < 0 then table.insert(dirs, {-1, 0})
|
elseif dx < 0 then table.insert(dirs, {-1, 0})
|
||||||
@@ -847,10 +879,18 @@ function AI.move_and_bomb(player, target)
|
|||||||
table.insert(dirs, d)
|
table.insert(dirs, d)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Try to move, avoiding going back to last position unless necessary
|
||||||
|
local fallback = nil
|
||||||
for _, dir in ipairs(dirs) do
|
for _, dir in ipairs(dirs) do
|
||||||
local newGridX = player.gridX + dir[1]
|
local newGridX = player.gridX + dir[1]
|
||||||
local newGridY = player.gridY + dir[2]
|
local newGridY = player.gridY + dir[2]
|
||||||
if Map.can_move_to(newGridX, newGridY, player) and not AI.is_dangerous(newGridX, newGridY) then
|
if Map.can_move_to(newGridX, newGridY, player) and not AI.is_dangerous(newGridX, newGridY) then
|
||||||
|
-- Avoid going back unless it's the only option
|
||||||
|
if newGridX == player.lastGridX and newGridY == player.lastGridY then
|
||||||
|
if not fallback then fallback = {newGridX, newGridY} end
|
||||||
|
else
|
||||||
|
player.lastGridX = player.gridX
|
||||||
|
player.lastGridY = player.gridY
|
||||||
player.gridX = newGridX
|
player.gridX = newGridX
|
||||||
player.gridY = newGridY
|
player.gridY = newGridY
|
||||||
return
|
return
|
||||||
@@ -858,8 +898,30 @@ function AI.move_and_bomb(player, target)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Use fallback if no other option
|
||||||
|
if fallback then
|
||||||
|
player.lastGridX = player.gridX
|
||||||
|
player.lastGridY = player.gridY
|
||||||
|
player.gridX = fallback[1]
|
||||||
|
player.gridY = fallback[2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function AI.update(player, target)
|
function AI.update(player, target)
|
||||||
if player.moving then return end
|
-- Even while moving, check if destination becomes dangerous
|
||||||
|
if player.moving then
|
||||||
|
if AI.is_dangerous(player.gridX, player.gridY) then
|
||||||
|
-- Destination is dangerous! Try to stop or reverse
|
||||||
|
local currentGridX = math.floor(player.pixelX / TILE_SIZE) + 1
|
||||||
|
local currentGridY = math.floor(player.pixelY / TILE_SIZE) + 1
|
||||||
|
if not AI.is_dangerous(currentGridX, currentGridY) then
|
||||||
|
-- Stay at current position
|
||||||
|
player.gridX = currentGridX
|
||||||
|
player.gridY = currentGridY
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local in_danger = AI.is_dangerous(player.gridX, player.gridY)
|
local in_danger = AI.is_dangerous(player.gridX, player.gridY)
|
||||||
|
|
||||||
@@ -915,6 +977,8 @@ function Player.create(gridX, gridY, color, is_ai)
|
|||||||
return {
|
return {
|
||||||
gridX = gridX,
|
gridX = gridX,
|
||||||
gridY = gridY,
|
gridY = gridY,
|
||||||
|
lastGridX = gridX,
|
||||||
|
lastGridY = gridY,
|
||||||
pixelX = (gridX - 1) * TILE_SIZE,
|
pixelX = (gridX - 1) * TILE_SIZE,
|
||||||
pixelY = (gridY - 1) * TILE_SIZE,
|
pixelY = (gridY - 1) * TILE_SIZE,
|
||||||
moving = false,
|
moving = false,
|
||||||
@@ -1158,7 +1222,6 @@ end
|
|||||||
-- 002:00043000001111000111111001111110011111100011110000011000000000000
|
-- 002:00043000001111000111111001111110011111100011110000011000000000000
|
||||||
-- 003:ddd1ddddddd1dddd1111111ddddd1dddddddd1dd1111111ddd1ddddddd1ddddd
|
-- 003:ddd1ddddddd1dddd1111111ddddd1dddddddd1dd1111111ddd1ddddddd1ddddd
|
||||||
-- 004:8888888888888888888888888888888888888888888888888888888888888888
|
-- 004:8888888888888888888888888888888888888888888888888888888888888888
|
||||||
-- 005:6666666666566656666666665666566666666666665656666666666665666566
|
|
||||||
-- </SPRITES>
|
-- </SPRITES>
|
||||||
|
|
||||||
-- <PALETTE>
|
-- <PALETTE>
|
||||||
|
|||||||
Reference in New Issue
Block a user