Advertisement
Akuukis

Nav Library

May 4th, 2014
680
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 35.91 KB | None | 0 0
  1. local robot = require("robot")
  2. local computer = require("computer")
  3. local component = require("component") -- TODO remove
  4. local clsNav = {}
  5. clsNav.title = "Navigation Library"
  6. clsNav.shortTitle = "nav"
  7. clsNav.version = 0.9000
  8. function clsNav:new(oldObject)
  9.   local object
  10.   if oldObject
  11.     and type(oldObject.title) == "string"
  12.     and oldObject.title == self.title
  13.     and type(oldObject.title) == "number"
  14.     and oldObject.version <= self.version
  15.   then
  16.     object = oldObject
  17.   else
  18.     object = {}
  19.   end
  20.   setmetatable(object, self)
  21.   self.__index = self
  22.   return object
  23. end
  24.  
  25. ---------------- Dependencies -------------------------------------------------
  26. local logger = logger or {}
  27. if not logger.fatal then
  28.   logger.fatal = function (...) io.write(string.format(...)) end
  29. end
  30. if not logger.warning then
  31.     logger.warning = function (...) io.write(string.format(...)) end
  32. end
  33. if not logger.info then
  34.     logger.info = function (...) io.write(string.format(...)) end
  35. end
  36. if not logger.spam then
  37.     --local fs = require("filesystem")
  38.     --fs.makeDirectory("logs")
  39.     logger.spam = function (...)
  40.     --  local file = fs.open("logs/test.log","a")
  41.     --  file:write(string.format(...))
  42.     --  file:close()
  43.       io.write(string.format(...))
  44.     end
  45. end
  46. local utils = utils or {}
  47. if not utils.deepCopy then
  48.   function utils.deepCopy(t)
  49.     -- Credits to MihailJP @ https://gist.github.com/MihailJP/3931841
  50.     if type(t) ~= "table" then return t end
  51.     --local meta = getmetatable(t)
  52.     local target = {}
  53.     for k, v in pairs(t) do
  54.       if type(v) == "table" then
  55.         target[k] = utils.deepCopy(v)
  56.       else
  57.         target[k] = v
  58.       end
  59.     end
  60.     --setmetatable(target, meta)
  61.     return target
  62.   end
  63. end
  64. if not utils.deepMerge then
  65.   function utils.deepMerge(t1, t2) -- merges t1 over t2
  66.     if type(t1) ~= "table" then return t1 end
  67.     --local meta1 = getmetatable(t1)
  68.     local target = utils.deepCopy(t2)
  69.     for k, v in pairs(t1) do
  70.         if (type(v) == "table") and (type(target[k] or false) == "table") then
  71.             utils.deepMerge(target[k], t1[k])
  72.         else
  73.             target[k] = v
  74.         end
  75.     end
  76.     --setmetatable(target, meta1)
  77.     return target
  78.   end
  79. end
  80. if not utils.freeMemory then
  81.   function utils.freeMemory()
  82.     local result = 0
  83.     for i = 1, 10 do
  84.       result = math.max(result, computer.freeMemory())
  85.       os.sleep(0)
  86.     end
  87.     return result
  88.   end
  89. end
  90. local thread = thread or {}
  91. if not thread.yield then thread.yield = function() os.sleep() end end
  92.  
  93. ---------------- Local variables ----------------------------------------------
  94. local mapGrid = {
  95.   {"A","B","C","D","E",},
  96.   {"F","G","H","I","J",},
  97.   {"K","L","M","N","O",},
  98.   {"P","Q","R","S","T",},
  99.   {"U","V","W","X","Y",},
  100. }
  101. local mapMaxDepth = 1
  102.  
  103. ---------------- Local functions ----------------------------------------------
  104. local function checkOptions(unknowns, ...)
  105.     local wishlist = {...}
  106.     if type(unknowns) ~= "table" then unknowns = {unknowns} end
  107.   for _,wish in pairs(wishlist) do
  108.     if type(wish) == "string" then
  109.       for _,unknown in pairs(unknowns) do
  110.         if type(unknown) == "string" and wish == unknown then return unknown end
  111.       end
  112.     end
  113.   end
  114.   return false
  115. end
  116. local function checkPos(unknown)
  117.   if type(unknown) ~= "table" then return false end
  118.   if
  119.     type(unknown.x) == "number"
  120.     and type(unknown.y) == "number"
  121.     and type(unknown.z) == "number"
  122.   then
  123.     return {
  124.       x = unknown.x,
  125.       y = unknown.y,
  126.       z = unknown.z,
  127.       f = (type(unknown.f) == "number" and unknown.f) or nil,
  128.       weight = (type(unknown.weight) == "number" and unknown.f) or nil,
  129.     }
  130.   elseif
  131.     type(unknown[1]) == "number"
  132.     and type(unknown[2]) == "number"
  133.     and type(unknown[3]) == "number"
  134.   then
  135.     return {
  136.       x = unknown[1],
  137.       y = unknown[2],
  138.       z = unknown[3],
  139.       f = (type(unknown[4]) == "number" and unknown[4]) or nil,
  140.       weight = (type(unknown[5]) == "number" and unknown[5]) or nil,
  141.     }
  142.   end
  143.   return false
  144. end
  145. local function checkPositions(unknown)
  146.   if type(unknown) ~= "table" then return false end
  147.   local positions = {}
  148.     if checkPos(unknown) then positions[#positions+1] = checkPos(unknown) end
  149.   for k,v in pairs(unknown) do
  150.     local pos = checkPos(v)
  151.     if pos then
  152.       positions[#positions+1] = pos
  153.     end
  154.   end
  155.   if #positions > 0 then
  156.     return positions
  157.   else
  158.     return false
  159.   end
  160. end
  161.  
  162. ---------------- Object variables ---------------------------------------------
  163. clsNav.pos = {
  164.     x = 0, -- North
  165.     z = 0, -- East
  166.     y = 0, -- Height
  167.     f = 0, -- Facing direction, modulus of 4 // 0,1,2,3 = North, East, South, West
  168. }
  169. clsNav.map = {
  170.   _initialized = os.time(),
  171.     _updated = os.time(),
  172.   _maxDepth = 1,
  173. }
  174.  
  175. ---------------- Methods ------------------------------------------------------
  176. function clsNav:getVersion()
  177.     return self.version
  178.     end
  179. function clsNav:getMapFromPos(pos)
  180.   pos = checkPos(pos)
  181.   if not pos then return false end
  182.   local chunkName = ""
  183.   local chunkX = (pos.x-pos.x%16)/16
  184.   local chunkY = -(pos.y-pos.y%16)/16 -- invert so that (-1,-1) is at bottom left, NOT up left
  185.   local power = 1
  186.   while chunkX > 0 or chunkY > 0 or self.map._maxDepth > #chunkName do
  187.     local gridX = (chunkX+2)%5-2
  188.     local gridY = (chunkY+2)%5-2
  189.     chunkX = (chunkX+2 - (chunkX+2)%5)/5
  190.     chunkY = (chunkY+2 - (chunkY+2)%5)/5
  191.     chunkName = mapGrid[gridY+3][gridX+3]..chunkName
  192.   end
  193.   self.map._maxDepth = math.max(self.map._maxDepth, #chunkName)
  194.   local posNo = 10000*pos.z + 100*pos.x + pos.y
  195.   return chunkName, posNo
  196. end
  197. function clsNav:putMap(pos, value)
  198.   if type(value) ~= "table" then return false end -- to delete do putMap(pos,{})
  199.   local chunkName, posNo = self:getMapFromPos(pos)
  200.   if (not chunkName) or (not posNo) then return false end
  201.  
  202.   if not self.map[chunkName] then
  203.     -- check for filesystem files, otherwise...    
  204.     self.map[chunkName] = {}
  205.   end
  206.    
  207.   if not self.map[chunkName][posNo] then self.map[chunkName][posNo] = {} end
  208.  
  209.   self.map._updated = os.time()
  210.   self.map[chunkName]._accessed = os.time()
  211.   self.map[chunkName]._updated = os.time()
  212.   value._updated = os.time()
  213.   self.map[chunkName][posNo] = utils.deepMerge(value, self.map[chunkName][posNo])
  214.   return true
  215. end
  216. function clsNav:setPos(pos_or_face)
  217.   --logger.spam("Nav:setPos(%s)\n",pos_or_face)
  218.   pos = checkPos(pos_or_face)
  219.   if pos then
  220.     self.pos = pos
  221.   else
  222.     self.pos = self:getPos(pos_or_face)
  223.   end
  224. end
  225. function clsNav:comparePos(poslist1,poslist2,isFaced) -- input either pos or tables of pos
  226.   poslist1 = checkPositions(poslist1)
  227.   if type(poslist1) ~= "table" then return false, 123, "comparePos: No legal pos in argument #1" end
  228.   poslist2 = checkPositions(poslist2)
  229.   if type(poslist2) ~= "table" then return false, 123, "comparePos: No legal pos in argument #2" end
  230.  
  231.     for i,pos1 in pairs(poslist1) do
  232.         --logger.spam("Pos1(%s): %s,%s,%s,%s\n", i, Pos1.x, Pos1.z, Pos1.y, Pos1.f)
  233.     for j,pos2 in pairs(poslist2) do
  234.       --logger.spam("  Pos2(%s): %s,%s,%s,%s\n", j, Pos2.x, Pos2.z, Pos2.y, Pos2.f)
  235.       if
  236.         pos1.x == pos2.x
  237.         and pos1.y == pos2.y
  238.         and pos1.z == pos2.z
  239.       then
  240.         if
  241.           (isFaced
  242.           and pos1.f
  243.           and pos2.f
  244.           and pos1.f == pos2.f)
  245.           or
  246.           (not isFaced)
  247.         then
  248.           return pos1, pos2
  249.         end
  250.       end
  251.         end
  252.   end
  253.  
  254.     return false
  255. end
  256. function clsNav:getPos(pos, dir) -- Input Position (table), FacingDirection (num) in any order
  257.   if type(dir) == "nil" and type(pos) == "number" then
  258.     dir = pos%6
  259.     pos = nil
  260.     elseif type(dir) == "number" then
  261.     dir = dir%6
  262.   else
  263.     dir = nil
  264.   end
  265.   pos = checkPos(pos or self.pos)
  266.   if not pos then return false, "Failed to lookup pos" end
  267.    
  268.     if dir == nil then return pos
  269.     elseif dir == 0 then return {["x"] = pos.x+1, ["y"] = pos.y, ["z"] = pos.z, ["f"] = dir}
  270.     elseif dir == 1 then return {["x"] = pos.x, ["y"] = pos.y+1, ["z"] = pos.z, ["f"] = dir}
  271.     elseif dir == 2 then return {["x"] = pos.x-1, ["y"] = pos.y, ["z"] = pos.z, ["f"] = dir}
  272.     elseif dir == 3 then return {["x"] = pos.x, ["y"] = pos.y-1, ["z"] = pos.z, ["f"] = dir}
  273.     elseif dir == 4 then return {["x"] = pos.x, ["y"] = pos.y, ["z"] = pos.z+1, ["f"] = pos.f}
  274.     elseif dir == 5 then return {["x"] = pos.x, ["y"] = pos.y, ["z"] = pos.z-1, ["f"] = pos.f}
  275.     end
  276.    
  277.     return false
  278. end
  279. function clsNav:getMap(pos)
  280.   local chunkName, posNo = self:getMapFromPos(pos)
  281.   if (not chunkName) or (not posNo) then return false, "Bad arguments" end
  282.  
  283.   if not self.map[chunkName] then
  284.     -- check for filesystem files, otherwise...    
  285.     return {}
  286.   end
  287.  
  288.   if not self.map[chunkName][posNo] then return {} end
  289.  
  290.   self.map[chunkName]._accessed = os.time()
  291.   return utils.deepCopy(self.map[chunkName][posNo])
  292. end
  293. function clsNav:detectAround() -- TODO: add other detections, repair tagging
  294.     --logger.Check("detectAround:%s,%s",location,value)
  295.   local _,substance = robot.detect()
  296.   self:putMap(self:getPos(self:getPos().f),{["substance"]=substance})
  297.   _,substance = robot.detectUp()
  298.   self:putMap(self:getPos(4),{["substance"]=substance})
  299.   _,substance = robot.detectDown()
  300.   self:putMap(self:getPos(5),{["substance"]=substance})
  301. end
  302. function clsNav:getPath(targets, options)
  303.     --This code (Aug, 2014) is written by Akuukis
  304.     --  who based on code (Sep 21, 2006) by Altair
  305.     --      who ported and upgraded code of LMelior
  306.  
  307.   local options = options or {}
  308.   local upvalues = {} -- upvalue, anti-crashing mechanism
  309.   local pathtries = 3
  310.   local function serializePos(pos)
  311.     return string.format("%05s",pos.x)..string.format("%05s",pos.y)..string.format("%03s",pos.z)
  312.   end
  313.   local function getHeuristic(startPos, targetPoslist, flag)
  314.     local minCost = math.huge
  315.     for _,targetPos in pairs(targetPoslist) do
  316.       if flag == "manhattan" then
  317.         minCost = math.min(minCost, 1 * (
  318.           (targetPos.pathWeight or 0) +
  319.           math.abs(startPos.x-targetPos.x) +
  320.           math.abs(startPos.y-targetPos.y) +
  321.           math.abs(startPos.z-targetPos.z) )
  322.         )
  323.       elseif flag == "euclidean" then
  324.         minCost = math.min(minCost, (targetPos.pathWeight or 0) + math.abs((
  325.           math.abs(startPos.x-targetPos.x) +
  326.           math.abs(startPos.y-targetPos.y) +
  327.           math.abs(startPos.z-targetPos.z) ) ^ (1/3) )
  328.         )
  329.       else -- manhattan
  330.         minCost = math.min(minCost, 1 * (
  331.           (targetPos.pathWeight or 0) +
  332.           math.abs(startPos.x-targetPos.x) +
  333.           math.abs(startPos.y-targetPos.y) +
  334.           math.abs(startPos.z-targetPos.z) )
  335.         )
  336.       end
  337.     end
  338.     if minCost ~= math.huge then return minCost else error(debug.traceback()) end
  339.   end
  340.   local function getCostBlocks(nextBase, flag)
  341.     local defaultMoveCost = 1 -- cost in time to move 1 square, no diagonal movement.
  342.     local defaultWanderCost = 1 -- average cost to move in unexplored block
  343.     local defaultDestroyCost = 3 -- average cost in time to move 1 square with solid block inside
  344.     if nextBase.substance == "air" then
  345.       if flag == "careful" then
  346.         return defaultMoveCost
  347.       elseif flag == "normal" then
  348.         return defaultMoveCost
  349.       elseif flag == "simple" then
  350.         return defaultMoveCost
  351.       elseif flag == "brutal" then
  352.         return defaultMoveCost
  353.       end
  354.     elseif nextBase.substance == "replaceable" then
  355.       if flag == "careful" then
  356.         return defaultMoveCost
  357.       elseif flag == "normal" then
  358.         return defaultMoveCost
  359.       elseif flag == "simple" then
  360.         return defaultMoveCost
  361.       elseif flag == "brutal" then
  362.         return defaultMoveCost
  363.       end
  364.     elseif nextBase.substance == "solid" then
  365.       if flag == "careful" then
  366.         return false
  367.       elseif flag == "normal" then
  368.         return defaultDestroyCost
  369.       elseif flag == "simple" then
  370.         return defaultMoveCost
  371.       elseif flag == "brutal" then
  372.         return defaultMoveCost
  373.       end
  374.     elseif nextBase.substance == "liquid" then
  375.       if flag == "careful" then
  376.         return false
  377.       elseif flag == "normal" then
  378.         return defaultDestroyCost
  379.       elseif flag == "simple" then
  380.         return defaultMoveCost
  381.       elseif flag == "brutal" then
  382.         return defaultDestroyCost
  383.       end
  384.     elseif nextBase.substance == "entity" then
  385.       if flag == "careful" then
  386.         return defaultMoveCost
  387.       elseif flag == "normal" then
  388.         return defaultMoveCost
  389.       elseif flag == "simple" then
  390.         return defaultMoveCost
  391.       elseif flag == "brutal" then
  392.         return defaultMoveCost
  393.       end
  394.     else
  395.       return defaultWanderCost
  396.     end
  397.   end
  398.   local function getCostTurn(curBase, dir, flag)
  399.     if flag == "simple" then return 0 end
  400.     local defaultTurnCost = 0.9 -- cost in time to turn 90 degrees
  401.     local turnCost = 0
  402.     if (not curBase.f) or curBase.f == dir or dir == 4 or dir == 5 then
  403.       return 0
  404.     elseif (dir-curBase.f)%4 == 2 then
  405.       return 2 * defaultTurnCost
  406.     else
  407.       return 1 * defaultTurnCost
  408.     end
  409.   end
  410.   local function getModTags(nextBase, flag)
  411.     local defaultRoadMod = 0.1 -- cost modifier for moving along roads
  412.     if nextBase.tag then
  413.       if flag == "careful" or flag == "normal" then
  414.         if nextBase.tag.evade then
  415.           return false
  416.         elseif nextBase.tag.road then
  417.           return defaultRoadMod
  418.         end
  419.       elseif flag == "simple" then
  420.         if nextBase.tag.evade then
  421.           return false
  422.         end
  423.       elseif flag == "brutal" then
  424.         -- ignore tags
  425.       end
  426.     end
  427.     return 1
  428.   end
  429.   local function tryPath(starts, targets, options)
  430.  
  431.     starts = checkPositions(starts)
  432.     if (not starts) or (not next(starts)) then return false, "No legal start provided" end
  433.     --for k,v in pairs(starts) do
  434.     --  logger.spam("tryPath: Start %s/%s (%2s,%2s,%2s,%2s,%2s)\n", k,#starts,v.x,v.y,v.z,v.f,v.weight)
  435.     --end
  436.     targets = checkPositions(targets)
  437.     if (not targets) or (not next(targets)) then return false, "No legal target provided" end
  438.     --for k,v in pairs(targets) do
  439.     --  logger.spam("tryPath: Target %s/%s (%2s,%2s,%2s,%2s,%2s)\n", k,#targets,v.x,v.y,v.z,v.f,v.weight)
  440.     --end
  441.    
  442.     local defaultHeurMod = 1 -- cost modifier for heuristics. Keep it big enough!
  443.     local flag = checkOptions(options, "careful", "normal", "simple", "brutal") or "careful"
  444.     local flag2 = checkOptions(options, "manhattan", "euclidean") or "manhattan"
  445.     local flag3 = checkOptions(options, "forwards", "backwards", "bidirectional") or "bidirectional"
  446.     --for k,v in pairs(options) do
  447.     --  logger.spam("tryPath: Option %s/%s %s\n", k,#options,v)
  448.     --end
  449.    
  450.     logger.info("Pathfinding: %s starts, %s targets, %s options.\n",#starts,#targets,#options + ((options._path and 1) or 0))
  451.    
  452.     options = nil
  453.     local SCL, SNL, SOL, TCL, TNL, TOL = {}, {}, {min={weight=math.huge}}, {}, {}, {min={weight=math.huge}}
  454.     for i=1,#starts do
  455.       local key = serializePos(starts[i])
  456.       if not SNL[key] then
  457.         local nextBase = self:getMap(starts[i])
  458.         local costBlocks = getCostBlocks(nextBase, flag)
  459.         local modTag = getModTags(nextBase, flag)
  460.         if costBlocks and modTag then
  461.           SNL[key] = starts[i] -- x,y,z,f
  462.           SNL[key].pathWeight = (SNL[key].weight or 0) + costBlocks * modTag
  463.           SNL[key].heurWeight = getHeuristic(starts[i], targets, flag2) * defaultHeurMod
  464.           SNL[key].weight = SNL[key].pathWeight + SNL[key].heurWeight
  465.           SNL[key].parent = false
  466.           SNL.min = SNL.min or {}
  467.           SNL.min.weight = math.min(SNL.min.weight or math.huge, SNL[key].weight)
  468.         end
  469.       end
  470.     end
  471.     for i=1,#targets do
  472.       local key = serializePos(targets[i])
  473.       if not TNL[key] then
  474.         local nextBase = self:getMap(targets[i])
  475.         local costBlocks = getCostBlocks(nextBase, flag)
  476.         local modTag = getModTags(nextBase, flag)
  477.         if costBlocks and modTag then
  478.           TNL[key] = targets[i] -- x,y,z,f
  479.           TNL[key].pathWeight = (TNL[key].weight or 0) + costBlocks * modTag
  480.           TNL[key].heurWeight = getHeuristic(targets[i], starts, flag2) * defaultHeurMod
  481.           TNL[key].weight = TNL[key].pathWeight + TNL[key].heurWeight
  482.           TNL[key].parent = false
  483.           TNL.min = TNL.min or {}
  484.           TNL.min.weight = math.min(TNL.min.weight or math.huge, TNL[key].weight)
  485.         end
  486.       end
  487.     end
  488.    
  489.     if (not starts) or (not next(starts)) then return false, "No legal start left, all filtered" end
  490.     if (not targets) or (not next(targets)) then return false, "No legal target left, all filtered" end
  491.     logger.info("Searching.")
  492.    
  493.     local side = false
  494.     local closedk = 0
  495.     local timer = os.time()
  496.     while true do
  497.      
  498.       if flag3 == "forwards" then
  499.         side = true
  500.       elseif flag3 == "backwards" then
  501.         side = false
  502.       elseif flag3 == "bidirectional" then
  503.         side = not side -- switch sides
  504.       end
  505.      
  506.       local openlist = (side and SOL) or TOL
  507.       local openlistNew = (side and SNL) or TNL
  508.       local closedlist = (side and SCL) or TCL
  509.       local targetlist = (side and {TCL.last, table.unpack(targets)}) or {SCL.last, table.unpack(starts)}
  510.      
  511.       local temp1, temp2 = openlist.min, openlistNew.min
  512.       openlist.min, openlistNew.min = nil, nil
  513.       if (not next(openlist)) and (not next(openlistNew)) then return false, "trapped in box or illegal targets" end -- trapped in box, cannot find path
  514.       openlist.min, openlistNew.min = temp1, temp2
  515.      
  516.       do  -- Find next node with the lowest weight (Prefer newer nodes)
  517.         local searchlist = (openlist.min.weight < openlistNew.min.weight and openlist) or openlistNew
  518.         local lowestDS = math.huge
  519.         searchlist.min.weight = math.huge
  520.         local basis = nil
  521.         for k,node in pairs(searchlist) do
  522.           if (node.weight or math.huge) < searchlist.min.weight then
  523.             if node.weight < lowestDS then
  524.               searchlist.min.weight = lowestDS
  525.               lowestDS = node.weight
  526.               basis = k
  527.             else
  528.               searchlist.min.weight = node.weight
  529.             end
  530.           end
  531.         end
  532.         closedlist[basis] = searchlist[basis]
  533.         closedlist[basis].key = basis
  534.         closedlist.last = closedlist[basis]
  535.         closedk = closedk + 1
  536.         searchlist[basis] = nil
  537.         for k,node in pairs(openlistNew) do
  538.           if k ~= "min" then
  539.             openlist[k] = node
  540.             openlistNew[k] = nil
  541.           else
  542.             openlist.min.weight = math.min(node.weight,openlist.min.weight)
  543.             openlistNew.min.weight = math.huge
  544.           end
  545.         end
  546.       end
  547.       local curBase = closedlist.last
  548.  
  549.       if closedk%25==0 then logger.info(".") end
  550.         -- logger.spam("%s %0s: %4s,%2s,%2s %3s+%4s=%4s ",
  551.           -- (side and "=>") or "<=",
  552.           -- closedk,
  553.           -- curBase.x,
  554.           -- curBase.y,
  555.           -- curBase.z,
  556.           -- math.floor(curBase.pathWeight*10)/10,
  557.           -- math.floor(curBase.heurWeight*10)/10,
  558.           -- math.floor((curBase.weight)*10)/10
  559.           -- --self:getMap(self:getPos(curBase)).substance
  560.           -- --utils.freeMemory() --GC!
  561.         -- )
  562.      
  563.       if self:comparePos(curBase,targetlist,false) then -- check if we have reached one of targetlist..
  564.         SOL, TOL, SNL, TNL, openlist, openlistNew, targetlist = nil, nil, nil, nil, nil, nil, nil -- free some RAM
  565.        
  566.         local startlist, endlist = SCL, TCL
  567.         local startBase, endBase = nil, nil
  568.         for k,target in pairs(startlist) do
  569.           if self:comparePos(curBase,target,false) then startBase = target end
  570.         end
  571.         for k,target in pairs(endlist) do
  572.           if self:comparePos(curBase,target,false) then endBase = target end
  573.         end
  574.        
  575.         if
  576.           ((not startBase) or startBase.parent == false)
  577.           and ((not endBase) or endBase.parent == false)
  578.         then return {}, {}, 0 end -- if we started at target
  579.        
  580.         local function extractPath(list,base)
  581.           if not base then return {} end
  582.           local path = {[1] = base}
  583.           --logger.spam("(%s,%s,%s|%s)",base.x,base.y,base.z,base.parent)
  584.           while path[#path].parent do
  585.             path[#path+1] = list[path[#path].parent]
  586.             list[path[#path-1].parent] = nil
  587.             --logger.spam("(%s,%s,%s|%s)",path[#path].x,path[#path].y,path[#path].z,path[#path].parent)
  588.             if #path%100==0 then thread.yield() end
  589.           end
  590.           --logger.spam("\n")
  591.           return path
  592.         end
  593.        
  594.         local startPath = extractPath(startlist, startBase) -- backwards
  595.         local endPath = extractPath(endlist, endBase) -- backwards
  596.        
  597.         local path = {}
  598.         for i=1,#startPath do path[#path+1] = startPath[#startPath-i+1] end
  599.         path[#path]=nil
  600.         for i=1,#endPath do path[#path+1] = endPath[i] end
  601.        
  602.         -- Change list of XZ coordinates into a list of directions
  603.         --logger.spam("dirPath. ")
  604.         thread.yield()
  605.         local dirPath = {}
  606.         for i=1,#path-1 do
  607.           if path[i+1].x > path[i].x then dirPath[i]=0 -- North
  608.           elseif path[i+1].y > path[i].y then dirPath[i]=1 -- East
  609.           elseif path[i+1].x < path[i].x then dirPath[i]=2 -- South
  610.           elseif path[i+1].y < path[i].y then dirPath[i]=3 -- West
  611.           elseif path[i+1].z > path[i].z then dirPath[i]=4 -- Up
  612.           elseif path[i+1].z < path[i].z then dirPath[i]=5 -- Down
  613.           end
  614.           --logger.spam("%s,", dirPath[i])
  615.         end
  616.         --logger.spam("\n")
  617.         return dirPath, path
  618.       end  
  619.      
  620.       for dir=0,5 do
  621.         local nextPos = self:getPos(curBase,dir)
  622.         local nextKey = serializePos(nextPos)
  623.         if not closedlist[nextKey] then
  624.           local nextBase = self:getMap(nextPos)
  625.           local costBlocks = getCostBlocks(nextBase, flag)
  626.           local costTurn = getCostTurn(curBase, dir, flag)
  627.           local modTag = getModTags(nextBase, flag)
  628.           if costBlocks and costTurn and modTag then
  629.             openlistNew[nextKey] = nextPos -- x,y,z,f
  630.             openlistNew[nextKey].pathWeight = curBase.pathWeight + (costBlocks + costTurn) * modTag
  631.             openlistNew[nextKey].heurWeight = getHeuristic(nextPos, targetlist, flag2) * defaultHeurMod
  632.             openlistNew[nextKey].weight = openlistNew[nextKey].pathWeight + openlistNew[nextKey].heurWeight
  633.             if openlist[nextKey] and openlist[nextKey].weight < openlistNew[nextKey].weight then
  634.               openlistNew[nextKey] = nil
  635.             else
  636.               openlist[nextKey] = nil
  637.               openlistNew[nextKey].parent = curBase.key
  638.               openlistNew.min.weight = math.min(openlistNew.min.weight or math.huge, openlistNew[nextKey].weight)
  639.             end
  640.           end
  641.         end
  642.       end
  643.  
  644.       do  -- find lowest weight for closedlist to add as lastTarget
  645.         local lowestDS = curBase.weight or math.huge
  646.         local lowestHeurWeight = curBase.heurWeight or math.huge
  647.         local basis = "last"
  648.         for k,node in pairs(closedlist) do
  649.           if (node.weight or math.huge) < lowestDS then
  650.             lowestDS = node.weight
  651.             basis = k
  652.           end
  653.           if (node.heurWeight or math.huge) < lowestHeurWeight then
  654.             lowestHeurWeight = node.heurWeight
  655.           end
  656.         end
  657.         if side then
  658.           upvalues.starts = {closedlist[basis], curBase}
  659.         else
  660.           upvalues.targets = {closedlist[basis], curBase}
  661.         end
  662.         upvalues.minHeurWeight = math.min(upvalues.minHeurWeight or math.huge, lowestHeurWeight)
  663.         upvalues.cycles = (upvalues.cycles or 0) + 1
  664.       end
  665.      
  666.       thread.yield()
  667.     end
  668.     return false
  669.   end
  670.   local oldUpvalues = {starts={},targets={},minHeurWeight=math.huge}
  671.  
  672.   repeat
  673.        
  674.     local ok, dirPath, path = pcall(
  675.       tryPath,
  676.       utils.deepCopy(upvalues.starts or {self:getPos()}),
  677.       utils.deepCopy(upvalues.targets or targets),
  678.       options
  679.     )
  680.     logger.info("\n")
  681.    
  682.     if ok then
  683.       if not dirPath then
  684.         return false, path -- error
  685.       elseif self:comparePos(path[1],self:getPos(),false) then
  686.         if self:comparePos(path[#path],targets,false) then
  687.           return dirPath, path, true, upvalues.cycles -- full path
  688.         else
  689.           return dirPath, path, false, upvalues.cycles -- partial path
  690.         end
  691.       else
  692.         logger.spam("Found another start: %s,%s,%s,%s\n",path[1].x,path[1].y,path[1].z,path[1].f)
  693.         targets = path[1] -- search to new target that leads to real target
  694.         upvalues.starts = nil
  695.         pathtries = 3
  696.         options._path = nil
  697.       end
  698.     else
  699.       logger.spam("Searching failed: %s\n",dirPath)
  700.     end
  701.    
  702.     if pathtries == 3 then
  703.       logger.spam("Retry with 'backwards'\n")
  704.       pathtries = pathtries - 1
  705.       options._path = "backwards"
  706.     elseif pathtries == 2 then
  707.       if upvalues.minHeurWeight < oldUpvalues.minHeurWeight then
  708.         logger.spam("Continue with 'backwards' %s/%s\n",upvalues.minHeurWeight,oldUpvalues.minHeurWeight)
  709.         oldUpvalues.minHeurWeight = upvalues.minHeurWeight
  710.       else
  711.         logger.spam("Retry with 'forwards'\n")
  712.         pathtries = pathtries - 1
  713.         options._path = "forwards"
  714.       end
  715.     elseif pathtries == 1 then
  716.       if upvalues.minHeurWeight < oldUpvalues.minHeurWeight then
  717.         logger.spam("Continue with 'forwards' %s/%s\n",upvalues.minHeurWeight,oldUpvalues.minHeurWeight)
  718.         oldUpvalues.minHeurWeight = upvalues.minHeurWeight
  719.       else
  720.         logger.spam("Retry with 'bidirectional'\n")
  721.         pathtries = pathtries - 1
  722.         options._path = "bidirectional"
  723.       end
  724.     elseif pathtries == 0 then
  725.       if upvalues.minHeurWeight < oldUpvalues.minHeurWeight then
  726.         logger.spam("Continue with 'bidirectional' %s/%s\n",upvalues.minHeurWeight,oldUpvalues.minHeurWeight)
  727.         oldUpvalues.minHeurWeight = upvalues.minHeurWeight
  728.       else
  729.         logger.spam("lastTargets %s at (%s,%s,%s)\n",lastTargets, lastTargets and lastTargets.x, lastTargets and lastTargets.y, lastTargets and lastTargets.z)
  730.         return false
  731.       end
  732.     end
  733.    
  734.     thread.yield()
  735.   until pathtries < 0
  736.  
  737.  
  738. end
  739. function clsNav:move(dir, options) -- dir={0=North|1=East|2=South|3=West|4=up|5=down}, returns true if succeeded
  740.  
  741.   local flag = checkOptions(options, "careful", "normal", "simple", "brutal") or "careful"
  742.   local flag2 = checkOptions(options, "fast", "explore", "patrol") or "explore"
  743.  
  744.     if not self:turnTo(dir) then return false, "no dir provided" end
  745.   for dir=0,3 do -- look around
  746.     if
  747.       (flag2 == "patrol") or
  748.       (flag2 == "explore" and not self:getMap(self:getPos(dir))._updated)
  749.     then
  750.       self:turnTo(dir)
  751.     end
  752.   end
  753.     if dir ~= self:getPos().f then self:turnTo(dir) end
  754.  
  755.     if flag == "careful" or flag == "normal" or flag == "simple" then
  756.     local pos = self:getMap(self:getPos(dir))
  757.     if pos and pos.tag then
  758.       return false, "Tagged as "..pos.tag
  759.     end
  760.   end
  761.    
  762.     if flag == "careful" then
  763.     local substance
  764.     if dir == 4 then _, substance = robot.detectUp()
  765.     elseif dir == 5 then _, substance = robot.detectDown()
  766.     else _, substance = robot.detect()
  767.     end
  768.     if substance ~= "air" and substance ~= "replaceable" then return false, substance end
  769.   end
  770.  
  771.   local ok, reason
  772.   if dir == 4 then
  773.     while flag ~= "careful" and robot.detectUp() do robot.swingUp() end
  774.     ok, reason = robot.up()
  775.   elseif dir == 5 then
  776.     while flag ~= "careful" and robot.detectDown() do robot.swingDown() end
  777.       ok, reason = robot.down()
  778.   else
  779.     while flag ~= "careful" and robot.detect() do robot.swing() end
  780.     ok, reason = robot.forward()
  781.   end
  782.   if ok then
  783.     self:setPos(dir)
  784.     self:detectAround()
  785.     --logger.spam("Nav.Move() Return true\n")
  786.     return true
  787.     else
  788.     return false, reason
  789.     end
  790.     return false, "Unexpected error"
  791. end
  792.  
  793. -- Core methods
  794. function clsNav:turnRight()
  795.     robot.turnRight()
  796.     --logger.spam("Nav.turnRight() Nav.Pos.f. %s => ",self:getPos().f)
  797.     self.pos.f = (self:getPos().f+1)%4
  798.     --logger.spam("%s\n",self:getPos().f)
  799.     self:detectAround()
  800.     return true
  801. end
  802. function clsNav:turnLeft()
  803.     robot.turnLeft()
  804.     --logger.spam("Nav:turnLeft() Nav.Pos.f. %s => ",self:getPos().f)
  805.     self.pos.f = (self:getPos().f+3)%4
  806.     --logger.spam("%s\n",self:getPos().f)
  807.     self:detectAround()
  808.     return true
  809. end
  810. function clsNav:turnAround()
  811.     if 1==math.floor(math.random(0,1)+0.5) then
  812.         self:turnRight()
  813.         self:turnRight()
  814.     else
  815.         self:turnLeft()
  816.         self:turnLeft()
  817.     end
  818.     return true
  819. end
  820. function clsNav:go(targets, options) -- table target1 [, table target2 ...] text option1 [, text option2 ...]
  821.    
  822.   local timer = os.time()
  823.   targets = checkPositions(targets) -- Filters legal targets
  824.   if (not targets) or (not next(targets)) then targets = {checkPos({0,0,0,0})} end
  825.  
  826.     local flag = checkOptions(options, "absolute", "relative", "relFace") or "absolute"
  827.     if flag == "relative" then
  828.         for j in ipairs(targets) do
  829.             targets[j].x = targets[j].x + self:getPos().x
  830.             targets[j].y = targets[j].y + self:getPos().y
  831.             targets[j].z = targets[j].z + self:getPos().z
  832.         end
  833.     end
  834.     if flag == "relFace" then
  835.         for j in ipairs(targets) do
  836.             targets[j].x = targets[j].x + self:getPos().x
  837.             targets[j].y = targets[j].y + self:getPos().y
  838.             targets[j].z = targets[j].z + self:getPos().z
  839.             if self:getPos().f == 0 then targets[j].x = self:getPos().x + targets[j].x; targets[j].y = self:getPos().y + targets[j].y end
  840.             if self:getPos().f == 1 then targets[j].x = self:getPos().x - targets[j].y; targets[j].y = self:getPos().y + targets[j].x end
  841.             if self:getPos().f == 2 then targets[j].x = self:getPos().x - targets[j].x; targets[j].y = self:getPos().y - targets[j].y end
  842.             if self:getPos().f == 3 then targets[j].x = self:getPos().x + targets[j].y; targets[j].y = self:getPos().y - targets[j].x end
  843.         end
  844.     end
  845.    
  846.     local tries = 2 -- TODO
  847.   local count = 0
  848.     --if #targets > 0 then logger.spam("Nav.Go(x%s)(%s,%s,%s)\n", #targets, targets[1].x, targets[1].z, targets[1].y) end
  849.     while tries <= 2 do
  850.     count = count + 1
  851.     logger.info("Go: Try No.%s, (%2s,%2s,%2s,%2s) -> ...\n",
  852.       count,self:getPos().x,self:getPos().y,self:getPos().z,self:getPos().f)
  853.     for k,v in pairs(targets) do
  854.       logger.info("  %s: (%2s,%2s,%2s,%2s)\n", k,v.x,v.y,v.z,v.f)
  855.     end
  856.    
  857.         local destination = self:comparePos(targets, self:getPos())
  858.         if destination then
  859.             self:turnTo(destination.f)
  860.       logger.info("Go: Arrived with %s try in %s seconds!\n",count,(os.time()-timer)/100)
  861.             return true
  862.         end
  863.    
  864.     local dirPath,err,ok,cycles = self:getPath(targets,options)
  865.         if not dirPath then -- TODO: Cannot find path!
  866.       logger.info("Go: Failed to find path after %s tries in %s seconds because %s\n",count,(os.time()-timer)/100,err)
  867.       if tries == 1 then
  868.         logger.info("Go: Retry in 10 seconds...\n")
  869.         os.sleep(10)
  870.         self:detectAround()
  871.         self:turnAround()
  872.         self:turnAround()
  873.       end
  874.       tries = tries + 1
  875.         else
  876.       tries = 1
  877.       if ok then
  878.         logger.info("Go: Found %s step full path (of %s nodes)\n",#dirPath,cycles)
  879.       else
  880.         logger.info("Go: Found %s step partial path (of %s nodes)\n",#dirPath,cycles)
  881.       end
  882.       local i = 1
  883.       local ok, err = true, ""
  884.       while i <= #dirPath and (ok or err == "no dir provided") do
  885.         thread.yield()
  886.         ok, err = self:move(dirPath[i], options)
  887.         if ok then
  888.           logger.info("Go: Step %s/%s done (%2s,%2s,%2s,%2s)\n",i,#dirPath,
  889.             self:getPos().x,self:getPos().y,self:getPos().z,self:getPos().f)
  890.         else
  891.           logger.info("Go: Step failed because %s\n",err)
  892.         end
  893.         i = i + 1
  894.       end
  895.     end
  896.     end
  897.     return false, "Go: Aborting, exceeded retries"
  898. end
  899.  
  900. -- Shortcut methods
  901. function clsNav:turnTo(dir)
  902.     if type(dir) ~= "number" then return false end
  903.   if type(dir) == "nil" or dir >= 4 then return true end
  904.     if dir==self:getPos().f or dir==self:getPos().f+4 then return true
  905.     elseif dir==self:getPos().f-1 or dir==self:getPos().f+3 then return self:turnLeft()
  906.     elseif dir==self:getPos().f-2 or dir==self:getPos().f+2 then return self:turnAround()
  907.     elseif dir==self:getPos().f-3 or dir==self:getPos().f+1 then return self:turnRight()
  908.     end
  909. end
  910. function clsNav:step(options)
  911.     return self:move(self:getPos().f,options)
  912. end
  913. function clsNav:stepUp(options)
  914.     return self:move(4,options)
  915. end
  916. function clsNav:stepDown(options)
  917.     return self:move(5,options)
  918. end
  919. function clsNav:goNextTo(center, options)
  920.     local SixTargets = {}
  921.     --logger.spam("Center: ")
  922.     --for i,v in pairs(center) do logger.spam("'%s'=%s, ",i,v) end
  923.     --logger.spam("SixTargets: %s\n",SixTargets)
  924.     for i=0,5 do
  925.         SixTargets[i+1] = self:getPos(center,i)
  926.         --logger.spam("SixTargets: %s\n",SixTargets)
  927.         --for j,v in pairs(self:getPos(center,i)) do logger.spam("'%s'=%s, ",j,v) end
  928.         --for j,v in pairs(SixTargets[1]) do logger.spam("'%s'=%s, ",j,v) end
  929.         --logger.spam("%s: %s,%s,%s,%s\n",SixTargets[i+1], SixTargets[i+1].x,SixTargets[i+1].z,SixTargets[i+1].y,SixTargets[i+1].f)
  930.     end
  931.     --logger.Check("")
  932.     SixTargets[1].f = 2
  933.     SixTargets[2].f = 3
  934.     SixTargets[3].f = 0
  935.     SixTargets[4].f = 1
  936.     SixTargets[5].f = nil
  937.     SixTargets[6].f = nil
  938.     self:go( SixTargets[1], SixTargets[2], SixTargets[3], SixTargets[4], SixTargets[5], SixTargets[6], options )
  939. end
  940. function clsNav:drawMap(offset)
  941.   offset = checkPos(offset)
  942.   local resX, resY = component.gpu.getResolution()
  943.  
  944.   local lowLeft = self:getPos()
  945.   lowLeft.x = lowLeft.x - (resY+resY%2)/2
  946.   lowLeft.y = lowLeft.y - (resX+resX%2)/2
  947.   if offset then
  948.     lowLeft.x = lowLeft.x + offset.x
  949.     lowLeft.y = lowLeft.y + offset.y
  950.     lowLeft.z = lowLeft.z + offset.z
  951.   end
  952.  
  953.   for yScreen=1,resY do
  954.     local output = ""
  955.     for xScreen=1,resX do
  956.       local subst = self:getMap({lowLeft.x+yScreen,lowLeft.y+xScreen,lowLeft.z}).substance
  957.       if subst == nil then output = output.." "
  958.       elseif subst == "air" then output = output.."."
  959.       elseif subst == "solid" then output = output.."#"
  960.       elseif subst == "entity" then output = output.."e"
  961.       elseif subst == "liquid" then output = output.."~"
  962.       elseif subst == "replaceable" then output = output..","
  963.       else error(debug.traceback())
  964.       end
  965.     end
  966.     component.gpu.set(1,resY+1-yScreen,output)
  967.   end
  968.   return true
  969. end
  970.  
  971. return clsNav
  972.  
  973. ---------------- Details & Notes ----------------------------------------------
  974.  
  975. --[[ Tutorials
  976. General: http://www.lua.org/pil/contents.html
  977. Varargs: http://lua-users.org/wiki/VarargTheSecondClassCitizen
  978. Intro to A* - http://www.raywenderlich.com/4946/introduction-to-a-pathfinding
  979. Try it out! - http://zerowidth.com/2013/05/05/jump-point-search-explained.html
  980. Better version to try out! - https://qiao.github.io/PathFinding.js/visual/
  981. Useful - http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
  982. --]]
  983. --[[ Coord system
  984. # MC logic
  985. Coords... North: z--, East: x++, South: z++, West: x--, Up: y++, Down: y--
  986. Coords... X points East, Z points South, Y points Up
  987. Facing... 0: South, 1: West, 2: North, 3: East
  988. Facing... Starts at South, goes clockwise
  989. # Nav logic
  990. Coords... North: x++, East: y++, South: x--, West: y--, Up: z++, Down: z--
  991. Coords... X points North, Y points East, Z points Up
  992. Facing... 0: North, 1: East, 2: South, 4: West
  993. Facing... Starts at North, goes clockwise
  994.  
  995. use getNavFromMC() or getMCfromNav() to switch if needed, returns table
  996. --]]
  997. --[=[ Map structure
  998. self.map[chunkID][SerializedCoord] = nil --[[unknown--]] or childID --[[if lended--]] or {
  999.   substance = "air" or "solid" or "entity" or "replaceable" or "liquid"
  1000.   content = nil --[[unknown--]] or {--[[anything useful--]]}
  1001.   updated = 0 -- time in seconds from Colony start
  1002.   tag = {[string][,...]} -- RoCoWa related only, like "road", "evade", etc.
  1003. --]=]
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement