From b1f3cda7f74c29569375f7e65576344ff10c8936 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 7 May 2024 14:33:49 +0200 Subject: [PATCH] #STRATEGO - Smarter route finding --- .../Moose/Functional/Stratego.lua | 174 +++++++++++++++--- 1 file changed, 150 insertions(+), 24 deletions(-) diff --git a/Moose Development/Moose/Functional/Stratego.lua b/Moose Development/Moose/Functional/Stratego.lua index 3eff164af..74caf5cc7 100644 --- a/Moose Development/Moose/Functional/Stratego.lua +++ b/Moose Development/Moose/Functional/Stratego.lua @@ -15,7 +15,7 @@ -- -- @module Functional.Stratego -- @image Functional.Stratego.png --- Last Update April 2024 +-- Last Update May 2024 --- @@ -181,7 +181,7 @@ STRATEGO = { debug = false, drawzone = false, markzone = false, - version = "0.2.8", + version = "0.2.9", portweight = 3, POIweight = 1, maxrunways = 3, @@ -212,9 +212,10 @@ STRATEGO = { -- @field #number coalition -- @field #boolean port -- @field Core.Zone#ZONE_RADIUS zone, --- @field Core.Point#COORDINATRE coord +-- @field Core.Point#COORDINATE coord -- @field #string type -- @field Ops.OpsZone#OPSZONE opszone +-- @field #number connections --- -- @type STRATEGO.DistData @@ -490,6 +491,7 @@ function STRATEGO:AnalyseBases() coord = coord, type = abtype, opszone = opszone, + connections = 0, } airbasetable[abname] = tbl nonconnectedab[abname] = true @@ -576,10 +578,12 @@ function STRATEGO:AnalysePOIs(Set,Weight,Key) coord = coord, type = Key, opszone = opszone, + connections = 0, } - airbasetable[zone:GetName()] = tbl - nonconnectedab[zone:GetName()] = true + airbasetable[zname] = tbl + nonconnectedab[zname] = true local name = string.gsub(zname,"[%p%s]",".") + --self:I({name=name,zone=zname}) easynames[name]=zname end ) @@ -598,6 +602,30 @@ function STRATEGO:GetToFrom(StartPoint,EndPoint) return fromto, tofrom end +--- [USER] Get available connecting nodes from one start node +-- @param #STRATEGO self +-- @param #string StartPoint The starting name +-- @return #boolean found +-- @return #table Nodes +function STRATEGO:GetRoutesFromNode(StartPoint) + self:T(self.lid.."GetRoutesFromNode") + local pstart = string.gsub(StartPoint,"[%p%s]",".") + local found = false + pstart=pstart..";" + local routes = {} + local listed = {} + for _,_data in pairs(self.routexists) do + if string.find(_data,pstart,1,true) and not listed[_data] then + local target = string.gsub(_data,pstart,"") + local fname = self.easynames[target] + table.insert(routes,fname) + found = true + listed[_data] = true + end + end + return found,routes +end + --- [USER] Manually add a route, for e.g. Island hopping or to connect isolated networks. Use **after** STRATEGO has been started! -- @param #STRATEGO self -- @param #string Startpoint Starting Point, e.g. AIRBASE.Syria.Hatay @@ -630,6 +658,8 @@ function STRATEGO:AddRoutesManually(Startpoint,Endpoint,Color,Linetype,Draw) local factor = self.airbasetable[Startpoint].baseweight*self.routefactor self.airbasetable[Startpoint].weight = self.airbasetable[Startpoint].weight+factor self.airbasetable[Endpoint].weight = self.airbasetable[Endpoint].weight+factor + self.airbasetable[Endpoint].connections = self.airbasetable[Endpoint].connections + 2 + self.airbasetable[Startpoint].connections = self.airbasetable[Startpoint].connections+2 if self.debug or Draw then startcoordinate:LineToAll(targetcoordinate,-1,color,1,linetype,nil,string.format("%dkm",dist)) end @@ -648,7 +678,7 @@ function STRATEGO:AnalyseRoutes(tgtrwys,factor,color,linetype) for _,_data in pairs(self.airbasetable) do local fromto,tofrom = self:GetToFrom(startpoint,_data.name) if _data.name == startpoint then - -- sam as we + -- same as we elseif _data.baseweight == tgtrwys and not (self.routexists[fromto] or self.routexists[tofrom]) then local tgtc = _data.coord local dist = UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000 @@ -670,6 +700,8 @@ function STRATEGO:AnalyseRoutes(tgtrwys,factor,color,linetype) self.nonconnectedab[startpoint] = false self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+factor self.airbasetable[_data.name].weight = self.airbasetable[_data.name].weight+factor + self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections + 1 + self.airbasetable[_data.name].connections = self.airbasetable[_data.name].connections + 1 if self.debug then startcoord:LineToAll(tgtc,-1,color,1,linetype,nil,string.format("%dkm",dist)) end @@ -716,6 +748,8 @@ function STRATEGO:AnalyseUnconnected(Color) end self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+1 self.airbasetable[closest].weight = self.airbasetable[closest].weight+1 + self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections+2 + self.airbasetable[closest].connections = self.airbasetable[closest].connections+2 local data = { start = startpoint, target = closest, @@ -732,6 +766,38 @@ function STRATEGO:AnalyseUnconnected(Color) return self end +--[[ +function STRATEGO:PruneDeadEnds(abtable) + local found = false + local newtable = {} + for name, _data in pairs(abtable) do + local data = _data -- #STRATEGO.Data + if data.connections > 2 then + newtable[name] = data + else + -- dead end + found = true + local neighbors, nearest, distance = self:FindNeighborNodes(name) + --self:I("Pruning "..name) + if nearest then + for _name,_ in pairs(neighbors) do + local abname = self.easynames[_name] or _name + --self:I({easyname=_name,airbasename=abname}) + if abtable[abname] then + abtable[abname].connections = abtable[abname].connections -1 + end + end + end + if self.debug then + data.coord:CircleToAll(5000,-1,{1,1,1},1,{1,1,1},1,3,true,"Dead End") + end + end + end + abtable = nil + return found,newtable +end +--]] + --- [USER] Get a list of the nodes with the highest weight. -- @param #STRATEGO self -- @param #number Coalition (Optional) Find for this coalition only. E.g. coalition.side.BLUE. @@ -1141,29 +1207,34 @@ function STRATEGO:FindNeighborNodes(Name,Enemies,Friends) self:T(self.lid.."FindNeighborNodes") local neighbors = {} local name = string.gsub(Name,"[%p%s]",".") + --self:I({Name=Name,name=name}) local shortestdist = 1000*1000 local nearest = nil for _route,_data in pairs(self.disttable) do if string.find(_route,name,1,true) then local dist = self.disttable[_route] -- #STRATEGO.DistData + --self:I({route=_route,name=name}) local tname = string.gsub(_route,name,"") local tname = string.gsub(tname,";","") + --self:I({tname=tname,cname=self.easynames[tname]}) local cname = self.easynames[tname] -- name of target - local encoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE - if Enemies == true then - if self.airbasetable[cname].coalition == encoa then - neighbors[cname] = dist + if cname then + local encoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE + if Enemies == true then + if self.airbasetable[cname].coalition == encoa then + neighbors[cname] = dist + end + elseif Friends == true then + if self.airbasetable[cname].coalition ~= encoa then + neighbors[cname] = dist + end + else + neighbors[cname] = dist end - elseif Friends == true then - if self.airbasetable[cname].coalition ~= encoa then - neighbors[cname] = dist + if neighbors[cname] and dist.dist < shortestdist then + shortestdist = dist.dist + nearest = cname end - else - neighbors[cname] = dist - end - if neighbors[cname] and dist.dist < shortestdist then - shortestdist = dist.dist - nearest = cname end end end @@ -1180,13 +1251,16 @@ end -- @param #number LineType (Optional) Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 6. -- @return #table Route Table of #string name entries of the route -- @return #boolean Complete If true, the route was found end-to-end. +-- @return #boolean Reverse If true, the route was found with a reverse search, the route table will be from sorted from end point to start point. function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType) self:T(self.lid.."FindRoute") --self:I({Start,End,Hops}) --local bases = UTILS.DeepCopy(self.airbasetable) - local Route = {} + local Route = {} + local InRoute = {} local hops = Hops or 4 local routecomplete = false + local reverse = false local function Checker(neighbors) for _name,_data in pairs(neighbors) do @@ -1200,13 +1274,16 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType) local function NextClosest(Start,End) local ecoord = self.airbasetable[End].coord - local nodes = self:FindNeighborNodes(Start) + local nodes,nearest = self:FindNeighborNodes(Start) + --self:I(tostring(nearest)) local closest = nil local closedist = 1000*1000 for _name,_dist in pairs(nodes) do local kcoord = self.airbasetable[_name].coord + local nnodes = self.airbasetable[_name].connections > 2 and true or false + if _name == End then nnodes = true end local dist = math.floor((kcoord:Get2DDistance(ecoord)/1000)+0.5) - if dist < closedist then + if (dist < closedist and nnodes and InRoute[_name] ~= true) then closedist = dist closest = _name end @@ -1231,6 +1308,7 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType) -- One hop Route[#Route+1] = Start + InRoute[Start] = true local nodes = self:FindNeighborNodes(Start) local endpoint = Checker(nodes) @@ -1240,9 +1318,11 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType) else local spoint = Start for i=1,hops do + --self:I("Start="..tostring(spoint)) local Next = NextClosest(spoint,End) if Next then Route[#Route+1] = Next + InRoute[Next] = true local nodes = self:FindNeighborNodes(Next) local endpoint = Checker(nodes) if endpoint then @@ -1255,8 +1335,54 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType) end end end - if (self.debug or Draw) then DrawRoute(Route) end - return Route, routecomplete + + -- optimize route + local function OptimizeRoute(Route) + local foundcut = false + local largestcut = 0 + local cut = {} + for i=1,#Route do + --self:I({Start=Route[i]}) + local found,nodes = self:GetRoutesFromNode(Route[i]) + for _,_name in pairs(nodes or {}) do + for j=i+2,#Route do + if _name == Route[j] then + --self:I({"Shortcut",Route[i],Route[j]}) + if j-i > largestcut then + largestcut = j-i + cut = {i=i,j=j} + foundcut = true + end + end + end + end + end + if foundcut then + local newroute = {} + for i=1,#Route do + if i<= cut.i or i>=cut.j then + table.insert(newroute,Route[i]) + end + end + return newroute + end + return Route, foundcut + end + + if routecomplete == true then + local foundcut = true + while foundcut ~= false do + Route, foundcut = OptimizeRoute(Route) + end + else + -- reverse search + Route, routecomplete = self:FindRoute(End,Start,Hops,Draw,Color,LineType) + reverse = true + end + + if (self.debug or Draw) then DrawRoute(Route) end + + return Route, routecomplete, reverse end --- [USER] Add budget points.