tillmas / dcsgroundai

A set of scripts to allow ground forces to operate cohesively in DCS
GNU General Public License v3.0
0 stars 1 forks source link

assignMission function problems #3

Closed tillmas closed 1 year ago

tillmas commented 1 year ago

I am trying to get this function to take the inputs, sort the unfriendly target zones by distance from the reference unit, and then push out the n closest. It is throwing an error right now, and I am having a hard time troubleshooting it. I think that there must be a simpler way to do it. Perhaps splitting the function up into attack and defend.

function assignMission(TargetZones,UnfriendlyZones,FriendlyZones,ReferenceUnits,numAttack,numDefend)

--function will give a commander an attack order and a defend order
--Unfriendly zones should be the array of unfriendly zones
--friendly zones should be an array of the friendly zones
--ReferenceUnit will be where the algorithm starts looking from
--will return two tables, the attack zone and the defend zone

    defZone = {}
    attZone = {}
    local targetFriendly = {}
    local targetUnfriendly = {}

-- this will put all of the target zones into either friendly or unfriendly
for i=1,table.getn(TargetZones) do
    if table.contains(UnfriendlyZones,TargetZones[i]) == true then
        table.insert(targetUnfriendly,TargetZones[i])
    else
        table.insert(targetFriendly,TargetZones[i])
    end
end
--if the number of attacks is more than the number of unfriendly target zones, just return all of them

if table.getn(targetUnfriendly) <= numAttack then
    attZone = targetUnfriendly

else
    -- build a table with the distances between each zone and the reference
    local distances = {}

    for j = 1,table.getn(targetUnfriendly) do
        local testCoords = mist.getRandomPointInZone(targetUnfriendly[j],0)
        local refCoords = mist.getAvgPos(ReferenceUnits)

        local tempDistance = mist.utils.get2DDist(refCoords, testCoords)
        distances[targetUnfriendly[j]] = tempDistance
    end

    --sort the unfriendly zones by distance
    table.sort(distances)

    --invert the table to get the sorted index (zone names)
    table.invert(distances)

    for k = 1,numAttack do

        table.insert(attZone,distances[i])
    end
end

if table.getn(targetFriendly) <= numDefend then
    defZone = targetFriendly

else
    -- build a table with the distances between each zone and the reference
    local distances = {}

    for j = 1,table.getn(targetFriendly) do
        local testCoords = mist.getRandomPointInZone(targetFriendly[j],0)
        local refCoords = mist.getAvgPos(ReferenceUnit)

        local tempDistance = mist.utils.get2DDist(refCoords, testCoords)
        distances[targetFriendly[j]] = tempDistance
    end

    --sort the unfriendly zones by distance
    table.sort(distances)

    --invert the table to get the sorted index (zone names)
    table.invert(distances)

    for k = 1,numDefend do

        table.insert(defZone,distances[i])
    end
end

return attZone, defZone

end

agridenour commented 1 year ago
  1. I think the bug you're seeing is caused by table.sort(distances). distances appears to be an associative array (of the form { [non-sequential integer key]: value }), so the sort function doesn't know what it's sorting.

Instead the distances array should contain pairs of the form [zoneName, distanceToReferenceUnit], then use the sorting function

function(a, b)
   return a[2] < b[2]
end

For a complete example:

local zoneList = {}
local output = {}

for _, z in pairs(mist.DBs.zonesByName) do
    zoneList[#zoneList+1] = z
end

local distances = {}
for i = 1, #zoneList do

    local zoneCoords = mist.getRandomPointInZone(zoneList[i].name, 0)

    local bluePlatoons = {"[g]C1-1", "[g]C2-1"}

    local refCoords = mist.getAvgPos(mist.makeUnitTable(bluePlatoons))

    distances[i] = {zoneList[i].name, mist.utils.get2DDist(refCoords, zoneCoords)}
end

table.sort(distances, function(a, b) return a[2] < b[2] end)

for i = 1, 3 do
   output[i] = distances[i][1] 
end

return output

-- output
--[
--  "1-7",
--  "1-5",
--  "1-10"
--]
  1. I also think the assignMissions function should only allocate unit groups to zones. A separate function can use assignMission to determine if it's an attack mission or a defend mission. By separating that out, we have more control over what specific orders are given for each mission (even if it's just "units go here" as a start).

I'll work a bit on structuring that, but basically I think it works out to be something like:

-- returns [numZones] zones from a list of [targetZones] that are closes to a specified [referencePoint] 
function assignMission(targetZones, referencePoint, numZones)

which we can call with the structure...

-- determine zone states
local attackZones = {}
local defenseZones = {}

-- something that sorts out zone control states

-- for each battalion commander...
--    assign attack missions
local attackMissions = assignMission(attackZones, referencePoint, numAttackZones)

--    assign defense missions
local defenseMissions = assignMission(defenseZones, referencePoint, numDefenseZones)

Then the commander logic can work out force allocation for attack and defense separately

tillmas commented 1 year ago

Am I correct that this bit of code is simply creating a list of all of the zones specified in the mission editor? Likely the end of this is the right place to pick the arbitrary number of zones over which to fight.

local zoneList = {}
local output = {}

for _, z in pairs(mist.DBs.zonesByName) do
    zoneList[#zoneList+1] = z
end

I wonder if splitting the function at this point makes sense. We are going to run everything above this once per mission load, and everything below this once per commander.

agridenour commented 1 year ago

Yes, correct. The example I provided needs to be organized. Putting my two examples together results in something like what's shown below. This may not be the correct logic, but it should be (close to) the right overall structure.

-- 1. Zone Control
-- this could be done a little easier/simpler with the included zone handling in MIST
local zonePrefix = "1-" -- string prefix used to select zones 
local numTargetZones = 10 -- set by the mission designer
local zoneList = {} -- list of all zones the script might select as a target
local targetZones = {} -- list of zones selected as targets

--  1a. Get all zones with specified prefix in name
local zoneList = getAllZonesWithPrefix(zonePrefix)

--  1b. Choose zones of interest for the mission
local targetZones = getTargetZones(zoneList, numTargetZones)

-- determine zone states
local attackZones = {}
local defenseZones = {}

-- something that sorts out zones by control status
for z in targetZones do
   if z.controlledBy == "blue" then
       defenseZones[#defenseZones+1] = z.name
   else
       attackZones[#attackZones +1] = z.name
   end
end

-- for each battalion commander...
--    assign attack missions
local attackMissions = assignMission(attackZones, referencePoint, numAttackZones)

--    assign defense missions
local defenseMissions = assignMission(defenseZones, referencePoint, numDefenseZones)
tillmas commented 1 year ago

I'm going to close this issue because it has really split into two non-related things. For now, I replaced the zoneList section of the code with your much better code. I totally agree with the first half of your design above, I hadn't thought of the need for a prefix or label on the fighting zones to separate them from normal trigger zones - that is a great catch.