From 56f5302463aabf6143899977fc9c32cc8d34dac7 Mon Sep 17 00:00:00 2001 From: Afforess Date: Sat, 26 Mar 2016 21:04:42 -0400 Subject: [PATCH] major ai planning --- libs/ai/attack_plan.lua | 146 +++++++++++++++++++++++++++ libs/biter_expansion.lua | 13 ++- libs/expansion/aggressive.lua | 1 + libs/expansion/assault.lua | 1 + libs/expansion/beachhead.lua | 1 + libs/expansion/normal.lua | 1 + libs/expansion/passive.lua | 1 + libs/expansion/peaceful.lua | 1 + libs/logger.lua | 2 +- libs/map.lua | 135 +++++++++++-------------- libs/region.lua | 148 +++++++++++++++++++++------- libs/region/danger_cache.lua | 4 - libs/region/player_target_cache.lua | 93 +++++++++++++++++ 13 files changed, 424 insertions(+), 123 deletions(-) create mode 100644 libs/ai/attack_plan.lua create mode 100644 libs/region/player_target_cache.lua diff --git a/libs/ai/attack_plan.lua b/libs/ai/attack_plan.lua new file mode 100644 index 0000000..1033083 --- /dev/null +++ b/libs/ai/attack_plan.lua @@ -0,0 +1,146 @@ +require 'libs/pathfinder' + +attack_plan = {} +attack_plan.__index = attack_plan + +function attack_plan.find_best_player_target(attack_data, max_chunks) + local best_value = nil + local best_position = nil + local surface = game.surfaces[attack_data.surface_name] + local region_data = region.lookup_region_from_position(surface, attack_data.position) + + for dx = -(max_chunks), max_chunks do + for dy = -(max_chunks), max_chunks do + local tile_x = dx * 32 + attack_data.position.x + local tile_y = dx * 32 + attack_data.position.y + + local value = region.get_player_target_value_at(region_data, {x = tile_x, y = tile_y}) + value = value / (1 + ((dx * dx) + (dy * dy))) + if value > 0 and (best_value == nil or value > best_value)then + best_value = value + best_position = { x = tile_x, y = tile_y } + end + end + end + + return best_position +end + +function attack_plan.tick(attack_data) + if attack_data.completed then + return + end + local expansion_phase = BiterExpansion.get_expansion_phase(global.expansion_index) + + if not attack_data.target_position then + local chunk_search = expansion_phase.min_biter_attack_chunk_distance + local attack_target = attack_plan.find_best_player_target(attack_data, chunk_search) + if attack_target == nil then + Logger.log("Failed to find an attack target within " .. chunk_search .. " chunks of " .. serpent.line(attack_data.position)) + attack_data.completed = true + return + else + Logger.log("Best attack target within " .. chunk_search .. " chunks of " .. serpent.line(attack_data.position) .. " is " .. serpent.line(attack_target)) + attack_data.target_position = attack_target + end + end + + if attack_data.unit_group and not attack_data.wait_for_attack then + if attack_data.unit_group.valid then + attack_data.attack_in_progress = attack_data.attack_in_progress + 1 + else + attack_data.completed = true + Logger.log("Took " .. attack_data.attack_in_progress .. " ticks for the unit group to become invalid (attack complete)") + end + -- 1 minute + if attack_data.attack_in_progress > (60 * 60 * 60 * 1) then + Logger.log("Attack unit group never become invalid! Data: {" .. serpent.line(attack_data) .. "}") + attack_data.completed = true + end + return + else + attack_plan.coordinate_biters(attack_data) + return + end + + if not attack_data.pathfinder_data then + local surface = game.surfaces[attack_data.surface_name] + + attack_data.pathfinder_data = pathfinder.partial_a_star(surface, attack_data.position, attack_data.target_position, 10) + elseif not attack_data.pathfinder_data.completed then + attack_data.pathfinder_data = pathfinder.resume_a_star(attack_data.pathfinder_data, 1) + if attack_data.pathfinder_data.iterations > 1000 then + Logger.log("Pathfinding between " .. serpent.line(attack_data.position) .. " and " .. serpent.line(attack_data.target_position) .. " took more than 1000 iterations") + attack_data.completed = true + return + end + else + local path = attack_data.pathfinder_data.path + if not path then + Logger.log("Failed to find a path between " .. serpent.line(attack_data.position) .. " and " .. serpent.line(attack_data.target_position)) + attack_data.completed = true + else + attack_plan.attack_target(attack_data) + attack_data.completed = true + end + end +end + +function attack_plan.coordinate_biters(attack_data) + local expansion_phase = BiterExpansion.get_expansion_phase(global.expansion_index) + local surface = game.surfaces[attack_data.surface_name] + local pos = attack_data.position + local range = math.min(35, 15 + (3 * attack_data.biter_base.count)) + local area = {left_top = {x = pos.x - range, y = pos.y - range}, right_bottom = {x = pos.x + range, y = pos.y + range}} + local enemy_units = surface.find_entities_filtered({area = area, type = "unit", force = game.forces.enemy}) + + if #enemy_units == 0 then + Logger.log("Failed to find any enemy units at " .. serpent.line(pos, {comment = false})) + attack_data.completed = true + return + end + + Logger.log("Found " .. #enemy_units .. " enemy units at " .. serpent.line(pos, {comment = false})) + local total_x = 0 + local total_y = 0 + for _, entity in pairs(enemy_units) do + local entity_pos = entity.position + total_x = total_x + entity_pos.x + total_y = total_y + entity_pos.y + end + local avg_pos = {x = total_x / #enemy_units, y = total_y / #enemy_units} + Logger.log("Average biter position for attack position (" .. serpent.line(pos, {comment = false}) .. "): " .. serpent.line(avg_pos, {comment = false})) + + local safe_pos = surface.find_non_colliding_position("behemoth-spitter", avg_pos, 16, 0.5) + Logger.log("Safe position for grouping: " .. serpent.line(safe_pos, {comment = false})) + if not safe_pos then + attack_data.completed = true + return + end + + local unit_group = surface.create_unit_group({position = safe_pos, force = game.forces.enemy}) + for _, entity in pairs(enemy_units) do + unit_group.add_member(entity) + end + attack_data.unit_group = unit_group + attack_data.attack_in_progress = 0 + local command = {type = defines.command.attack_area, destination = attack_data.target_position, radius = 16, distraction = defines.distraction.by_damage} + + if game.evolution_factor > 0.5 and #enemy_units < 20 then + local target_pos = attack_data.target_position + local half_way_pos = { x = (safe_pos.x + target_pos.x) / 2, y = (safe_pos.y + target_pos.y) / 2} + local safe_base_pos = surface.find_non_colliding_position("behemoth-spitter", half_way_pos, 16, 0.5) + Logger.log("Safe position for building a new base: " .. serpent.line(safe_base_pos, {comment = false})) + if safe_base_pos then + command = {type = defines.command.build_base, destination = safe_base_pos, ignore_planner = 1, distraction = defines.distraction.by_damage} + end + end + + Logger.log("Command: " .. serpent.line(command, {comment=false})) + unit_group.set_command(command) + unit_group.start_moving() +end + +function attack_plan.new(surface_name, position, biter_base) + return { position = position, surface_name = surface_name, biter_base = biter_base, completed = false } +end diff --git a/libs/biter_expansion.lua b/libs/biter_expansion.lua index bd61a3e..6c82f02 100644 --- a/libs/biter_expansion.lua +++ b/libs/biter_expansion.lua @@ -46,7 +46,7 @@ function BiterExpansion.new() if expansion_phase.tick then expansion_phase:tick() end - + -- apparently this is really slow if game.tick % 600 == 0 then self:update_expansion_factors(expansion_phase) @@ -97,6 +97,9 @@ function BiterExpansion.new() self:reset_unit_group() + game.map_settings.steering.moving.separation_force = 0.005 + game.map_settings.steering.moving.separation_factor = 1 + -- cause pollution to spread farther game.map_settings.pollution.diffusion_ratio = 0.05 game.map_settings.pollution.min_to_diffuse = 10 @@ -116,10 +119,10 @@ function BiterExpansion.new() end -- At 12 hours, the time factor will be at 0.000004 (vanilla value). - -- after 108 hours of game play, max value of 0.00002 will be reached - local time_factor = math.min(0.00002, 0.000002 + 0.00000000000077160494 * ticks_played) - -- after 64 hours of gameplay, max value of 0.000025 will be reached - local pollution_factor = math.max(0.000025, 0.000005 + 0.0000000000014467593 * ticks_played) + -- after 108 hours of game play, max value of 0.00008 will be reached + local time_factor = math.min(0.00008, 0.000002 + 0.0000000000030864198 * ticks_played) + -- after 64 hours of gameplay, max value of 0.00005 will be reached + local pollution_factor = math.max(0.00005, 0.000005 + 0.0000000000028935186 * ticks_played) if global.harpa_list and global.idle_harpa_list then if #global.harpa_list > 0 or #global.idle_harpa_list > 0 then diff --git a/libs/expansion/aggressive.lua b/libs/expansion/aggressive.lua index 8ba0689..2e824a2 100644 --- a/libs/expansion/aggressive.lua +++ b/libs/expansion/aggressive.lua @@ -7,6 +7,7 @@ local expansion = { name = "Aggressive Expansion", evo_modifier = 0.96, minimum_attack_value = 0, min_biter_attack_group = 50, + min_biter_attack_chunk_distance = 12, min_biter_search_distance = 96} function expansion:update_expansion_state() diff --git a/libs/expansion/assault.lua b/libs/expansion/assault.lua index 097b7cf..596c439 100644 --- a/libs/expansion/assault.lua +++ b/libs/expansion/assault.lua @@ -7,6 +7,7 @@ local expansion = { name = "Assault", evo_modifier = 0.90, minimum_attack_value = 0, min_biter_attack_group = 75, + min_biter_attack_chunk_distance = 16, min_biter_search_distance = 128} function expansion:update_expansion_state() diff --git a/libs/expansion/beachhead.lua b/libs/expansion/beachhead.lua index a1e2239..d55f218 100644 --- a/libs/expansion/beachhead.lua +++ b/libs/expansion/beachhead.lua @@ -7,6 +7,7 @@ local expansion = { name = "Beachhead", evo_modifier = 0.85, minimum_attack_value = 0, min_biter_attack_group = 50, + min_biter_attack_chunk_distance = 20, min_biter_search_distance = 172} function expansion:update_expansion_state() diff --git a/libs/expansion/normal.lua b/libs/expansion/normal.lua index 15a0d67..bf92a4f 100644 --- a/libs/expansion/normal.lua +++ b/libs/expansion/normal.lua @@ -7,6 +7,7 @@ local expansion = { name = "Normal", evo_modifier = 1, minimum_attack_value = 1000, min_biter_attack_group = 10, + min_biter_attack_chunk_distance = 8, min_biter_search_distance = 32} function expansion:update_expansion_state() diff --git a/libs/expansion/passive.lua b/libs/expansion/passive.lua index d951c41..1629fa8 100644 --- a/libs/expansion/passive.lua +++ b/libs/expansion/passive.lua @@ -7,6 +7,7 @@ local expansion = { name = "Passive Expansion", evo_modifier = 0.975, minimum_attack_value = 100, min_biter_attack_group = 32, + min_biter_attack_chunk_distance = 10, min_biter_search_distance = 64} function expansion:update_expansion_state() diff --git a/libs/expansion/peaceful.lua b/libs/expansion/peaceful.lua index b542a5b..8be20f8 100644 --- a/libs/expansion/peaceful.lua +++ b/libs/expansion/peaceful.lua @@ -7,6 +7,7 @@ local expansion = { name = "Peaceful", evo_modifier = 1, minimum_attack_value = 10000, min_biter_attack_group = 10, + min_biter_attack_chunk_distance = 1, min_biter_search_distance = 16} function expansion:update_expansion_state() diff --git a/libs/logger.lua b/libs/logger.lua index 31fd329..934c5e2 100644 --- a/libs/logger.lua +++ b/libs/logger.lua @@ -1,6 +1,6 @@ require "defines" -Logger = {prefix='misanthrope', name = 'main', log_buffer = {}, last_write_tick = 0, last_write_size = 0, ever_written = false, debug = true} +Logger = {prefix='misanthrope', name = 'main', log_buffer = {}, last_write_tick = 0, last_write_size = 0, ever_written = false, debug = false} function Logger.log(str) local run_time_s = 0 diff --git a/libs/map.lua b/libs/map.lua index 8b82cfd..c014a7f 100644 --- a/libs/map.lua +++ b/libs/map.lua @@ -1,5 +1,6 @@ require "libs/region" require "libs/biter_targets" +require "libs/ai/attack_plan" Map = {} @@ -12,6 +13,9 @@ function Map.new() if not global.visited_regions then global.visited_regions = {} end -- list of regions with biter or spitter spawners in them if not global.enemy_regions then global.enemy_regions = {} end + + if not global.attack_plans then global.attack_plans = {} end + global.enemyRegions = nil global.powerShorts = nil global.powerLineTargets = nil @@ -22,78 +26,49 @@ function Map.new() global.regionQueue = nil global.visitedRegions = nil global.enemyRegionQueue = nil + global.foo = nil + global.bar = nil local Map = {} function Map:tick() self:iterate_map() self:iterate_enemy_regions() + local count = 0 + for key, plans in pairs(global.attack_plans) do + local all_completed = true + for _, plan in pairs(plans) do + attack_plan.tick(plan) + if not plan.completed then + all_completed = false + end + end + if all_completed then + global.attack_plans[key] = nil + end + end end function Map:update_region_ai(region_data) - if region.any_potential_targets(region_data, 1) then - Logger.log("Updating biter AI for " .. region.tostring(region_data)) - self:attack_targets(region_data) - return true + local plan_key = region.region_key(region_data) + local region_attack_plans = global.attack_plans[plan_key] + if region_attack_plans then + Logger.log("Not scheduling another attack plan, " .. plan_key .. " already has an active attack plan") end - return false - end - - function Map:attack_targets(region_data) - local expansion_phase = BiterExpansion.get_expansion_phase(global.expansion_index) - - -- setting highest_value > 0 means don't attack if there are only targets with good defenses - local highest_value = expansion_phase.minimum_attack_value - local highest_value_entity = nil - local any_targets = false - local enemy_force = game.forces.enemy - local neutral_force = game.forces.neutral - local search_distance = expansion_phase.min_biter_search_distance + math.random(32, 64) - local surface = region.get_surface(region_data) - - for _, biter_base in pairs(region_data.enemy_bases) do - local search_area = {left_top = {x = biter_base.position.x - search_distance, y = biter_base.position.y - search_distance}, - right_bottom = {x = biter_base.position.y + search_distance, y = biter_base.position.y + search_distance}} - - for entity_name, target_data in pairs(BITER_TARGETS) do - local targets = surface.find_entities_filtered({area = search_area, name = entity_name}) - for i = 1, #targets do - local force = targets[i].force - if force ~= enemy_force and force ~= neutral_force then - any_targets = true - - local value = (target_data.value + math.random(target_data.value)) * 10000 * biter_base.count - local defenses = region.get_danger_at(region_data, targets[i].position) * target_data.danger_modifier - local attack_count = region.count_attacks_on_position(region_data, targets[i].position) - value = value / math.max(1, 1 + defenses) - value = value / math.max(1, 1 + attack_count) - -- Logger.log("Biter base (" .. serpent.line(biter_base) .. ") found potential target: " .. targets[i].name .. " at position " .. serpent.line(targets[i].position) .. "\n\t\tBase value: " .. target_data.value .. ". Defense level: " .. defenses .. ". Attack count: " .. attack_count .. ". Calculated value: " .. value .. ". Highest value: " .. highest_value) - if value > highest_value then - highest_value = value - highest_value_entity = targets[i] - end - end - end + region.update_biter_base_locations(region_data) + if #region_data.enemy_bases > 0 then + region_attack_plans = {} + for _, base in pairs(region_data.enemy_bases) do + local plan = attack_plan.new(region_data.surface_name, base.position, base) + table.insert(region_attack_plans, plan) end + + Logger.log("Created new region attack plans (" .. plan_key .. "): " .. serpent.line(#region_attack_plans)) + global.attack_plans[plan_key] = region_attack_plans end - - -- cache whether any player-made structures exist in the region, don't bother attacking again until it does - region_data.any_targets = any_targets - - if highest_value_entity ~= nil then - Logger.log("Highest value target: " .. highest_value_entity.name .. " at position " .. serpent.line(highest_value_entity.position) .. ", with a value of " .. highest_value) - local unit_count = expansion_phase.min_biter_attack_group + math.random(expansion_phase.min_biter_attack_group / 2) - surface.set_multi_command({command = {type=defines.command.attack, target=highest_value_entity, distraction=defines.distraction.none}, unit_count = unit_count, unit_search_distance = search_distance}) - region.mark_attack_position(region_data, highest_value_entity.position) - - return true - else - Logger.log("No valuable targets for " .. region.tostring(region_data) .. ". Minimum value was: " .. highest_value) - return false - end + return true end function Map:iterate_enemy_regions() - -- check and update enemy regions every 5 s in non-peaceful, and every 60s in peaceful local frequency = 300 if global.expansion_state == "Peaceful" then frequency = 3600 @@ -103,35 +78,37 @@ function Map.new() if #global.enemy_regions == 0 then Logger.log("No enemy regions found.") else - local iterations = math.min(5, #global.enemy_regions) - for i = 1, iterations do - local enemy_region = table.remove(global.enemy_regions, 1) - if enemy_region == nil then break end - - Logger.log("Checking enemy region: " .. region.tostring(enemy_region)) - if region.update_biter_base_locations(enemy_region) then - -- add back to the end of the list - table.insert(global.enemy_regions, enemy_region) - - if global.expansion_state == "Peaceful" then - break - end - Logger.log("Updating enemy region ai: " .. region.tostring(enemy_region)) - if self:update_region_ai(enemy_region) then - break - end - end - end + Logger.log("Current enemy regions: " .. #global.enemy_regions) + local enemy_region = table.remove(global.enemy_regions, 1) + + Logger.log("") + Logger.log("Checking enemy region: " .. region.tostring(enemy_region)) + if region.update_biter_base_locations(enemy_region) and region.any_potential_targets(enemy_region, 16) then + -- add back to the end of the list + table.insert(global.enemy_regions, enemy_region) + + if global.expansion_state == "Peaceful" then + return + end + Logger.log("Updating enemy region ai: " .. region.tostring(enemy_region)) + self:update_region_ai(enemy_region) + end + Logger.log("Number of enemy regions: " .. #global.enemy_regions) + Logger.log("") + end end end function Map:iterate_map() - if (game.tick % 150 == 0) then + if (game.tick % 120 == 0) then local region_data = self:next_region() + + region.update_player_target_cache(region_data) + region.update_danger_cache(region_data) Logger.log("Iterating map, region: " .. region.tostring(region_data)) - if not self:is_enemy_region(region_data) and region.update_biter_base_locations(region_data) then + if not self:is_enemy_region(region_data) and (region.update_biter_base_locations(region_data) and region.any_potential_targets(region_data, 16)) then Logger.log("Found enemy region: " .. region.tostring(region_data)) table.insert(global.enemy_regions, region_data) end diff --git a/libs/region.lua b/libs/region.lua index 70d2dd0..6e6cb55 100644 --- a/libs/region.lua +++ b/libs/region.lua @@ -1,4 +1,6 @@ require "libs/region/danger_cache" +require "libs/region/player_target_cache" + --region is a 4x4 area of chunks region = {} @@ -6,7 +8,7 @@ region.__index = region region.REGION_SIZE = 4 region.CHUNK_SIZE = 32 -region.MAX_UINT = 4294967296 +MAX_UINT = 4294967296 function region.get_surface(region_data) return game.surfaces[region_data.surface_name] @@ -14,42 +16,42 @@ end function region.get_chunk_x(region_data) if region_data.x < 0 then - return bit32.lshift(region_data.x, 2) - region.MAX_UINT + return bit32.lshift(region_data.x, 2) - MAX_UINT end return bit32.lshift(region_data.x, 2) end function region.get_chunk_y(region_data) if region_data.y < 0 then - return bit32.lshift(region_data.y, 2) - region.MAX_UINT + return bit32.lshift(region_data.y, 2) - MAX_UINT end return bit32.lshift(region_data.y, 2) end function region.get_lower_pos_x(region_data) if region_data.x < 0 then - return bit32.lshift(region_data.x, 7) - region.MAX_UINT + return bit32.lshift(region_data.x, 7) - MAX_UINT end return bit32.lshift(region_data.x, 7) end function region.get_lower_pos_y(region_data) if region_data.y < 0 then - return bit32.lshift(region_data.y, 7) - region.MAX_UINT + return bit32.lshift(region_data.y, 7) - MAX_UINT end return bit32.lshift(region_data.y, 7) end function region.get_upper_pos_x(region_data) if (1 + region_data.x) < 0 then - return bit32.lshift(1 + region_data.x, 7) - region.MAX_UINT + return bit32.lshift(1 + region_data.x, 7) - MAX_UINT end return bit32.lshift(1 + region_data.x, 7) end function region.get_upper_pos_y(region_data) if (1 + region_data.y) < 0 then - return bit32.lshift(1 + region_data.y, 7) - region.MAX_UINT + return bit32.lshift(1 + region_data.y, 7) - MAX_UINT end return bit32.lshift(1 + region_data.y, 7) end @@ -114,26 +116,55 @@ end function region.update_biter_base_locations(region_data) local spawners = region.find_entities(region_data, {"biter-spawner", "spitter-spawner"}, 0) region_data.enemy_bases = {} + -- mark all spawners as their own bases, then consolidate for i = 1, #spawners do local spawner = spawners[i] - local position = spawner.position - - local found_base = false - for j = 1, #region_data.enemy_bases do - local base = region_data.enemy_bases[j] - local dist_squared = (base.position.x - position.x) * (base.position.x - position.x) + (base.position.y - position.y) * (base.position.y - position.y) - if dist_squared < 625 then - base.count = base.count + 1 - found_base = true - break + local position = { x = math.floor(spawner.position.x), y = math.floor(spawner.position.y) } + local base = {position = position, spawner_positions = {position}, count = 1} + table.insert(region_data.enemy_bases, base) + end + for i = #region_data.enemy_bases, 1, -1 do + local base = region_data.enemy_bases[i] + if base.count == 1 then + local merged = false + + for j = #region_data.enemy_bases, 1, -1 do + if i ~= j then + local other_base = region_data.enemy_bases[j] + for _, position in pairs(other_base.spawner_positions) do + local dist_squared = (base.position.x - position.x) * (base.position.x - position.x) + (base.position.y - position.y) * (base.position.y - position.y) + -- merge if <= 16 tiles away from any of their spawners + if dist_squared <= 256 then + table.insert(other_base.spawner_positions, base.position) + other_base.count = other_base.count + 1 + merged = true + break + end + end + + if merged then + break + end + end + end + + if merged then + table.remove(region_data.enemy_bases, i) end - end - if not found_base then - local base = {position = position, count = 1} - table.insert(region_data.enemy_bases, base) end end + -- recalculate base position based on center of spawners + for i = #region_data.enemy_bases, 1, -1 do + local base = region_data.enemy_bases[i] + local total_x = 0 + local total_y = 0 + for _, position in pairs(base.spawner_positions) do + total_x = total_x + position.x + total_y = total_y + position.y + end + base.position = {x = math.floor(total_x / #base.spawner_positions), y = math.floor(total_y / #base.spawner_positions)} + end Logger.log("Updated " .. region.tostring(region_data) .. " biter base locations, found: " .. serpent.line(region_data.enemy_bases)) return #region_data.enemy_bases > 0 end @@ -147,6 +178,42 @@ function region.offset(region_data, dx, dy) return region.lookup_region(region_data.surface_name, region_data.x + dx, region_data.y + dy) end +function region.get_player_target_cache(region_data) + local cache = region_data.player_target_cache + if cache and cache.calculated_at < 23750000 then + cache = nil + end + if cache == nil then + cache = player_target_cache.new(region_data) + region_data.player_target_cache = cache + end + return cache +end + +function region.update_player_target_cache(region_data) + local cache = region.get_player_target_cache(region_data) + if cache.calculated_at == -1 or (game.tick - cache.calculated_at) > (60 * 60 * 60 * 6) then + Logger.log(region.tostring(region_data) .. " - Player Target Cache recalculating...") + player_target_cache.calculate(cache) + return true + end + return false +end + +function region.get_player_target_value_at(region_data, position) + local x = math.floor(position.x) + local y = math.floor(position.y) + if region.is_coords_inside(region_data, x, y) then + region.update_player_target_cache(region_data) + + local cache = region.get_player_target_cache(region_data) + return player_target_cache.get_value(cache, x, y) + end + + local other_region = region.lookup_region_from_position(region.get_surface(region_data), position) + return region.get_player_target_value_at(other_region, position) +end + function region.get_danger_cache(region_data) local cache = region_data.danger_cache if cache == nil then @@ -156,16 +223,23 @@ function region.get_danger_cache(region_data) return cache end +function region.update_danger_cache(region_data) + local cache = region.get_danger_cache(region_data) + if cache.calculated_at == -1 or (game.tick - cache.calculated_at) > (60 * 60 * 60 * 6) then + Logger.log(region.tostring(region_data) .. " - Danger cache recalculating...") + danger_cache.calculate(cache) + return true + end + return false +end + function region.get_danger_at(region_data, position) local x = math.floor(position.x) local y = math.floor(position.y) if region.is_coords_inside(region_data, x, y) then - local cache = region.get_danger_cache(region_data) - if cache.calculated_at == -1 or (game.tick - cache.calculated_at) > (60 * 60 * 60 * 3) then - Logger.log(region.tostring(region_data) .. " - Danger cache recalculating...") - danger_cache.calculate(cache) - end + region.update_danger_cache(region_data) + local cache = region.get_danger_cache(region_data) if cache.all_zeros then return 0 end @@ -223,16 +297,22 @@ function region.count_attacks_on_position(region_data, pos) return 0 end -function region.any_potential_targets(region_data, range) - for dx = -(range), range, 1 do - for dy = -(range), range, 1 do - local other_region = region.offset(region_data, dx, dy) - if other_region.any_targets == nil or other_region.any_targets then +function region.any_potential_targets(region_data, chunk_range) + local surface = game.surfaces[region_data.surface_name] + + for dx = -(chunk_range), chunk_range do + for dy = -(chunk_range), chunk_range do + local tile_x = dx * 32 + region.get_lower_pos_x(region_data) + local tile_y = dx * 32 + region.get_lower_pos_y(region_data) + + local value = region.get_player_target_value_at(region_data, {x = tile_x, y = tile_y}) + if value > 0 then return true end end end - return false + + return best_position end function region.region_key(region_data) @@ -244,10 +324,10 @@ function region.lookup_region_from_position(surface, pos) local region_y = bit32.arshift(math.floor(pos.y), 7) -- left arithmatic shift returns unsigned int, must add sign back for negative values if (pos.x < 0) then - region_x = region_x - region.MAX_UINT + region_x = region_x - MAX_UINT end if (pos.y < 0) then - region_y = region_y - region.MAX_UINT + region_y = region_y - MAX_UINT end return region.lookup_region(surface.name, region_x, region_y) diff --git a/libs/region/danger_cache.lua b/libs/region/danger_cache.lua index 9aaf711..22acf7f 100644 --- a/libs/region/danger_cache.lua +++ b/libs/region/danger_cache.lua @@ -103,10 +103,6 @@ function danger_cache.get_danger(cache, position) return -1 end -function danger_cache.reset(cache) - cache.danger_cache = {all_zeros = true, region_key = cache.region_key, calculated_at = -1} -end - function danger_cache.new(region_data) return {all_zeros = true, region_key = region.region_key(region_data), calculated_at = -1} end diff --git a/libs/region/player_target_cache.lua b/libs/region/player_target_cache.lua new file mode 100644 index 0000000..624ac1c --- /dev/null +++ b/libs/region/player_target_cache.lua @@ -0,0 +1,93 @@ +--represents the value of player items to biters for each chunk in a region +player_target_cache = {} +player_target_cache.__index = player_target_cache + +function player_target_cache.get_region(cache) + return global.regions[cache.region_key] +end + +function player_target_cache.tostring(cache) + local cache_str = "[ " + + for chunk_x = 0, 4 do + for chunk_y = 0, 4 do + local idx = bit32.band(bit32.bor(bit32.lshift(bit32.band(chunk_x, 0x3), 2), bit32.band(chunk_y, 0x3)), 0xF) + if not cache.all_zeros then + cache_str = cache_str .. "\n\t{Chunk (" .. chunk_x .. ", " .. chunk_y .. ") value: " .. cache.values[idx] .. "}" + else + cache_str = cache_str .. "\n\t{Chunk (" .. chunk_x .. ", " .. chunk_y .. ") value: 0}" + end + end + end + cache_str = cache_str .. " ]" + + return "PlayerTargetCache {region: ".. region.tostring(player_target_cache.get_region(cache)) .. ", cache (calculated_at: " .. cache.calculated_at .. "): ".. cache_str .. "}" +end + +function player_target_cache.calculate(cache) + cache.all_zeros = nil + cache.values = {} + local size = 16 + for x = 0, size - 1 do + cache.values[x] = 0 + end + cache.calculated_at = game.tick + + local region_data = player_target_cache.get_region(cache) + local area = region.region_area(region_data, 0) + local surface = region.get_surface(region_data) + + local any_values = false + for entity_name, target_data in pairs(BITER_TARGETS) do + local entities = surface.find_entities_filtered({area = area, name = entity_name}) + for i = 1, #entities do + local entity = entities[i] + if entity.force ~= game.forces.enemy and entity.force ~= game.forces.neutral then + local entity_x = math.floor(entity.position.x) + local entity_y = math.floor(entity.position.y) + local value = target_data.value + local danger = region.get_danger_at(region_data, entity.position) + value = math.max(1, math.floor(value / (1 + danger))) + + local chunk_x = bit32.arshift(entity_x, 5) + if entity_x < 0 then + chunk_x = chunk_x - MAX_UINT + end + local chunk_y = bit32.arshift(entity_y, 5) + if entity_y < 0 then + chunk_y = chunk_y - MAX_UINT + end + + local idx = bit32.band(bit32.bor(bit32.lshift(bit32.band(chunk_x, 0x3), 2), bit32.band(chunk_y, 0x3)), 0xF) + cache.values[idx] = cache.values[idx] + value + any_values = true + end + end + end + + if not any_values then + cache.values = nil + cache.all_zeros = true + end +end + +function player_target_cache.get_value(cache, x, y) + if cache.all_zeros then + return 0 + end + local chunk_x = bit32.arshift(x, 5) + if x < 0 then + chunk_x = chunk_x - MAX_UINT + end + local chunk_y = bit32.arshift(y, 5) + if y < 0 then + chunk_y = chunk_y - MAX_UINT + end + + local idx = bit32.band(bit32.bor(bit32.lshift(bit32.band(chunk_x, 0x3), 2), bit32.band(chunk_y, 0x3)), 0xF) + return cache.values[idx] +end + +function player_target_cache.new(region_data) + return {all_zeros = true, region_key = region.region_key(region_data), calculated_at = -1} +end