diff --git a/README.md b/README.md index b3f8402..9cdb98b 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,18 @@ Base Expansion # Version History +0.14.8 - +- Feature: Rallying death cry, when a native dies on a chunk past the death threshold it will attempt to summon reinforcements from nearby nests +- Tweak: Increased unit group merge distance from 16 to 28 tiles +- Tweak: Increased retreat grab radius from 15 tiles to 24 tiles +- Tweak: Decreased vengence squad cost from 50 to 45 +- Improvement: On group merge recalculate the kamikaze threshold, so groups that become large have a chance to kamikaze before attacking +- Improvement: Disallow group merges when units are taking and receiving damage +- Fix: Corrected unit group frenzy trigger based on engaging a target +- Optimization: Switched to increment tick counter for processing and logic event +- Optimization: Rate limited rally cry to 3/0.75 sec +- Optimization: Rate limited retreat to 8/0.75 sec + 0.14.7 - - Feature: Counterattack waves trigger when the player is standing in a chunk with the death pheromone past the retreat threshold - Feature: Reinforcement waves trigger when the player is standind in a chunk that contains a nest diff --git a/control.lua b/control.lua index c938176..cb0628e 100644 --- a/control.lua +++ b/control.lua @@ -1,6 +1,7 @@ -- imports local entityUtils = require("libs/EntityUtils") +local mapUtils = require("libs/MapUtils") local unitGroupUtils = require("libs/UnitGroupUtils") local chunkProcessor = require("libs/ChunkProcessor") local mapProcessor = require("libs/MapProcessor") @@ -18,8 +19,20 @@ local tests = require("tests") local INTERVAL_LOGIC = constants.INTERVAL_LOGIC local INTERVAL_PROCESS = constants.INTERVAL_PROCESS +local MAX_RALLY_CRIES = constants.MAX_RALLY_CRIES +local MAX_RETREATS = constants.MAX_RETREATS + +local MOVEMENT_PHEROMONE = constants.MOVEMENT_PHEROMONE + +local BASE_RALLY_CHANCE = constants.BASE_RALLY_CHANCE +local BONUS_RALLY_CHANCE = constants.BONUS_RALLY_CHANCE + +local RETREAT_MOVEMENT_PHEROMONE_LEVEL = constants.RETREAT_MOVEMENT_PHEROMONE_LEVEL + -- imported functions +local getChunkByPosition = mapUtils.getChunkByPosition + local processPendingChunks = chunkProcessor.processPendingChunks local processMap = mapProcessor.processMap @@ -27,9 +40,11 @@ local processPlayers = mapProcessor.processPlayers local scanMap = mapProcessor.scanMap local planning = aiPlanning.planning -local removeScout = aiBuilding.removeScout +-- local removeScout = aiBuilding.removeScout -- local scouting = aiBuilding.scouting +local rallyUnits = aiBuilding.rallyUnits + local deathScent = pheromoneUtils.deathScent local regroupSquads = unitGroupUtils.regroupSquads @@ -89,7 +104,7 @@ local function onConfigChanged() regionMap.pI = nil regionMap.pP = nil regionMap.pR = nil - + global.version = constants.VERSION_9 end if (global.version < constants.VERSION_10) then @@ -138,11 +153,23 @@ local function onConfigChanged() global.version = constants.VERSION_12 end + if (global.version <= constants.VERSION_13) then + -- switched over to tick event + regionMap.logicTick = roundToNearest(game.tick + INTERVAL_LOGIC, INTERVAL_LOGIC) + regionMap.processTick = roundToNearest(game.tick + INTERVAL_PROCESS, INTERVAL_PROCESS) + + -- used to rate limit the number of rally cries during a period of time + natives.rallyCries = MAX_RALLY_CRIES + natives.retreats = MAX_RETREATS + + global.version = constants.VERSION_13 + end end local function onTick(event) local tick = event.tick - if (tick % INTERVAL_PROCESS == 0) then + if (tick == regionMap.processTick) then + regionMap.processTick = regionMap.processTick + INTERVAL_PROCESS local surface = game.surfaces[1] local evolutionFactor = game.evolution_factor local players = game.players @@ -150,10 +177,16 @@ local function onTick(event) processPendingChunks(regionMap, surface, pendingChunks) scanMap(regionMap, surface) - if (tick % INTERVAL_LOGIC == 0) then + if (tick == regionMap.logicTick) then + regionMap.logicTick = regionMap.logicTick + INTERVAL_LOGIC + + natives.rallyCries = MAX_RALLY_CRIES + + natives.retreats = MAX_RETREATS + planning(natives, evolutionFactor, tick) - regroupSquads(natives) + regroupSquads(natives, evolutionFactor) processPlayers(players, regionMap, surface, natives, evolutionFactor, tick) @@ -182,20 +215,38 @@ local function onDeath(event) if (entity.force.name == "enemy") then if (entity.type == "unit") then local entityPosition = entity.position - - -- drop death pheromone where unit died - deathScent(regionMap, entityPosition) - - if (event.force ~= nil) and (event.force.name == "player") then - retreatUnits(entityPosition, - convertUnitGroupToSquad(natives, - entity.unit_group), - regionMap, - surface, - natives) + local deathChunk = getChunkByPosition(regionMap, entityPosition.x, entityPosition.y) + + if (deathChunk ~= nil) then + -- drop death pheromone where unit died + deathScent(deathChunk) + + if ((event.force ~= nil) and (event.force.name == "player")) then + local evolutionFactor = game.evolution_factor + + if (deathChunk[MOVEMENT_PHEROMONE] < -(evolutionFactor * RETREAT_MOVEMENT_PHEROMONE_LEVEL)) then + if (natives.retreats >= 0) then + retreatUnits(deathChunk, + convertUnitGroupToSquad(natives, + entity.unit_group), + regionMap, + surface, + natives) + end + local rallyThreshold = BASE_RALLY_CHANCE + (evolutionFactor * BONUS_RALLY_CHANCE) + if (natives.rallyCries >= 0) and (math.random() < rallyThreshold) then + natives.rallyCries = natives.rallyCries - 1 + rallyUnits(deathChunk, + regionMap, + surface, + natives, + evolutionFactor) + end + end + end end - removeScout(entity, global.natives) + -- removeScout(entity, natives) elseif (entity.type == "unit-spawner") then addRemoveEntity(regionMap, entity, natives, false, false) end diff --git a/info.json b/info.json index c549942..3ad4831 100644 --- a/info.json +++ b/info.json @@ -1,7 +1,7 @@ { "name" : "Rampant", "factorio_version" : "0.14", - "version" : "0.14.7", + "version" : "0.14.8", "title" : "Rampant AI", "author" : "Veden", "homepage" : "https://forums.factorio.com/viewtopic.php?f=94&t=31445", diff --git a/libs/AIAttack.lua b/libs/AIAttack.lua index 23f9b52..61746b1 100644 --- a/libs/AIAttack.lua +++ b/libs/AIAttack.lua @@ -7,8 +7,6 @@ local mapUtils = require("MapUtils") local unitGroupUtils = require("UnitGroupUtils") local playerUtils = require("PlayerUtils") local neighborUtils = require("NeighborUtils") -package.path = "../?.lua;" .. package.path -local config = require("config") -- constants @@ -21,12 +19,6 @@ local SQUAD_GUARDING = constants.SQUAD_GUARDING local PLAYER_BASE_GENERATOR = constants.PLAYER_BASE_GENERATOR -local NO_RETREAT_BASE_PERCENT = constants.NO_RETREAT_BASE_PERCENT -local NO_RETREAT_EVOLUTION_BONUS_MAX = constants.NO_RETREAT_EVOLUTION_BONUS_MAX -local NO_RETREAT_SQUAD_SIZE_BONUS_MAX = constants.NO_RETREAT_SQUAD_SIZE_BONUS_MAX - -local CONFIG_ATTACK_WAVE_MAX_SIZE = config.attackWaveMaxSize - -- imported functions local getNeighborChunksWithDirection = mapUtils.getNeighborChunksWithDirection @@ -34,6 +26,7 @@ local getChunkByPosition = mapUtils.getChunkByPosition local canMoveChunkDirection = mapUtils.canMoveChunkDirection local addSquadMovementPenalty = unitGroupUtils.addSquadMovementPenalty local lookupSquadMovementPenalty = unitGroupUtils.lookupSquadMovementPenalty +local calculateKamikazeThreshold = unitGroupUtils.calculateKamikazeThreshold local positionFromDirectionAndChunk = mapUtils.positionFromDirectionAndChunk local euclideanDistanceNamed = mapUtils.euclideanDistanceNamed @@ -42,8 +35,6 @@ local playersWithinProximityToPosition = playerUtils.playersWithinProximityToPos local scoreNeighborsWithDirection = neighborUtils.scoreNeighborsWithDirection -local mLog = math.log10 - -- module code local function validLocation(x, chunk, neighborChunk) @@ -104,7 +95,7 @@ function aiAttack.squadAttack(regionMap, surface, natives) group.set_command(attackCmd) group.start_moving() elseif not squad.frenzy and not squad.rabid and - ((group.state == defines.group_state.attacking_distraction) or (group.state == defines.group_state.attacking_distraction) or + ((group.state == defines.group_state.attacking_distraction) or (group.state == defines.group_state.attacking_target) or (attackChunk[PLAYER_BASE_GENERATOR] ~= 0)) then squad.frenzy = true squad.frenzyPosition.x = squad.group.position.x @@ -122,9 +113,7 @@ function aiAttack.squadBeginAttack(natives, players, evolution_factor) for i=1,#squads do local squad = squads[i] if (squad.status == SQUAD_GUARDING) and squad.group.valid then - local kamikazeThreshold = NO_RETREAT_BASE_PERCENT + (evolution_factor * NO_RETREAT_EVOLUTION_BONUS_MAX) - local squadSizeBonus = mLog((#squad.group.members / CONFIG_ATTACK_WAVE_MAX_SIZE) + 0.1) + 1 - kamikazeThreshold = kamikazeThreshold + (NO_RETREAT_SQUAD_SIZE_BONUS_MAX * squadSizeBonus) + local kamikazeThreshold = calculateKamikazeThreshold(squad, evolution_factor) local playerNearby = playersWithinProximityToPosition(players, squad.group.position, 100) if playerNearby then diff --git a/libs/AIBuilding.lua b/libs/AIBuilding.lua index 63239a2..701f3ac 100644 --- a/libs/AIBuilding.lua +++ b/libs/AIBuilding.lua @@ -33,9 +33,15 @@ local CONFIG_USE_THRESHOLD_MIN = config.attackWaveGenerationThresholdMin local CONFIG_USE_THRESHOLD_MAX = config.attackWaveGenerationThresholdMax local CONFIG_USE_THRESHOLD_RANGE = CONFIG_USE_THRESHOLD_MAX - CONFIG_USE_THRESHOLD_MIN +local RETREAT_MOVEMENT_PHEROMONE_LEVEL = constants.RETREAT_MOVEMENT_PHEROMONE_LEVEL + +local RALLY_CRY_DISTANCE = 3 + -- imported functions local getNeighborChunks = mapUtils.getNeighborChunks +local getChunkByPosition = mapUtils.getChunkByPosition +local getChunkByIndex = mapUtils.getChunkByIndex local scoreNeighbors = neighborUtils.scoreNeighbors local createSquad = unitGroupUtils.createSquad local attackWaveScaling = config.attackWaveScaling @@ -91,7 +97,7 @@ function aiBuilding.makeScouts(surface, natives, chunk, evolution_factor) local enemy = surface.find_nearest_enemy({ position = { x = chunk.pX + HALF_CHUNK_SIZE, y = chunk.pY + HALF_CHUNK_SIZE }, max_distance = 100}) - + if (enemy ~= nil) and enemy.valid and (enemy.type == "unit") then natives.points = natives.points - AI_SCOUT_COST global.natives.scouts[#global.natives.scouts+1] = enemy @@ -117,6 +123,19 @@ function aiBuilding.scouting(regionMap, natives) --]] end +function aiBuilding.rallyUnits(chunk, regionMap, surface, natives, evolutionFactor) + local cX = chunk.cX + local cY = chunk.cY + for x=cX - RALLY_CRY_DISTANCE, cX + RALLY_CRY_DISTANCE do + for y=cY - RALLY_CRY_DISTANCE, cY + RALLY_CRY_DISTANCE do + local rallyChunk = getChunkByIndex(regionMap, x, y) + if (x ~= cX) and (y ~= cY) and (rallyChunk[ENEMY_BASE_GENERATOR] ~= 0) then + aiBuilding.formSquads(regionMap, surface, natives, rallyChunk, evolutionFactor, AI_VENGENCE_SQUAD_COST) + end + end + end +end + function aiBuilding.formSquads(regionMap, surface, natives, chunk, evolution_factor, cost) if (natives.points > cost) and (chunk[ENEMY_BASE_GENERATOR] ~= 0) and (#natives.squads < (AI_MAX_SQUAD_COUNT * evolution_factor)) then local valid = false diff --git a/libs/AIDefense.lua b/libs/AIDefense.lua index 14bd501..c9971c8 100644 --- a/libs/AIDefense.lua +++ b/libs/AIDefense.lua @@ -9,12 +9,12 @@ local neighborUtils = require("NeighborUtils") -- constants +local RETREAT_GRAB_RADIUS = constants.RETREAT_GRAB_RADIUS + local MOVEMENT_PHEROMONE = constants.MOVEMENT_PHEROMONE local PLAYER_PHEROMONE = constants.PLAYER_PHEROMONE local BASE_PHEROMONE = constants.BASE_PHEROMONE -local RETREAT_MOVEMENT_PHEROMONE_LEVEL = constants.RETREAT_MOVEMENT_PHEROMONE_LEVEL - local ENEMY_BASE_GENERATOR = constants.ENEMY_BASE_GENERATOR local HALF_CHUNK_SIZE = constants.HALF_CHUNK_SIZE @@ -25,7 +25,6 @@ local RETREAT_FILTER = constants.RETREAT_FILTER -- imported functions -local getChunkByPosition = mapUtils.getChunkByPosition local getNeighborChunksWithDirection = mapUtils.getNeighborChunksWithDirection local findNearBySquad = unitGroupUtils.findNearBySquad local addSquadMovementPenalty = unitGroupUtils.addSquadMovementPenalty @@ -46,14 +45,14 @@ local function scoreRetreatLocation(position, squad, neighborChunk, surface) return safeScore - dangerScore end -function aiDefense.retreatUnits(position, squad, regionMap, surface, natives) - local chunk = getChunkByPosition(regionMap, position.x, position.y) - if (chunk ~= nil) and (chunk[MOVEMENT_PHEROMONE] < -(game.evolution_factor * RETREAT_MOVEMENT_PHEROMONE_LEVEL)) and (chunk[ENEMY_BASE_GENERATOR] == 0) then +function aiDefense.retreatUnits(chunk, squad, regionMap, surface, natives) + if (chunk[ENEMY_BASE_GENERATOR] == 0) then local performRetreat = false local enemiesToSquad if (squad == nil) then - enemiesToSquad = surface.find_enemy_units(position, 15) + enemiesToSquad = surface.find_enemy_units({x=chunk.pX, + y=chunk.pY}, RETREAT_GRAB_RADIUS) if (#enemiesToSquad > 0) then performRetreat = true end diff --git a/libs/Constants.lua b/libs/Constants.lua index 6423c28..6452ea3 100644 --- a/libs/Constants.lua +++ b/libs/Constants.lua @@ -7,6 +7,7 @@ constants.VERSION_9 = 9 constants.VERSION_10 = 10 constants.VERSION_11 = 11 constants.VERSION_12 = 12 +constants.VERSION_13 = 13 -- misc @@ -28,7 +29,7 @@ constants.INTERVAL_LOGIC = 40 constants.AI_POINT_GENERATOR_AMOUNT = 6 constants.AI_SCOUT_COST = 45 constants.AI_SQUAD_COST = 175 -constants.AI_VENGENCE_SQUAD_COST = 50 +constants.AI_VENGENCE_SQUAD_COST = 45 constants.AI_SETTLER_COST = 75 constants.AI_BASE_BUILDING_COST = 500 constants.AI_TUNNEL_COST = 100 @@ -95,6 +96,20 @@ constants.SQUAD_GUARDING = 2 -- used when squad is idle constants.SQUAD_BURROWING = 3 constants.SQUAD_RAIDING = 4 -- used when player stuff is close +-- Squad Related + +constants.RETREAT_GRAB_RADIUS = 24 + +constants.BASE_RALLY_CHANCE = 0.01 +constants.BONUS_RALLY_CHANCE = 0.01 + +constants.MAX_RETREATS = 7 + +constants.MAX_RALLY_CRIES = 2 +constants.RALLY_CRY_DISTANCE = 3 + +constants.GROUP_MERGE_DISTANCE = 28 + -- player building pheromones constants.BUILDING_PHEROMONES = {} diff --git a/libs/PheromoneUtils.lua b/libs/PheromoneUtils.lua index f4204fd..6119de8 100644 --- a/libs/PheromoneUtils.lua +++ b/libs/PheromoneUtils.lua @@ -30,16 +30,13 @@ local getChunkByPosition = mapUtils.getChunkByPosition local getCardinalChunks = mapUtils.getCardinalChunks -- module code - + function pheromoneUtils.scents(chunk) chunk[BASE_PHEROMONE] = chunk[BASE_PHEROMONE] + chunk[PLAYER_BASE_GENERATOR] - chunk[ENEMY_BASE_GENERATOR] end - -function pheromoneUtils.deathScent(regionMap, position) - local chunk = getChunkByPosition(regionMap, position.x, position.y) - if (chunk ~= nil) then - chunk[MOVEMENT_PHEROMONE] = chunk[MOVEMENT_PHEROMONE] - DEATH_PHEROMONE_GENERATOR_AMOUNT - end + +function pheromoneUtils.deathScent(chunk) + chunk[MOVEMENT_PHEROMONE] = chunk[MOVEMENT_PHEROMONE] - DEATH_PHEROMONE_GENERATOR_AMOUNT end function pheromoneUtils.playerScent(playerChunk) diff --git a/libs/UnitGroupUtils.lua b/libs/UnitGroupUtils.lua index dd3c4f2..96e614b 100644 --- a/libs/UnitGroupUtils.lua +++ b/libs/UnitGroupUtils.lua @@ -4,24 +4,37 @@ local unitGroupUtils = {} local mapUtils = require("MapUtils") local constants = require("Constants") +package.path = "../?.lua;" .. package.path +local config = require("config") -- constants local MOVEMENT_PHEROMONE_GENERATOR_AMOUNT = constants.MOVEMENT_PHEROMONE_GENERATOR_AMOUNT local GROUP_STATE_FINISHED = defines.group_state.finished +local GROUP_STATE_ATTACKING_TARGET = defines.group_state.attacking_target +local GROUP_STATE_ATTACKING_DISTRACTION = defines.group_state.attacking_distraction local SQUAD_RETREATING = constants.SQUAD_RETREATING local SQUAD_GUARDING = constants.SQUAD_GUARDING +local GROUP_MERGE_DISTANCE = constants.GROUP_MERGE_DISTANCE + +local NO_RETREAT_BASE_PERCENT = constants.NO_RETREAT_BASE_PERCENT +local NO_RETREAT_EVOLUTION_BONUS_MAX = constants.NO_RETREAT_EVOLUTION_BONUS_MAX +local NO_RETREAT_SQUAD_SIZE_BONUS_MAX = constants.NO_RETREAT_SQUAD_SIZE_BONUS_MAX + +local CONFIG_ATTACK_WAVE_MAX_SIZE = config.attackWaveMaxSize -- imported functions +local mLog = math.log10 + local tableRemove = table.remove local tableInsert = table.insert local euclideanDistanceNamed = mapUtils.euclideanDistanceNamed -- module code - + function unitGroupUtils.findNearBySquad(natives, position, distance, filter) local squads = natives.squads for i=1,#squads do @@ -118,48 +131,65 @@ function unitGroupUtils.lookupSquadMovementPenalty(squad, chunkX, chunkY) return 0 end -function unitGroupUtils.regroupSquads(natives) +function unitGroupUtils.calculateKamikazeThreshold(squad, evolution_factor) + local kamikazeThreshold = NO_RETREAT_BASE_PERCENT + (evolution_factor * NO_RETREAT_EVOLUTION_BONUS_MAX) + local squadSizeBonus = mLog((#squad.group.members / CONFIG_ATTACK_WAVE_MAX_SIZE) + 0.1) + 1 + return kamikazeThreshold + (NO_RETREAT_SQUAD_SIZE_BONUS_MAX * squadSizeBonus) +end + +local function isAttacking(squad) + return (squad.state == GROUP_STATE_ATTACKING_TARGET) or (squad.state == GROUP_STATE_ATTACKING_DISTRACTION) +end + +function unitGroupUtils.regroupSquads(natives, evolution_factor) local squads = natives.squads for i=1,#squads do local squad = squads[i] - if squad.group.valid then + if squad.group.valid and not isAttacking(squad) then local squadPosition = squad.group.position - for x=i+1, #squads do - local mergeSquad = squads[x] - local mergeGroup = mergeSquad.group - if mergeGroup.valid and (mergeSquad.status == squad.status) and (euclideanDistanceNamed(squadPosition, mergeGroup.position) < 16) then - unitGroupUtils.membersToSquad(squad, mergeGroup.members, true) + local mergedSquads = false + for x=i+1, #squads do + local mergeSquad = squads[x] + local mergeGroup = mergeSquad.group + if mergeGroup.valid and (mergeSquad.status == squad.status) and not isAttacking(mergeSquad) and (euclideanDistanceNamed(squadPosition, mergeGroup.position) < GROUP_MERGE_DISTANCE) then + unitGroupUtils.membersToSquad(squad, mergeGroup.members, true) if mergeSquad.kamikaze then squad.kamikaze = true end + mergedSquads = true mergeGroup.destroy() - end - end - end + end + end + if mergedSquads and not squad.kamikaze then + local kamikazeThreshold = unitGroupUtils.calculateKamikazeThreshold(squad, evolution_factor) + if (math.random() < kamikazeThreshold) then + squad.kamikaze = true + end + end + end end - + for i=#squads,1,-1 do - local squad = squads[i] - if (squad.group == nil) then - tableRemove(squads, i) - elseif not squad.group.valid then - tableRemove(squads, i) - elseif (#squad.group.members == 0) then - squad.group.destroy() - tableRemove(squads, i) - else - if (squad.status == SQUAD_RETREATING) and (squad.cycles == 0) then - squad.status = SQUAD_GUARDING + local squad = squads[i] + if (squad.group == nil) then + tableRemove(squads, i) + elseif not squad.group.valid then + tableRemove(squads, i) + elseif (#squad.group.members == 0) then + squad.group.destroy() + tableRemove(squads, i) + else + if (squad.status == SQUAD_RETREATING) and (squad.cycles == 0) then + squad.status = SQUAD_GUARDING squad.frenzy = true squad.frenzyPosition.x = squad.group.position.x squad.frenzyPosition.y = squad.group.position.y elseif (squad.group.state == GROUP_STATE_FINISHED) then squad.status = SQUAD_GUARDING - elseif (squad.cycles > 0) then - squad.cycles = squad.cycles - 1 - end - - end + elseif (squad.cycles > 0) then + squad.cycles = squad.cycles - 1 + end + end end end diff --git a/make.rkt b/make.rkt index b2bf772..239a1e4 100644 --- a/make.rkt +++ b/make.rkt @@ -73,7 +73,7 @@ (copyDirectory "graphics" modFolder) (copyDirectory "prototypes" modFolder))) - ;; (copyFiles modFolder) + ;;(copyFiles modFolder) ;; (copyFiles zipModFolder) (makeZip modFolder) ;;(makeZip zipModFolder)