mirror of
https://gitlab.com/the-no-frauds-club/cosmonarchy-bw-prerelease.git
synced 2026-09-17 01:01:12 +00:00
236 lines
8.0 KiB
Lua
236 lines
8.0 KiB
Lua
require("melee.melee_common")
|
|
require("melee.personalities")
|
|
|
|
---@class AIPersonality
|
|
---@field name string
|
|
---@field unit_composition table<UnitId, number>
|
|
---@field main_build_order fun(melee_ai: MeleeAI, town: Town)[]
|
|
---@field expansion_build_order fun(melee_ai: MeleeAI, town: Town)[]
|
|
---@field build_anti_ground_defenses fun(melee_ai: MeleeAI, town: Town)
|
|
---@field build_anti_air_defenses fun(melee_ai: MeleeAI, town: Town)
|
|
---@field expand_rate? number Default is 3 minutes 30 seconds
|
|
---@field expand_wait_after_fail? number Default is 30 seconds
|
|
---@field attack_wave_budget_scaling? number Default is 1.0
|
|
---@field attack_wave_cost_bias? number Default derived from AI:new_attack()
|
|
|
|
---@class TownState
|
|
---@field started_anti_ground_defenses boolean
|
|
---@field started_anti_air_defenses boolean
|
|
|
|
---@class MeleeAI
|
|
---@field debugging {enabled: boolean, last_tick: number, last_script_id: number}
|
|
---@field player Player
|
|
---@field personality AIPersonality
|
|
---@field ai AI
|
|
---@field main_town Town
|
|
---@field towns table<Town, TownState>
|
|
---@field started_expanding boolean
|
|
---@field started_attacking boolean
|
|
---@field seen_air_threats boolean
|
|
MeleeAI = {}
|
|
MeleeAI.__index = MeleeAI
|
|
|
|
setmetatable(MeleeAI, {
|
|
__call = function(cls, ...)
|
|
return cls.new(...)
|
|
end,
|
|
})
|
|
|
|
---@param config? table
|
|
---@param player? Player
|
|
function MeleeAI.new(config, player)
|
|
local self = setmetatable({}, MeleeAI)
|
|
self.debugging = {
|
|
enabled = PlayerVars(player).ai_debug or false,
|
|
last_tick = -1,
|
|
last_script_id = -1,
|
|
}
|
|
self.player = player or Player()
|
|
self.personality = config or Personalities.pick_random(player)
|
|
self.ai = AI(self.player.id)
|
|
self.main_town = self.ai:start_town()
|
|
self.towns = {
|
|
[self.main_town] = {
|
|
started_anti_ground_defenses = false,
|
|
started_anti_air_defenses = false,
|
|
},
|
|
}
|
|
self.started_expanding = false
|
|
self.started_attacking = false
|
|
self.seen_air_threats = true -- TODO: detect dynamically or after timeout
|
|
-- TODO: maybe have a dynamic unit loss threshold check that triggers a stronger defense response
|
|
|
|
self:cdebug(TextColor.Yellow3, "Starting AI with personality: " .. self.personality.name)
|
|
|
|
return self
|
|
end
|
|
|
|
function MeleeAI:debug_header()
|
|
if self.debugging.last_tick ~= Time.elapsed or self.debugging.last_script_id ~= Script.id then
|
|
self.debugging.last_tick = Time.elapsed
|
|
self.debugging.last_script_id = Script.id
|
|
print()
|
|
cprint(TextColor.Yellow3, "-- " .. tostring(self.player.name) .. " (P" .. tostring(self.player.id) .. "):\n")
|
|
end
|
|
end
|
|
|
|
function MeleeAI:debug(...)
|
|
if self.debugging.enabled then
|
|
self:debug_header()
|
|
print(...)
|
|
end
|
|
end
|
|
|
|
function MeleeAI:cdebug(color, ...)
|
|
if self.debugging.enabled then
|
|
self:debug_header()
|
|
cprint(color, ...)
|
|
end
|
|
end
|
|
|
|
-- How often the banked-resource snapshot is refreshed while a dynamic_wait is
|
|
-- in progress, so a long wait reacts to resources changing mid-wait instead
|
|
-- of freezing the scale from a single sample taken at the start.
|
|
local DYNAMIC_WAIT_RESAMPLE_INTERVAL = secs(2)
|
|
|
|
---@param duration integer
|
|
function MeleeAI:dynamic_wait(duration)
|
|
local elapsed = 0
|
|
while elapsed < duration do
|
|
local chunk = math.min(DYNAMIC_WAIT_RESAMPLE_INTERVAL, duration - elapsed)
|
|
local resources = self.player.minerals + self.player.gas
|
|
local resource_scale = math.max(0, math.min(1, (1100 - resources) / 600))
|
|
local scaled_chunk = math.floor(chunk * resource_scale)
|
|
if scaled_chunk > 0 then
|
|
wait(scaled_chunk)
|
|
end
|
|
elapsed = elapsed + chunk
|
|
end
|
|
end
|
|
|
|
---@protected
|
|
---@return table<UnitId, number>
|
|
function MeleeAI:get_final_unit_composition()
|
|
local unit_composition = {}
|
|
for unit_id, preference in pairs(self.personality.unit_composition) do
|
|
unit_composition[unit_id] = preference
|
|
end
|
|
return unit_composition
|
|
end
|
|
|
|
function MeleeAI:run()
|
|
for unit_id, preference in pairs(self:get_final_unit_composition()) do
|
|
if preference > 0 then
|
|
multirun(function()
|
|
wait_until(function() return self.player:can_make(unit_id) end, secs(1))
|
|
self:cdebug(TextColor.Teal, "Added " .. UnitId.name(unit_id) .. " to defense")
|
|
self.ai.defense:auto_set(unit_id)
|
|
wait_while(function() return self.player:can_make(unit_id) end, secs(1))
|
|
self:cdebug(TextColor.Teal, "Added " .. UnitId.name(unit_id) .. " to defense")
|
|
self.ai.defense:auto_remove(unit_id)
|
|
end)
|
|
end
|
|
end
|
|
|
|
multirun(function()
|
|
self.main_town:auto_kill_script()
|
|
self.main_town:build(1, MeleeCommon.default_town_building(self.player.race), 120)
|
|
self.main_town:build(self.main_town.max_workers, MeleeCommon.default_worker(self.player.race), 100)
|
|
for i, build_order_step in ipairs(self.personality.main_build_order) do
|
|
build_order_step(self, self.main_town)
|
|
if i == 1 then
|
|
-- If opener didn't enable this, we now should
|
|
self:start_attacking()
|
|
self:start_expanding()
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
function MeleeAI:start_expanding()
|
|
if self.started_expanding then
|
|
return
|
|
end
|
|
self.started_expanding = true
|
|
multirun(function()
|
|
while true do
|
|
local town = self.ai:expand()
|
|
self:cdebug(TextColor.Green, "Expanding:")
|
|
self:debug(town)
|
|
if town.is_alive then
|
|
self.towns[town] = {
|
|
started_anti_ground_defenses = false,
|
|
started_anti_air_defenses = false,
|
|
}
|
|
town:multirun(function()
|
|
for i, build_order_step in ipairs(self.personality.expansion_build_order) do
|
|
build_order_step(self, town)
|
|
if i == 1 then
|
|
self:start_anti_ground_defenses(town)
|
|
self:start_anti_air_defenses(town)
|
|
end
|
|
end
|
|
end)
|
|
wait(self.personality.expand_rate or (mins(3) + secs(30)))
|
|
else
|
|
wait(self.personality.expand_wait_after_fail or secs(30))
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
function MeleeAI:start_attacking()
|
|
if self.started_attacking then
|
|
return
|
|
end
|
|
self.started_attacking = true
|
|
multirun(function()
|
|
while true do
|
|
local budget = self.player:unit_count(MeleeCommon.default_worker(self.player.race)) * 50
|
|
+ (self.player.minerals + self.player.gas) / 2
|
|
budget = budget * (self.personality.attack_wave_budget_scaling or 1.0)
|
|
|
|
local attack = self.ai:new_attack(budget, self.personality.attack_wave_cost_bias or nil)
|
|
for unit_id, preference in pairs(self:get_final_unit_composition()) do
|
|
if preference > 0 and self.player:can_make(unit_id) then
|
|
attack:compose(preference, unit_id)
|
|
end
|
|
end
|
|
|
|
self:cdebug(TextColor.Orange, "Sending attack:")
|
|
self:debug(attack)
|
|
attack:send()
|
|
wait(secs(5))
|
|
end
|
|
end)
|
|
end
|
|
|
|
function MeleeAI:start_anti_ground_defenses(town)
|
|
if self.personality.build_anti_ground_defenses == nil then
|
|
return
|
|
end
|
|
if self.towns[town].started_anti_ground_defenses then
|
|
return
|
|
end
|
|
self.towns[town].started_anti_ground_defenses = true
|
|
|
|
multirun(function()
|
|
self.personality.build_anti_ground_defenses(self, town)
|
|
end)
|
|
end
|
|
|
|
function MeleeAI:start_anti_air_defenses(town)
|
|
if self.personality.build_anti_air_defenses == nil then
|
|
return
|
|
end
|
|
if self.towns[town].started_anti_air_defenses then
|
|
return
|
|
end
|
|
self.towns[town].started_anti_air_defenses = true
|
|
|
|
multirun(function()
|
|
wait_until(function() return self.seen_air_threats end, secs(1))
|
|
self.personality.build_anti_air_defenses(self, town)
|
|
end)
|
|
end
|