From a36d54a372c0e1140d33b41607c18629064ce9aa Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Wed, 30 Oct 2019 22:50:01 -0600 Subject: [PATCH] restructure --- builder/.package | 1 + gps/.package | 3 + milo/MiloLocal.lua | 10 +- milo/apis/init.lua | 12 +- milo/core/learnWizard.lua | 2 +- milo/core/machines.lua | 2 +- milo/plugins/machineLearn.lua | 2 +- milo/plugins/turtleLearn.lua | 2 +- miloApps/.package | 9 + {milo => miloApps}/apps/brewArray.lua | 0 {milo => miloApps}/apps/cobblegen.lua | 0 {milo => miloApps}/apps/enderchest.lua | 0 {milo => miloApps}/apps/furni.lua | 0 {milo => miloApps}/apps/storageGen.lua | 0 {milo => miloApps}/apps/water.lua | 0 miners/simpleMiner.lua | 2 +- packages.list | 1 + pickup/.package | 1 + turtle/apis/jumper/core/bheap.lua | 175 ++++ turtle/apis/jumper/core/node.lua | 41 + turtle/apis/jumper/core/path.lua | 67 ++ turtle/apis/jumper/core/utils.lua | 57 ++ turtle/apis/jumper/grid.lua | 101 ++ turtle/apis/jumper/pathfinder.lua | 104 ++ turtle/apis/jumper/search/astar.lua | 77 ++ turtle/apis/pathfind.lua | 256 +++++ turtle/autorun/6.tl3.lua | 1273 ++++++++++++++++++++++++ {common => turtle}/canvasClient.lua | 0 28 files changed, 2185 insertions(+), 13 deletions(-) create mode 100644 miloApps/.package rename {milo => miloApps}/apps/brewArray.lua (100%) rename {milo => miloApps}/apps/cobblegen.lua (100%) rename {milo => miloApps}/apps/enderchest.lua (100%) rename {milo => miloApps}/apps/furni.lua (100%) rename {milo => miloApps}/apps/storageGen.lua (100%) rename {milo => miloApps}/apps/water.lua (100%) create mode 100644 turtle/apis/jumper/core/bheap.lua create mode 100644 turtle/apis/jumper/core/node.lua create mode 100644 turtle/apis/jumper/core/path.lua create mode 100644 turtle/apis/jumper/core/utils.lua create mode 100644 turtle/apis/jumper/grid.lua create mode 100644 turtle/apis/jumper/pathfinder.lua create mode 100644 turtle/apis/jumper/search/astar.lua create mode 100644 turtle/apis/pathfind.lua create mode 100644 turtle/autorun/6.tl3.lua rename {common => turtle}/canvasClient.lua (100%) diff --git a/builder/.package b/builder/.package index e1140b2..bfcf3b7 100644 --- a/builder/.package +++ b/builder/.package @@ -1,6 +1,7 @@ { required = { 'core', + 'turtle', }, title = 'Schematic Builder', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/builder', diff --git a/gps/.package b/gps/.package index 8e1ee40..9a31617 100644 --- a/gps/.package +++ b/gps/.package @@ -1,4 +1,7 @@ { + required = { + 'turtle', + }, title = 'GPS', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/gps', description = [[A single turtle GPS server diff --git a/milo/MiloLocal.lua b/milo/MiloLocal.lua index d3a93ca..68b409f 100644 --- a/milo/MiloLocal.lua +++ b/milo/MiloLocal.lua @@ -180,21 +180,17 @@ Event.on('turtle_inventory', function() end) end) -local cycleHandle -cycleHandle = Event.onInterval(5, function() +Event.onInterval(5, function() Event.trigger('milo_cycle') - if context.taskCounter > 0 then - --local average = context.taskTimer / context.taskCounter - --_syslog('Interval: ' .. math.max(5, 2 + average * 3)) - --cycleHandle.updateInterval(math.max(5, 2 + average * 3)) - end end) Event.on({ 'storage_offline', 'storage_online' }, function() if context.storage:isOnline() then Milo:resumeCrafting({ key = 'storageOnline' }) + turtle.setStatus('Milo: online') else Milo:pauseCrafting({ key = 'storageOnline', msg = 'Storage offline' }) + turtle.setStatus('Milo: offline') end end) diff --git a/milo/apis/init.lua b/milo/apis/init.lua index 5287811..41b42b2 100644 --- a/milo/apis/init.lua +++ b/milo/apis/init.lua @@ -211,6 +211,16 @@ function Milo:makeRequest(item, count, callback) return request end +function Milo:emptyInventory() + for i = 1, 16 do + if turtle.getItemCount(i) > 0 then + turtle.select(i) + turtle.drop() + end + end + turtle.select(1) +end + function Milo:eject(item, count) local total = 0 while count > 0 do @@ -224,7 +234,7 @@ function Milo:eject(item, count) --Sound.play('ui.button.click') Sound.play('entity.illusion_illager.death', .3) - turtle.emptyInventory() + self:emptyInventory() end return total end diff --git a/milo/core/learnWizard.lua b/milo/core/learnWizard.lua index ee5ff2f..0f8917f 100644 --- a/milo/core/learnWizard.lua +++ b/milo/core/learnWizard.lua @@ -77,7 +77,7 @@ end function learnPage:eventHandler(event) if event.type == 'cancel' then - turtle.emptyInventory() + Milo:emptyInventory() UI:setPreviousPage() elseif event.type == 'form_invalid' or event.type == 'general_error' then diff --git a/milo/core/machines.lua b/milo/core/machines.lua index b29a60b..844e491 100644 --- a/milo/core/machines.lua +++ b/milo/core/machines.lua @@ -346,7 +346,7 @@ function nodeWizard.filter:eventHandler(event) self:resetGrid() self.grid:update() self.grid:draw() - turtle.emptyInventory() + Milo:emptyInventory() elseif event.type == 'remove_entry' then local row = self.grid:getSelected() diff --git a/milo/plugins/machineLearn.lua b/milo/plugins/machineLearn.lua index 76e5069..388a4f0 100644 --- a/milo/plugins/machineLearn.lua +++ b/milo/plugins/machineLearn.lua @@ -122,7 +122,7 @@ function pages.confirmation:validate() end Milo:saveMachineRecipe(recipe, result, machine.name) - turtle.emptyInventory() + Milo:emptyInventory() local displayName = itemDB:getName(result) UI:setPage('listing', { diff --git a/milo/plugins/turtleLearn.lua b/milo/plugins/turtleLearn.lua index 43c6125..2c09abc 100644 --- a/milo/plugins/turtleLearn.lua +++ b/milo/plugins/turtleLearn.lua @@ -23,7 +23,7 @@ function pages.turtleCraft:validate() if recipe then local displayName = itemDB:getName(recipe) - turtle.emptyInventory() + Milo:emptyInventory() UI:setPage('listing', { filter = displayName, diff --git a/miloApps/.package b/miloApps/.package new file mode 100644 index 0000000..30e04c8 --- /dev/null +++ b/miloApps/.package @@ -0,0 +1,9 @@ +{ + required = { + 'turtle', + }, + title = 'Milo extra applications', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/miloApps', + description = [[Programs for turtles in a Milo network]], + licence = 'MIT', +} diff --git a/milo/apps/brewArray.lua b/miloApps/apps/brewArray.lua similarity index 100% rename from milo/apps/brewArray.lua rename to miloApps/apps/brewArray.lua diff --git a/milo/apps/cobblegen.lua b/miloApps/apps/cobblegen.lua similarity index 100% rename from milo/apps/cobblegen.lua rename to miloApps/apps/cobblegen.lua diff --git a/milo/apps/enderchest.lua b/miloApps/apps/enderchest.lua similarity index 100% rename from milo/apps/enderchest.lua rename to miloApps/apps/enderchest.lua diff --git a/milo/apps/furni.lua b/miloApps/apps/furni.lua similarity index 100% rename from milo/apps/furni.lua rename to miloApps/apps/furni.lua diff --git a/milo/apps/storageGen.lua b/miloApps/apps/storageGen.lua similarity index 100% rename from milo/apps/storageGen.lua rename to miloApps/apps/storageGen.lua diff --git a/milo/apps/water.lua b/miloApps/apps/water.lua similarity index 100% rename from milo/apps/water.lua rename to miloApps/apps/water.lua diff --git a/miners/simpleMiner.lua b/miners/simpleMiner.lua index afd7c14..4acd2b4 100644 --- a/miners/simpleMiner.lua +++ b/miners/simpleMiner.lua @@ -1,4 +1,4 @@ -local Pathing = require('opus.pathfind') +local Pathing = require('turtle.pathfind') local Point = require('opus.point') local Util = require('opus.util') diff --git a/packages.list b/packages.list index 7f56846..cf5eb83 100644 --- a/packages.list +++ b/packages.list @@ -10,6 +10,7 @@ [ 'gps' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/gps/.package', [ 'mbs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/mbs/.package', [ 'milo' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/milo/.package', + [ 'miloApps' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/miloApps/.package', [ 'miners' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/miners/.package', [ 'monitor' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/monitor/.package', [ 'neural' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/neural/.package', diff --git a/pickup/.package b/pickup/.package index ea5dd64..3e1b222 100644 --- a/pickup/.package +++ b/pickup/.package @@ -1,6 +1,7 @@ { required = { 'core', + 'turtle', }, title = 'Move resources around with turtles', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/pickup', diff --git a/turtle/apis/jumper/core/bheap.lua b/turtle/apis/jumper/core/bheap.lua new file mode 100644 index 0000000..c58ce2f --- /dev/null +++ b/turtle/apis/jumper/core/bheap.lua @@ -0,0 +1,175 @@ +--- A light implementation of Binary heaps data structure. +-- While running a search, some search algorithms (Astar, Dijkstra, Jump Point Search) have to maintains +-- a list of nodes called __open list__. Retrieve from this list the lowest cost node can be quite slow, +-- as it normally requires to skim through the full set of nodes stored in this list. This becomes a real +-- problem especially when dozens of nodes are being processed (on large maps). +-- +-- The current module implements a binary heap +-- data structure, from which the search algorithm will instantiate an open list, and cache the nodes being +-- examined during a search. As such, retrieving the lower-cost node is faster and globally makes the search end +-- up quickly. +-- +-- This module is internally used by the library on purpose. +-- It should normally not be used explicitely, yet it remains fully accessible. +-- + +--[[ + Notes: + This lighter implementation of binary heaps, based on : + https://github.com/Yonaba/Binary-Heaps +--]] + +if (...) then + + -- Dependency + local Utils = require((...):gsub('%.bheap$','.utils')) + + -- Local reference + local floor = math.floor + + -- Default comparison function + local function f_min(a,b) return a < b end + + -- Percolates up + local function percolate_up(heap, index) + if index == 1 then return end + local pIndex + if index <= 1 then return end + if index%2 == 0 then + pIndex = index/2 + else pIndex = (index-1)/2 + end + if not heap._sort(heap._heap[pIndex], heap._heap[index]) then + heap._heap[pIndex], heap._heap[index] = + heap._heap[index], heap._heap[pIndex] + percolate_up(heap, pIndex) + end + end + + -- Percolates down + local function percolate_down(heap,index) + local lfIndex,rtIndex,minIndex + lfIndex = 2*index + rtIndex = lfIndex + 1 + if rtIndex > heap._size then + if lfIndex > heap._size then return + else minIndex = lfIndex end + else + if heap._sort(heap._heap[lfIndex],heap._heap[rtIndex]) then + minIndex = lfIndex + else + minIndex = rtIndex + end + end + if not heap._sort(heap._heap[index],heap._heap[minIndex]) then + heap._heap[index],heap._heap[minIndex] = heap._heap[minIndex],heap._heap[index] + percolate_down(heap,minIndex) + end + end + + -- Produces a new heap + local function newHeap(template,comp) + return setmetatable({_heap = {}, + _sort = comp or f_min, _size = 0}, + template) + end + + + --- The `heap` class.
+ -- This class is callable. + -- _Therefore,_ heap(...) _is used to instantiate new heaps_. + -- @type heap + local heap = setmetatable({}, + {__call = function(self,...) + return newHeap(self,...) + end}) + heap.__index = heap + + --- Checks if a `heap` is empty + -- @class function + -- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise + -- @usage + -- if myHeap:empty() then + -- print('Heap is empty!') + -- end + function heap:empty() + return (self._size==0) + end + + --- Clears the `heap` (removes all items queued in the heap) + -- @class function + -- @treturn heap self (the calling `heap` itself, can be chained) + -- @usage myHeap:clear() + function heap:clear() + self._heap = {} + self._size = 0 + self._sort = self._sort or f_min + return self + end + + --- Adds a new item in the `heap` + -- @class function + -- @tparam value item a new value to be queued in the heap + -- @treturn heap self (the calling `heap` itself, can be chained) + -- @usage + -- myHeap:push(1) + -- -- or, with chaining + -- myHeap:push(1):push(2):push(4) + function heap:push(item) + if item then + self._size = self._size + 1 + self._heap[self._size] = item + percolate_up(self, self._size) + end + return self + end + + --- Pops from the `heap`. + -- Removes and returns the lowest cost item (with respect to the comparison function being used) from the `heap`. + -- @class function + -- @treturn value a value previously pushed into the heap + -- @usage + -- while not myHeap:empty() do + -- local lowestValue = myHeap:pop() + -- ... + -- end + function heap:pop() + local root + if self._size > 0 then + root = self._heap[1] + self._heap[1] = self._heap[self._size] + self._heap[self._size] = nil + self._size = self._size-1 + if self._size>1 then + percolate_down(self, 1) + end + end + return root + end + + --- Restores the `heap` property. + -- Reorders the `heap` with respect to the comparison function being used. + -- When given argument __item__ (a value existing in the `heap`), will sort from that very item in the `heap`. + -- Otherwise, the whole `heap` will be cheacked. + -- @class function + -- @tparam[opt] value item the modified value + -- @treturn heap self (the calling `heap` itself, can be chained) + -- @usage myHeap:heapify() + function heap:heapify(item) + if self._size == 0 then return end + if item then + local i = Utils.indexOf(self._heap,item) + if i then + percolate_down(self, i) + percolate_up(self, i) + end + return + end + for i = floor(self._size/2),1,-1 do + percolate_down(self,i) + end + return self + end + + return heap +end \ No newline at end of file diff --git a/turtle/apis/jumper/core/node.lua b/turtle/apis/jumper/core/node.lua new file mode 100644 index 0000000..09d1db4 --- /dev/null +++ b/turtle/apis/jumper/core/node.lua @@ -0,0 +1,41 @@ +--- The Node class. +-- The `node` represents a cell (or a tile) on a collision map. Basically, for each single cell (tile) +-- in the collision map passed-in upon initialization, a `node` object will be generated +-- and then cached within the `grid`. +-- +-- In the following implementation, nodes can be compared using the `<` operator. The comparison is +-- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search +-- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details. +-- +if (...) then + + local Node = {} + Node.__index = Node + + function Node:new(x,y,z) + return setmetatable({x = x, y = y, z = z }, Node) + end + + -- Enables the use of operator '<' to compare nodes. + -- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost + function Node.__lt(A,B) return (A._f < B._f) end + + function Node:getX() return self.x end + function Node:getY() return self.y end + function Node:getZ() return self.z end + + --- Clears temporary cached attributes of a `node`. + -- Deletes the attributes cached within a given node after a pathfinding call. + -- This function is internally used by the search algorithms, so you should not use it explicitely. + function Node:reset() + self._g, self._h, self._f = nil, nil, nil + self._opened, self._closed, self._parent = nil, nil, nil + return self + end + + return setmetatable(Node, + {__call = function(_,...) + return Node:new(...) + end} + ) +end \ No newline at end of file diff --git a/turtle/apis/jumper/core/path.lua b/turtle/apis/jumper/core/path.lua new file mode 100644 index 0000000..f02c41b --- /dev/null +++ b/turtle/apis/jumper/core/path.lua @@ -0,0 +1,67 @@ +--- The Path class. +-- The `path` class is a structure which represents a path (ordered set of nodes) from a start location to a goal. +-- An instance from this class would be a result of a request addressed to `Pathfinder:getPath`. +-- +-- This module is internally used by the library on purpose. +-- It should normally not be used explicitely, yet it remains fully accessible. +-- + +if (...) then + + local t_remove = table.remove + + local Path = {} + Path.__index = Path + + function Path:new() + return setmetatable({_nodes = {}}, Path) + end + + --- Iterates on each single `node` along a `path`. At each step of iteration, + -- returns the `node` plus a count value. Aliased as @{Path:nodes} + -- @usage + -- for node, count in p:iter() do + -- ... + -- end + function Path:nodes() + local i = 1 + return function() + if self._nodes[i] then + i = i+1 + return self._nodes[i-1],i-1 + end + end + end + + --- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path` + -- consisting of straight moves. Does the opposite of @{Path:fill} + -- @class function + -- @treturn path self (the calling `path` itself, can be chained) + -- @see Path:fill + -- @usage p:filter() + function Path:filter() + local i = 2 + local xi,yi,zi,dx,dy,dz, olddx, olddy, olddz + xi,yi,zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z + dx, dy,dz = xi - self._nodes[i-1].x, yi-self._nodes[i-1].y, zi-self._nodes[i-1].z + while true do + olddx, olddy, olddz = dx, dy, dz + if self._nodes[i+1] then + i = i+1 + xi, yi, zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z + dx, dy, dz = xi - self._nodes[i-1].x, yi - self._nodes[i-1].y, zi - self._nodes[i-1].z + if olddx == dx and olddy == dy and olddz == dz then + t_remove(self._nodes, i-1) + i = i - 1 + end + else break end + end + return self + end + + return setmetatable(Path, + {__call = function(_,...) + return Path:new(...) + end + }) +end \ No newline at end of file diff --git a/turtle/apis/jumper/core/utils.lua b/turtle/apis/jumper/core/utils.lua new file mode 100644 index 0000000..ab5f764 --- /dev/null +++ b/turtle/apis/jumper/core/utils.lua @@ -0,0 +1,57 @@ +-- Various utilities for Jumper top-level modules + +if (...) then + + -- Dependencies + local _PATH = (...):gsub('%.utils$','') + local Path = require (_PATH .. '.path') + + -- Local references + local pairs = pairs + local t_insert = table.insert + + -- Raw array items count + local function arraySize(t) + local count = 0 + for _ in pairs(t) do + count = count+1 + end + return count + end + + -- Extract a path from a given start/end position + local function traceBackPath(finder, node, startNode) + local path = Path:new() + path._grid = finder._grid + while true do + if node._parent then + t_insert(path._nodes,1,node) + node = node._parent + else + t_insert(path._nodes,1,startNode) + return path + end + end + end + + -- Lookup for value in a table + local indexOf = function(t,v) + for i = 1,#t do + if t[i] == v then return i end + end + return nil + end + + -- Is i out of range + local function outOfRange(i,low,up) + return (i< low or i > up) + end + + return { + arraySize = arraySize, + indexOf = indexOf, + outOfRange = outOfRange, + traceBackPath = traceBackPath + } + +end diff --git a/turtle/apis/jumper/grid.lua b/turtle/apis/jumper/grid.lua new file mode 100644 index 0000000..6cfc53a --- /dev/null +++ b/turtle/apis/jumper/grid.lua @@ -0,0 +1,101 @@ +--- The Grid class. +-- Implementation of the `grid` class. +-- The `grid` is a implicit graph which represents the 2D +-- world map layout on which the `pathfinder` object will run. +-- During a search, the `pathfinder` object needs to save some critical values. +-- These values are cached within each `node` +-- object, and the whole set of nodes are tight inside the `grid` object itself. + +if (...) then + + -- Dependencies + local _PATH = (...):gsub('%.grid$','') + + -- Local references + local Utils = require (_PATH .. '.core.utils') + local Node = require (_PATH .. '.core.node') + + -- Local references + local setmetatable = setmetatable + + -- Offsets for straights moves + local straightOffsets = { + {x = 1, y = 0, z = 0} --[[W]], {x = -1, y = 0, z = 0}, --[[E]] + {x = 0, y = 1, z = 0} --[[S]], {x = 0, y = -1, z = 0}, --[[N]] + {x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]] + } + + local Grid = {} + Grid.__index = Grid + + function Grid:new(dim) + local newGrid = { } + newGrid._min_x, newGrid._max_x = dim.x, dim.ex + newGrid._min_y, newGrid._max_y = dim.y, dim.ey + newGrid._min_z, newGrid._max_z = dim.z, dim.ez + newGrid._nodes = { } + newGrid._width = (newGrid._max_x-newGrid._min_x)+1 + newGrid._height = (newGrid._max_y-newGrid._min_y)+1 + newGrid._length = (newGrid._max_z-newGrid._min_z)+1 + return setmetatable(newGrid,Grid) + end + + function Grid:isWalkableAt(x, y, z) + local node = self:getNodeAt(x,y,z) + return node and node.walkable ~= 1 + end + + function Grid:getWidth() + return self._width + end + + function Grid:getHeight() + return self._height + end + + function Grid:getNodes() + return self._nodes + end + + function Grid:getBounds() + return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z + end + + --- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`. + -- @treturn {node,...} an array of nodes neighbouring a given node + function Grid:getNeighbours(node) + local neighbours = {} + for i = 1,#straightOffsets do + local n = self:getNodeAt( + node.x + straightOffsets[i].x, + node.y + straightOffsets[i].y, + node.z + straightOffsets[i].z + ) + if n and self:isWalkableAt(n.x, n.y, n.z) then + neighbours[#neighbours+1] = n + end + end + + return neighbours + end + + function Grid:getNodeAt(x,y,z) + if not x or not y or not z then return end + if Utils.outOfRange(x,self._min_x,self._max_x) then return end + if Utils.outOfRange(y,self._min_y,self._max_y) then return end + if Utils.outOfRange(z,self._min_z,self._max_z) then return end + + -- inefficient + if not self._nodes[y] then self._nodes[y] = {} end + if not self._nodes[y][x] then self._nodes[y][x] = {} end + if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end + return self._nodes[y][x][z] + end + + return setmetatable(Grid,{ + __call = function(self,...) + return self:new(...) + end + }) + +end diff --git a/turtle/apis/jumper/pathfinder.lua b/turtle/apis/jumper/pathfinder.lua new file mode 100644 index 0000000..0ff2844 --- /dev/null +++ b/turtle/apis/jumper/pathfinder.lua @@ -0,0 +1,104 @@ +--[[ + The following License applies to all files within the jumper directory. + + Note that this is only a partial copy of the full jumper code base. Also, + the code was modified to support 3D maps. +--]] + +--[[ +This work is under MIT-LICENSE +Copyright (c) 2012-2013 Roland Yonaba. + +-- https://opensource.org/licenses/MIT + +--]] + +local _VERSION = "" +local _RELEASEDATE = "" + +if (...) then + + -- Dependencies + local _PATH = (...):gsub('%.pathfinder$','') + local Utils = require (_PATH .. '.core.utils') + + -- Internalization + local pairs = pairs + local assert = assert + local setmetatable = setmetatable + + --- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper. + --
  • [A*](http://en.wikipedia.org/wiki/A*_search_algorithm)
  • + local Finders = { + ['ASTAR'] = require (_PATH .. '.search.astar'), + } + + -- Will keep track of all nodes expanded during the search + -- to easily reset their properties for the next pathfinding call + local toClear = {} + + -- Performs a traceback from the goal node to the start node + -- Only happens when the path was found + + local Pathfinder = {} + Pathfinder.__index = Pathfinder + + function Pathfinder:new(heuristic) + local newPathfinder = {} + setmetatable(newPathfinder, Pathfinder) + self._finder = Finders.ASTAR + self._heuristic = heuristic + return newPathfinder + end + + function Pathfinder:setGrid(grid) + self._grid = grid + return self + end + + --- Calculates a `path`. Returns the `path` from start to end location + -- Both locations must exist on the collision map. The starting location can be unwalkable. + -- @treturn path a path (array of nodes) when found, otherwise nil + -- @usage local path = myFinder:getPath(1,1,5,5) + function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh) + self:reset() + local startNode = self._grid:getNodeAt(startX, startY, startZ) + local endNode = self._grid:getNodeAt(endX, endY, endZ) + if not startNode or not endNode then + return nil + end + + startNode.heading = ih + endNode.heading = oh + + assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ)) + assert(endNode and self._grid:isWalkableAt(endX, endY, endZ), + ('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ)) + local _endNode = self._finder(self, startNode, endNode, toClear) + if _endNode then + return Utils.traceBackPath(self, _endNode, startNode) + end + return nil + end + + --- Resets the `pathfinder`. This function is called internally between + -- successive pathfinding calls, so you should not + -- use it explicitely, unless under specific circumstances. + -- @class function + -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) + -- @usage local path, len = myFinder:getPath(1,1,5,5) + function Pathfinder:reset() + for node in pairs(toClear) do node:reset() end + toClear = {} + return self + end + + -- Returns Pathfinder class + Pathfinder._VERSION = _VERSION + Pathfinder._RELEASEDATE = _RELEASEDATE + return setmetatable(Pathfinder,{ + __call = function(self,...) + return self:new(...) + end + }) +end diff --git a/turtle/apis/jumper/search/astar.lua b/turtle/apis/jumper/search/astar.lua new file mode 100644 index 0000000..c92dabc --- /dev/null +++ b/turtle/apis/jumper/search/astar.lua @@ -0,0 +1,77 @@ +-- Astar algorithm +-- This actual implementation of A-star is based on +-- [Nash A. & al. pseudocode](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/) + +if (...) then + + -- Internalization + local huge = math.huge + + -- Dependancies + local _PATH = (...):match('(.+)%.search.astar$') + local Heap = require (_PATH.. '.core.bheap') + + -- Updates G-cost + local function computeCost(node, neighbour, heuristic) + local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node) + + if node._g + mCost < neighbour._g then + neighbour._parent = node + neighbour._g = node._g + mCost + neighbour.heading = heading + end + end + + -- Updates vertex node-neighbour + local function updateVertex(openList, node, neighbour, endNode, heuristic) + local oldG = neighbour._g + computeCost(node, neighbour, heuristic) + if neighbour._g < oldG then + if neighbour._opened then neighbour._opened = false end + neighbour._h = heuristic(endNode, neighbour) + neighbour._f = neighbour._g + neighbour._h + openList:push(neighbour) + neighbour._opened = true + end + end + + -- Calculates a path. + -- Returns the path from location `` to location ``. + return function (finder, startNode, endNode, toClear) + local openList = Heap() + startNode._g = 0 + startNode._h = finder._heuristic(endNode, startNode) + startNode._f = startNode._g + startNode._h + openList:push(startNode) + toClear[startNode] = true + startNode._opened = true + + while not openList:empty() do + local node = openList:pop() + node._closed = true + if node == endNode then return node end + local neighbours = finder._grid:getNeighbours(node) + for i = 1,#neighbours do + local neighbour = neighbours[i] + if not neighbour._closed then + toClear[neighbour] = true + if not neighbour._opened then + neighbour._g = huge + neighbour._parent = nil + end + updateVertex(openList, node, neighbour, endNode, finder._heuristic) + end + end + + --[[ + printf('x:%d y:%d z:%d g:%d', node.x, node.y, node.z, node._g) + for i = 1,#neighbours do + local n = neighbours[i] + printf('x:%d y:%d z:%d f:%f g:%f h:%d', n.x, n.y, n.z, n._f, n._g, n.heading or -1) + end + --]] + + end + return nil + end +end diff --git a/turtle/apis/pathfind.lua b/turtle/apis/pathfind.lua new file mode 100644 index 0000000..f475418 --- /dev/null +++ b/turtle/apis/pathfind.lua @@ -0,0 +1,256 @@ +local Grid = require('turtle.jumper.grid') +local Pathfinder = require('turtle.jumper.pathfinder') +local Point = require('opus.point') +local Util = require('opus.util') + +local turtle = _G.turtle + +local function addBlock(grid, b, dim) + if Point.inBox(b, dim) then + local node = grid:getNodeAt(b.x, b.y, b.z) + if node then + node.walkable = 1 + end + end +end + +-- map shrinks/grows depending upon blocks encountered +-- the map will encompass any blocks encountered, the turtle position, and the destination +local function mapDimensions(dest, blocks, boundingBox, dests) + local box = Point.makeBox(turtle.point, turtle.point) + + Point.expandBox(box, dest) + + for _,d in pairs(dests) do + Point.expandBox(box, d) + end + + for _,b in pairs(blocks) do + Point.expandBox(box, b) + end + + -- expand one block out in all directions + if boundingBox then + box.x = math.max(box.x - 1, boundingBox.x) + box.z = math.max(box.z - 1, boundingBox.z) + box.y = math.max(box.y - 1, boundingBox.y) + box.ex = math.min(box.ex + 1, boundingBox.ex) + box.ez = math.min(box.ez + 1, boundingBox.ez) + box.ey = math.min(box.ey + 1, boundingBox.ey) + else + box.x = box.x - 1 + box.z = box.z - 1 + box.y = box.y - 1 + box.ex = box.ex + 1 + box.ez = box.ez + 1 + box.ey = box.ey + 1 + end + + return box +end + +local function nodeToPoint(node) + return { x = node.x, y = node.y, z = node.z, heading = node.heading } +end + +local function heuristic(n, node) + return Point.calculateMoves(node, n) +-- { x = node.x, y = node.y, z = node.z, heading = node.heading }, +-- { x = n.x, y = n.y, z = n.z, heading = n.heading }) +end + +local function dimsAreEqual(d1, d2) + return d1.ex == d2.ex and + d1.ey == d2.ey and + d1.ez == d2.ez and + d1.x == d2.x and + d1.y == d2.y and + d1.z == d2.z +end + +-- turtle sensor returns blocks in relation to the world - not turtle orientation +-- so cannot figure out block location unless we know our orientation in the world +-- really kinda dumb since it returns the coordinates as offsets of our location +-- instead of true coordinates +local function addSensorBlocks(blocks, sblocks) + for _,b in pairs(sblocks) do + if b.type ~= 'AIR' then + local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z } + pt.x = pt.x - b.x + pt.z = pt.z - b.z -- this will only work if we were originally facing west + local found = false + for _,ob in pairs(blocks) do + if pt.x == ob.x and pt.y == ob.y and pt.z == ob.z then + found = true + break + end + end + if not found then + table.insert(blocks, pt) + end + end + end +end + +local function selectDestination(pts, box, grid) + while #pts > 0 do + local pt = Point.closest(turtle.point, pts) + if box and not Point.inBox(pt, box) then + Util.removeByValue(pts, pt) + else + if grid:isWalkableAt(pt.x, pt.y, pt.z) then + return pt + end + Util.removeByValue(pts, pt) + end + end +end + +local function updateCanvas(path) + local t = { } + for node in path:nodes() do + table.insert(t, { x = node.x, y = node.y, z = node.z }) + end + os.queueEvent('canvas', { + type = 'canvas_path', + data = t, + }) +end + +local function pathTo(dest, options) + local blocks = options.blocks or turtle.getState().blocks or { } + local dests = options.dest or { dest } -- support alternative destinations + local box = options.box or turtle.getState().box + local lastDim + local grid + + if box then + box = Point.normalizeBox(box) + end + + -- Creates a pathfinder object + local finder = Pathfinder(heuristic) + + while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do + + -- map expands as we encounter obstacles + local dim = mapDimensions(dest, blocks, box, dests) + + -- reuse map if possible + if not lastDim or not dimsAreEqual(dim, lastDim) then + -- Creates a grid object + grid = Grid(dim) + finder:setGrid(grid) + + lastDim = dim + end + for _,b in pairs(blocks) do + addBlock(grid, b, dim) + end + + dest = selectDestination(dests, box, grid) + if not dest then + return false, 'failed to reach destination' + end + if turtle.point.x == dest.x and turtle.point.z == dest.z and turtle.point.y == dest.y then + break + end + + -- Define start and goal locations coordinates + local startPt = turtle.point + + -- Calculates the path, and its length + local path = finder:getPath( + startPt.x, startPt.y, startPt.z, turtle.point.heading, + dest.x, dest.y, dest.z, dest.heading) + + if not path then + Util.removeByValue(dests, dest) + else + updateCanvas(path) + + path:filter() + + for node in path:nodes() do + local pt = nodeToPoint(node) + + if turtle.isAborted() then + return false, 'aborted' + end + +--if this is the next to last node +--and we are traveling up or down, then the +--heading for this node should be the heading of the last node +--or, maybe.. +--if last node is up or down (or either?) + + -- use single turn method so the turtle doesn't turn around + -- when encountering obstacles + --if not turtle.gotoSingleTurn(pt.x, pt.y, pt.z, pt.heading) then + pt.heading = nil + if not turtle.go(pt) then + local bpt = Point.nearestTo(turtle.point, pt) + + if turtle.getFuelLevel() == 0 then + return false, 'Out of fuel' + end + table.insert(blocks, bpt) + os.queueEvent('canvas', { + type = 'canvas_barrier', + data = { bpt }, + }) + -- really need to check if the block we ran into was a turtle. + -- if so, this block should be temporary (1-2 secs) + + --local side = turtle.getSide(turtle.point, pt) + --if turtle.isTurtleAtSide(side) then + -- pt.timestamp = os.clock() + ? + --end + -- if dim has not changed, then need to update grid with + -- walkable = nil (after time has elapsed) + + --if device.turtlesensorenvironment then + -- addSensorBlocks(blocks, device.turtlesensorenvironment.sonicScan()) + --end + break + end + end + end + end + + if dest.heading then + turtle.setHeading(dest.heading) + end + return dest +end + +return { + pathfind = function(dest, options) + options = options or { } + --if not options.blocks and turtle.gotoPoint(dest) then + -- return dest + --end + return pathTo(dest, options) + end, + + -- set a global bounding box + -- box can be overridden by passing box in pathfind options + setBox = function(box) + turtle.getState().box = box + end, + + setBlocks = function(blocks) + turtle.getState().blocks = blocks + end, + + addBlock = function(block) + if turtle.getState().blocks then + table.insert(turtle.getState().blocks, block) + end + end, + + reset = function() + turtle.getState().box = nil + turtle.getState().blocks = nil + end, +} diff --git a/turtle/autorun/6.tl3.lua b/turtle/autorun/6.tl3.lua new file mode 100644 index 0000000..51db8ec --- /dev/null +++ b/turtle/autorun/6.tl3.lua @@ -0,0 +1,1273 @@ +if not _G.turtle then + return +end + +local Pathing = require('turtle.pathfind') +local Point = require('opus.point') +local synchronized = require('opus.sync').sync +local Util = require('opus.util') + +local os = _G.os +local peripheral = _G.peripheral +local turtle = _G.turtle + +local function noop() end +local headings = Point.headings +local state = { } + +turtle.pathfind = Pathing.pathfind +turtle.point = { x = 0, y = 0, z = 0, heading = 0 } + +function turtle.getState() return state end +function turtle.isAborted() return state.abort end +function turtle.getStatus() return state.status end +function turtle.setStatus(s) state.status = s end + +local function _defaultMove(action) + while not action.move() do + if not state.digPolicy(action) and not state.attackPolicy(action) then + return false + end + end + return true +end + +function turtle.getPoint() return turtle.point end +function turtle.setPoint(pt, isGPS) + turtle.point.x = pt.x + turtle.point.y = pt.y + turtle.point.z = pt.z + if pt.heading then + turtle.point.heading = pt.heading + end + turtle.point.gps = isGPS + return true +end + +function turtle.resetState() + state.abort = false + state.status = 'idle' + state.attackPolicy = noop + state.digPolicy = noop + state.movePolicy = _defaultMove + state.moveCallback = noop + state.blacklist = nil + state.reference = nil -- gps reference when converting to relative coords + Pathing.reset() + return true +end + +function turtle.reset() + turtle.point.x = 0 + turtle.point.y = 0 + turtle.point.z = 0 + turtle.point.heading = 0 -- should be facing + turtle.point.gps = false + + turtle.resetState() + return true +end + +local function _dig(name, inspect, dig) + if name then + local s, b = inspect() + if not s or b.name ~= name then + return false + end + end + return dig() +end + +function turtle.dig(s) + return _dig(s, turtle.inspect, turtle.native.dig) +end + +function turtle.digUp(s) + return _dig(s, turtle.inspectUp, turtle.native.digUp) +end + +function turtle.digDown(s) + return _dig(s, turtle.inspectDown, turtle.native.digDown) +end + +local actions = { + up = { + detect = turtle.native.detectUp, + dig = turtle.digUp, + move = turtle.native.up, + attack = turtle.native.attackUp, + place = turtle.native.placeUp, + drop = turtle.native.dropUp, + suck = turtle.native.suckUp, + compare = turtle.native.compareUp, + inspect = turtle.native.inspectUp, + side = 'top' + }, + down = { + detect = turtle.native.detectDown, + dig = turtle.digDown, + move = turtle.native.down, + attack = turtle.native.attackDown, + place = turtle.native.placeDown, + drop = turtle.native.dropDown, + suck = turtle.native.suckDown, + compare = turtle.native.compareDown, + inspect = turtle.native.inspectDown, + side = 'bottom' + }, + forward = { + detect = turtle.native.detect, + dig = turtle.dig, + move = turtle.native.forward, + attack = turtle.native.attack, + place = turtle.native.place, + drop = turtle.native.drop, + suck = turtle.native.suck, + compare = turtle.native.compare, + inspect = turtle.native.inspect, + side = 'front' + }, + back = { + detect = noop, + dig = noop, + move = turtle.native.back, + attack = noop, + place = noop, + suck = noop, + compare = noop, + side = 'back' + }, +} + +function turtle.getAction(direction) + return actions[direction] +end + +function turtle.getHeadingInfo(heading) + heading = heading or turtle.point.heading + return headings[heading] +end + +function turtle.isTurtleAtSide(side) + local sideType = peripheral.getType(side) + return sideType and sideType == 'turtle' +end + +-- [[ Policies ]] -- +turtle.policies = { } + +function turtle.addPolicy(name, policy) + turtle.policies[name] = policy +end + +function turtle.getPolicy(policy) + if type(policy) == 'function' then + return policy + end + local p = turtle.policies[policy] + if not p then + error('Invalid policy: ' .. tostring(policy)) + end + return p +end + +-- [[ Basic turtle actions ]] -- +local function inventoryAction(fn, name, qty) + local slots = turtle.getFilledSlots() + local s + for _,slot in pairs(slots) do + if slot.key == name or slot.name == name then + turtle.native.select(slot.index) + if not qty then + s = fn() + else + s = fn(math.min(qty, slot.count)) + qty = qty - slot.count + if qty < 0 then + break + end + end + end + end + if not s then + return false, 'No items found' + end + return s +end + +-- [[ Attack ]] -- +local function _attack(action) + if action.attack() then + repeat until not action.attack() + return true + end + return false +end + +function turtle.attack() return _attack(actions.forward) end +function turtle.attackUp() return _attack(actions.up) end +function turtle.attackDown() return _attack(actions.down) end + +turtle.addPolicy('attackNone', noop) +turtle.addPolicy('attack', function(action) + return _attack(action) +end) + +function turtle.setAttackPolicy(policy) state.attackPolicy = policy end + +-- [[ Place ]] -- +local function _place(action, indexOrId) + local slot + + if indexOrId then + slot = turtle.getSlot(indexOrId) + if not slot then + return false, 'No items to place' + end + end + + if slot and slot.count == 0 then + return false, 'No items to place' + end + + return Util.tryTimes(3, function() + if slot then + turtle.select(slot.index) + end + local result = { action.place() } + if result[1] then + return true + end + if not state.digPolicy(action) then + state.attackPolicy(action) + end + return table.unpack(result) + end) +end + +function turtle.place(slot) return _place(actions.forward, slot) end +function turtle.placeUp(slot) return _place(actions.up, slot) end +function turtle.placeDown(slot) return _place(actions.down, slot) end + +-- [[ Drop ]] -- +local function _drop(action, qtyOrName, qty) + if not qtyOrName or type(qtyOrName) == 'number' then + return action.drop(qtyOrName or 64) + end + return inventoryAction(action.drop, qtyOrName, qty) +end + +function turtle.drop(count, slot) return _drop(actions.forward, count, slot) end +function turtle.dropUp(count, slot) return _drop(actions.up, count, slot) end +function turtle.dropDown(count, slot) return _drop(actions.down, count, slot) end + +-- [[ Dig ]] -- +turtle.addPolicy('digNone', noop) + +turtle.addPolicy('dig', function(action) + return action.dig() +end) + +turtle.addPolicy('turtleSafe', function(action) + if action.side == 'back' then + return false + end + if not turtle.isTurtleAtSide(action.side) then + return action.dig() + end + return Util.tryTimes(6, function() + os.sleep(.25) + if not action.detect() then + return true + end + end) +end) + +local function isBlacklisted(b) + if b and state.blacklist then + for _, v in pairs(state.blacklist) do + if b.name:find(v) then + return true + end + end + end +end + +turtle.addPolicy('blacklist', function(action) + if action.side == 'back' then + return false + end + local s, m = action.inspect() + if not isBlacklisted(s and m) then + return action.dig() + end + if s and m and m.name:find('turtle') then + return Util.tryTimes(math.random(3, 6), function() + os.sleep(.25) + if not action.detect() then + return true + end + end) + end +end) + +turtle.addPolicy('digAndDrop', function(action) + if action.detect() then + local slots = turtle.getInventory() + if action.dig() then + turtle.reconcileInventory(slots) + return true + end + end + return false +end) + +function turtle.setDigPolicy(policy) state.digPolicy = policy end + +-- [[ Move ]] -- +turtle.addPolicy('moveNone', noop) +turtle.addPolicy('moveDefault', _defaultMove) +turtle.addPolicy('moveAssured', function(action) + if not _defaultMove(action) then + if action.side == 'back' then + return false + end + local oldStatus = state.status + print('assured move: stuck') + state.status = 'stuck' + repeat + os.sleep(1) + until _defaultMove(action) + state.status = oldStatus + end + return true +end) + +function turtle.setMoveCallback(cb) state.moveCallback = cb end +function turtle.clearMoveCallback() state.moveCallback = noop end +function turtle.getMoveCallback() return state.moveCallback end + +-- convenience method for setting multiple values +function turtle.set(args) + for k,v in pairs(args) do + + if k == 'attackPolicy' then + turtle.setAttackPolicy(turtle.getPolicy(v)) + + elseif k == 'digPolicy' then + turtle.setDigPolicy(turtle.getPolicy(v)) + + elseif k == 'movePolicy' then + state.movePolicy = turtle.getPolicy(v) + + elseif k == 'movementStrategy' then + turtle.setMovementStrategy(v) + + elseif k == 'pathingBox' then + turtle.setPathingBox(v) + + elseif k == 'point' then + turtle.setPoint(v) + + elseif k == 'moveCallback' then + turtle.setMoveCallback(v) + + elseif k == 'status' then + turtle.setStatus(v) + + elseif k == 'blacklist' then + state.blacklist = v + + elseif k == 'reference' then + state.reference = v + + else + error('Invalid turle.set: ' .. tostring(k)) + end + end +end + +-- [[ Fuel ]] -- +if type(turtle.getFuelLevel()) ~= 'number' then + -- Support unlimited fuel + function turtle.getFuelLevel() + return 100000 + end +end + +-- override to optionally specify a fuel +function turtle.refuel(qtyOrName, qty) + if not qtyOrName or type(qtyOrName) == 'number' then + return turtle.native.refuel(qtyOrName or 64) + end + return inventoryAction(turtle.native.refuel, qtyOrName, qty or 64) +end + +-- [[ Heading ]] -- +function turtle.getHeading() + return turtle.point.heading +end + +function turtle.turnRight() + turtle.setHeading((turtle.point.heading + 1) % 4) + return turtle.point +end + +function turtle.turnLeft() + turtle.setHeading((turtle.point.heading - 1) % 4) + return turtle.point +end + +function turtle.turnAround() + turtle.setHeading((turtle.point.heading + 2) % 4) + return turtle.point +end + +function turtle.setHeading(heading) + if not heading then + return false, 'Invalid heading' + end + + if heading == turtle.point.heading then + return turtle.point + end + + local fi = Point.facings[heading] + if not fi then + return false, 'Invalid heading' + end + + heading = fi.heading % 4 + if heading ~= turtle.point.heading then + while heading < turtle.point.heading do + heading = heading + 4 + end + if heading - turtle.point.heading == 3 then + turtle.native.turnLeft() + turtle.point.heading = (turtle.point.heading - 1) % 4 + state.moveCallback('turn', turtle.point) + else + local turns = heading - turtle.point.heading + while turns > 0 do + turns = turns - 1 + turtle.native.turnRight() + turtle.point.heading = (turtle.point.heading + 1) % 4 + state.moveCallback('turn', turtle.point) + end + end + end + + return turtle.point +end + +function turtle.headTowardsX(dx) + if turtle.point.x ~= dx then + if turtle.point.x > dx then + turtle.setHeading(2) + else + turtle.setHeading(0) + end + end +end + +function turtle.headTowardsZ(dz) + if turtle.point.z ~= dz then + if turtle.point.z > dz then + turtle.setHeading(3) + else + turtle.setHeading(1) + end + end +end + +function turtle.headTowards(pt) + local xd = math.abs(turtle.point.x - pt.x) + local zd = math.abs(turtle.point.z - pt.z) + if xd > zd then + turtle.headTowardsX(pt.x) + else + turtle.headTowardsZ(pt.z) + end +end + +-- [[ move ]] -- +function turtle.up() + if state.movePolicy(actions.up) then + turtle.point.y = turtle.point.y + 1 + state.moveCallback('up', turtle.point) + return true, turtle.point + end +end + +function turtle.down() + if state.movePolicy(actions.down) then + turtle.point.y = turtle.point.y - 1 + state.moveCallback('down', turtle.point) + return true, turtle.point + end +end + +function turtle.forward() + if state.movePolicy(actions.forward) then + turtle.point.x = turtle.point.x + headings[turtle.point.heading].xd + turtle.point.z = turtle.point.z + headings[turtle.point.heading].zd + state.moveCallback('forward', turtle.point) + return true, turtle.point + end +end + +function turtle.back() + if state.movePolicy(actions.back) then + turtle.point.x = turtle.point.x - headings[turtle.point.heading].xd + turtle.point.z = turtle.point.z - headings[turtle.point.heading].zd + state.moveCallback('back', turtle.point) + return true, turtle.point + end +end + +local function moveTowardsX(dx) + if not tonumber(dx) then error('moveTowardsX: Invalid arguments') end + local direction = dx - turtle.point.x + local move + + if direction == 0 then + return true + end + + if direction > 0 and turtle.point.heading == 0 or + direction < 0 and turtle.point.heading == 2 then + move = turtle.forward + else + move = turtle.back + end + + repeat + if not move() then + return false + end + until turtle.point.x == dx + return true +end + +local function moveTowardsZ(dz) + local direction = dz - turtle.point.z + local move + + if direction == 0 then + return true + end + + if direction > 0 and turtle.point.heading == 1 or + direction < 0 and turtle.point.heading == 3 then + move = turtle.forward + else + move = turtle.back + end + + repeat + if not move() then + return false + end + until turtle.point.z == dz + return true +end + +-- [[ go ]] -- +-- 1 turn goto (going backwards if possible) +function turtle.gotoSingleTurn(dx, dy, dz, dh) + dx = dx or turtle.point.x + dy = dy or turtle.point.y + dz = dz or turtle.point.z + + local function gx() + if turtle.point.x ~= dx then + moveTowardsX(dx) + end + if turtle.point.z ~= dz then + if dh and dh % 2 == 1 then + turtle.setHeading(dh) + else + turtle.headTowardsZ(dz) + end + end + end + + local function gz() + if turtle.point.z ~= dz then + moveTowardsZ(dz) + end + if turtle.point.x ~= dx then + if dh and dh % 2 == 0 then + turtle.setHeading(dh) + else + turtle.headTowardsX(dx) + end + end + end + + repeat + local x, z + local y = turtle.point.y + + repeat + x, z = turtle.point.x, turtle.point.z + + if turtle.point.heading % 2 == 0 then + gx() + gz() + else + gz() + gx() + end + until x == turtle.point.x and z == turtle.point.z + + if turtle.point.y ~= dy then + turtle.gotoY(dy) + end + + if turtle.point.x == dx and turtle.point.z == dz and turtle.point.y == dy then + return true + end + + until x == turtle.point.x and z == turtle.point.z and y == turtle.point.y + + return false +end + +local function gotoEx(dx, dy, dz) + -- determine the heading to ensure the least amount of turns + -- first check is 1 turn needed - remaining require 2 turns + if turtle.point.heading == 0 and turtle.point.x <= dx or + turtle.point.heading == 2 and turtle.point.x >= dx or + turtle.point.heading == 1 and turtle.point.z <= dz or + turtle.point.heading == 3 and turtle.point.z >= dz then + -- maintain current heading + -- nop + elseif dz > turtle.point.z and turtle.point.heading == 0 or + dz < turtle.point.z and turtle.point.heading == 2 or + dx < turtle.point.x and turtle.point.heading == 1 or + dx > turtle.point.x and turtle.point.heading == 3 then + turtle.turnRight() + else + turtle.turnLeft() + end + + if (turtle.point.heading % 2) == 1 then + if not turtle.gotoZ(dz) then return false end + if not turtle.gotoX(dx) then return false end + else + if not turtle.gotoX(dx) then return false end + if not turtle.gotoZ(dz) then return false end + end + + if dy then + if not turtle.gotoY(dy) then return false end + end + + return true +end + +-- fallback goto - will turn around if was previously moving backwards +local function gotoMultiTurn(dx, dy, dz) + if gotoEx(dx, dy, dz) then + return true + end + + local moved + repeat + local x, y, z = turtle.point.x, turtle.point.y, turtle.point.z + + -- try going the other way + if (turtle.point.heading % 2) == 1 then + turtle.headTowardsX(dx) + else + turtle.headTowardsZ(dz) + end + + if gotoEx(dx, dy, dz) then + return true + end + + if dy then + turtle.gotoY(dy) + end + + moved = x ~= turtle.point.x or y ~= turtle.point.y or z ~= turtle.point.z + until not moved + + return false +end + +-- go backwards - turning around if necessary to fight mobs / break blocks +function turtle.goback() + local hi = headings[turtle.point.heading] + return turtle.go({ + x = turtle.point.x - hi.xd, + y = turtle.point.y, + z = turtle.point.z - hi.zd, + heading = turtle.point.heading, + }) +end + +function turtle.gotoYfirst(pt) + if turtle.gotoY(pt.y) then + if turtle.go(pt) then + turtle.setHeading(pt.heading) + return true + end + end +end + +function turtle.go(pt) + if not pt.x and not pt.z and pt.y then + if turtle.gotoY(pt.y) then + turtle.setHeading(pt.heading) + return true + end + return false, 'Failed to reach location' + end + + local dx = pt.x or turtle.point.x + local dz = pt.z or turtle.point.z + local dy, dh = pt.y, pt.heading + if not turtle.gotoSingleTurn(dx, dy, dz, dh) then + if not gotoMultiTurn(dx, dy, dz) then + return false, 'Failed to reach location' + end + end + turtle.setHeading(dh) + return pt +end + +-- avoid lint errors +-- deprecated +turtle['goto'] = turtle.go +turtle['_goto'] = turtle.go + +-- TODO: localize these goto functions +function turtle.gotoX(dx) + turtle.headTowardsX(dx) + + while turtle.point.x ~= dx do + if not turtle.forward() then + return false + end + end + return true +end + +function turtle.gotoZ(dz) + turtle.headTowardsZ(dz) + + while turtle.point.z ~= dz do + if not turtle.forward() then + return false + end + end + return true +end + +function turtle.gotoY(dy) + while turtle.point.y > dy do + if not turtle.down() then + return false + end + end + + while turtle.point.y < dy do + if not turtle.up() then + return false + end + end + return true +end + +-- [[ Inventory ]] -- +function turtle.getSlot(indexOrId, slots) + if type(indexOrId) == 'string' then + slots = slots or turtle.getInventory() + local _,c = string.gsub(indexOrId, ':', '') + if c == 2 then -- combined id and dmg .. ie. minecraft:coal:0 + return Util.find(slots, 'key', indexOrId) + end + return Util.find(slots, 'name', indexOrId) + end + + local detail = turtle.getItemDetail(indexOrId) + if detail then + return { + name = detail.name, + damage = detail.damage, + count = detail.count, + key = detail.name .. ':' .. detail.damage, + + index = indexOrId, + + -- deprecate + qty = detail.count, + dmg = detail.damage, + id = detail.name, + } + end + + -- inconsistent return value + -- null is returned if indexOrId is a string and no item is present + return { + qty = 0, -- deprecate + count = 0, + index = indexOrId, + } +end + +function turtle.select(indexOrId) + if type(indexOrId) == 'number' then + return turtle.native.select(indexOrId) + end + + local s = turtle.getSlot(indexOrId) + if s then + turtle.native.select(s.index) + return s + end + + return false, 'Inventory does not contain item' +end + +function turtle.getInventory(slots) + slots = slots or { } + for i = 1, 16 do + slots[i] = turtle.getSlot(i) + end + return slots +end + +function turtle.getSummedInventory() + local slots = turtle.getFilledSlots() + local t = { } + for _,slot in pairs(slots) do + local entry = t[slot.key] + if not entry then + entry = { + count = 0, + damage = slot.damage, + name = slot.name, + key = slot.key, + + -- deprecate + qty = 0, + dmg = slot.dmg, + id = slot.id, + } + t[slot.key] = entry + end + entry.qty = entry.qty + slot.qty + entry.count = entry.qty + end + return t +end + +function turtle.has(item, count) + if item:match('.*:%d') then + local slot = turtle.getSummedInventory()[item] + return slot and slot.count >= (count or 1) + end + local slot = turtle.getSlot(item) + return slot and slot.count >= (count or 1) +end + +function turtle.getFilledSlots(startSlot) + startSlot = startSlot or 1 + + local slots = { } + for i = startSlot, 16 do + local count = turtle.getItemCount(i) + if count > 0 then + slots[i] = turtle.getSlot(i) + end + end + return slots +end + +function turtle.eachFilledSlot(fn) + local slots = turtle.getFilledSlots() + for _,slot in pairs(slots) do + fn(slot) + end +end + +function turtle.emptyInventory(dropAction) + dropAction = dropAction or turtle.native.drop + turtle.eachFilledSlot(function(slot) + turtle.select(slot.index) + dropAction() + end) + turtle.select(1) +end + +function turtle.reconcileInventory(slots, dropAction) + dropAction = dropAction or turtle.native.drop + for _,s in pairs(slots) do + local qty = turtle.getItemCount(s.index) + if qty > s.qty then + turtle.select(s.index) + dropAction(qty-s.qty, s) + end + end +end + +function turtle.selectSlotWithItems(startSlot) + startSlot = startSlot or 1 + for i = startSlot, 16 do + if turtle.getItemCount(i) > 0 then + turtle.select(i) + return i + end + end +end + +function turtle.selectSlotWithQuantity(qty, startSlot) + startSlot = startSlot or 1 + + for i = startSlot, 16 do + if turtle.getItemCount(i) == qty then + turtle.select(i) + return i + end + end +end + +function turtle.selectOpenSlot(startSlot) + return turtle.selectSlotWithQuantity(0, startSlot) +end + +function turtle.condense() + local slots = turtle.getInventory() + + for i = 1, 16 do + if slots[i].count < 64 then + for j = 16, i + 1, -1 do + if slots[j].count > 0 and (slots[i].count == 0 or slots[i].key == slots[j].key) then + turtle.select(j) + if turtle.transferTo(i) then + local transferred = turtle.getItemCount(i) - slots[i].count + slots[j].count = slots[j].count - transferred + slots[i].count = slots[i].count + transferred + slots[i].key = slots[j].key + if slots[j].count == 0 then + slots[j].key = nil + end + if slots[i].count == 64 then + break + end + else + break + end + end + end + end + end + turtle.select(1) + return true +end + +function turtle.getItemCount(idOrName) + if type(idOrName) == 'number' then + return turtle.native.getItemCount(idOrName) + end + local slots = turtle.getFilledSlots() + local count = 0 + for _,slot in pairs(slots) do + if slot.key == idOrName or slot.name == idOrName then + count = count + slot.count + end + end + return count +end + +-- [[ Equipment ]] -- +function turtle.equip(side, item) + if item then + if not turtle.select(item) then + return false, 'Unable to equip ' .. item + end + end + + if side == 'left' then + return turtle.equipLeft() + end + return turtle.equipRight() +end + +function turtle.isEquipped(item) + if peripheral.getType('left') == item then + return 'left' + elseif peripheral.getType('right') == item then + return 'right' + end +end + +function turtle.unequip(side) + if not turtle.selectSlotWithQuantity(0) then + return false, 'No slots available' + end + return turtle.equip(side) +end + +-- deprecate +function turtle.run(fn, ...) + local args = { ... } + local s, m + + if type(fn) == 'string' then + fn = turtle[fn] + end + + synchronized(turtle, function() + turtle.resetState() + s, m = pcall(function() fn(table.unpack(args)) end) + turtle.resetState() + if not s and m then + _G.printError(m) + end + end) + + return s, m +end + +function turtle.abort(abort) + state.abort = abort + if abort then + os.queueEvent('turtle_abort') + end +end + +-- [[ Pathing ]] -- +function turtle.setPersistent(isPersistent) + if isPersistent then + Pathing.setBlocks({ }) + else + Pathing.setBlocks() + end +end + +function turtle.setPathingBox(box) + Pathing.setBox(box) +end + +function turtle.addWorldBlock(pt) + Pathing.addBlock(pt) +end + +function turtle.addWorldBlocks(pts) + Util.each(pts, function(pt) + Pathing.addBlock(pt) + end) +end + +local movementStrategy = turtle.pathfind + +function turtle.setMovementStrategy(strategy) + if strategy == 'pathing' then + movementStrategy = turtle.pathfind + elseif strategy == 'goto' then + movementStrategy = turtle.go + else + error('Invalid movement strategy') + end +end + +function turtle.faceAgainst(pt, options) -- 4 sided + options = options or { } + options.dest = { } + + for i = 0, 3 do + local hi = Point.facings[i] + table.insert(options.dest, { + x = pt.x + hi.xd, + z = pt.z + hi.zd, + y = pt.y + hi.yd, + heading = (hi.heading + 2) % 4, + }) + end + + return movementStrategy(Point.closest(turtle.point, options.dest), options) +end + +-- move against this point +-- if the point does not contain a heading, then the turtle +-- will face the block (if on same plane) +-- if above or below, the heading is undetermined unless specified +function turtle.moveAgainst(pt, options) -- 6 sided + options = options or { } + options.dest = { } + + for i = 0, 5 do + local hi = turtle.getHeadingInfo(i) + local heading, direction + if i < 4 then + heading = (hi.heading + 2) % 4 + direction = 'forward' + elseif i == 4 then + direction = 'down' + elseif i == 5 then + direction = 'up' + end + + table.insert(options.dest, { + x = pt.x + hi.xd, + z = pt.z + hi.zd, + y = pt.y + hi.yd, + direction = direction, + heading = pt.heading or heading, + }) + end + + return movementStrategy(Point.closest(turtle.point, options.dest), options) +end + +local actionsAt = { + detect = { + up = turtle.detectUp, + down = turtle.detectDown, + forward = turtle.detect, + }, + dig = { + up = turtle.digUp, + down = turtle.digDown, + forward = turtle.dig, + }, + move = { + up = turtle.moveUp, + down = turtle.moveDown, + forward = turtle.move, + }, + attack = { + up = turtle.attackUp, + down = turtle.attackDown, + forward = turtle.attack, + }, + place = { + up = turtle.placeUp, + down = turtle.placeDown, + forward = turtle.place, + }, + drop = { + up = turtle.dropUp, + down = turtle.dropDown, + forward = turtle.drop, + }, + suck = { + up = turtle.suckUp, + down = turtle.suckDown, + forward = turtle.suck, + }, + compare = { + up = turtle.compareUp, + down = turtle.compareDown, + forward = turtle.compare, + }, + inspect = { + up = turtle.inspectUp, + down = turtle.inspectDown, + forward = turtle.inspect, + }, +} + +-- pt = { x,y,z,heading,direction } +-- direction should only be up or down if provided +-- heading can be provided to tell which way to face during action +-- ex: place a block at the point from above facing east +local function _actionAt(action, pt, ...) + if not pt.heading and not pt.direction then + local msg + pt, msg = turtle.moveAgainst(pt) + if pt then + return action[pt.direction](...) + end + return pt, msg + end + + local reversed = + { [0] = 2, [1] = 3, [2] = 0, [3] = 1, [4] = 5, [5] = 4, } + local dir = reversed[headings[pt.direction or pt.heading].heading] + local apt = { x = pt.x + headings[dir].xd, + y = pt.y + headings[dir].yd, + z = pt.z + headings[dir].zd, } + local direction + + -- ex: place a block at this point, from above, facing east + if dir < 4 then + apt.heading = (dir + 2) % 4 + direction = 'forward' + elseif dir == 4 then + apt.heading = pt.heading + direction = 'down' + elseif dir == 5 then + apt.heading = pt.heading + direction = 'up' + end + + if movementStrategy(apt) then + return action[direction](...) + end +end + +local function _actionDownAt(action, pt, ...) + pt = Util.shallowCopy(pt) + pt.direction = Point.DOWN + return _actionAt(action, pt, ...) +end + +local function _actionUpAt(action, pt, ...) + pt = Util.shallowCopy(pt) + pt.direction = Point.UP + return _actionAt(action, pt, ...) +end + +local function _actionForwardAt(action, pt, ...) + if turtle.faceAgainst(pt) then + return action.forward(...) + end +end + +function turtle.detectAt(pt) return _actionAt(actionsAt.detect, pt) end +function turtle.detectDownAt(pt) return _actionDownAt(actionsAt.detect, pt) end +function turtle.detectForwardAt(pt) return _actionForwardAt(actionsAt.detect, pt) end +function turtle.detectUpAt(pt) return _actionUpAt(actionsAt.detect, pt) end + +function turtle.digAt(pt, ...) return _actionAt(actionsAt.dig, pt, ...) end +function turtle.digDownAt(pt, ...) return _actionDownAt(actionsAt.dig, pt, ...) end +function turtle.digForwardAt(pt, ...) return _actionForwardAt(actionsAt.dig, pt, ...) end +function turtle.digUpAt(pt, ...) return _actionUpAt(actionsAt.dig, pt, ...) end + +function turtle.attackAt(pt) return _actionAt(actionsAt.attack, pt) end +function turtle.attackDownAt(pt) return _actionDownAt(actionsAt.attack, pt) end +function turtle.attackForwardAt(pt) return _actionForwardAt(actionsAt.attack, pt) end +function turtle.attackUpAt(pt) return _actionUpAt(actionsAt.attack, pt) end + +function turtle.placeAt(pt, arg, dir) return _actionAt(actionsAt.place, pt, arg, dir) end +function turtle.placeDownAt(pt, arg) return _actionDownAt(actionsAt.place, pt, arg) end +function turtle.placeForwardAt(pt, arg) return _actionForwardAt(actionsAt.place, pt, arg) end +function turtle.placeUpAt(pt, arg) return _actionUpAt(actionsAt.place, pt, arg) end + +function turtle.dropAt(pt, ...) return _actionAt(actionsAt.drop, pt, ...) end +function turtle.dropDownAt(pt, ...) return _actionDownAt(actionsAt.drop, pt, ...) end +function turtle.dropForwardAt(pt, ...) return _actionForwardAt(actionsAt.drop, pt, ...) end +function turtle.dropUpAt(pt, ...) return _actionUpAt(actionsAt.drop, pt, ...) end + +function turtle.suckAt(pt, qty) return _actionAt(actionsAt.suck, pt, qty or 64) end +function turtle.suckDownAt(pt, qty) return _actionDownAt(actionsAt.suck, pt, qty or 64) end +function turtle.suckForwardAt(pt, qty) return _actionForwardAt(actionsAt.suck, pt, qty or 64) end +function turtle.suckUpAt(pt, qty) return _actionUpAt(actionsAt.suck, pt, qty or 64) end + +function turtle.compareAt(pt) return _actionAt(actionsAt.compare, pt) end +function turtle.compareDownAt(pt) return _actionDownAt(actionsAt.compare, pt) end +function turtle.compareForwardAt(pt) return _actionForwardAt(actionsAt.compare, pt) end +function turtle.compareUpAt(pt) return _actionUpAt(actionsAt.compare, pt) end + +function turtle.inspectAt(pt) return _actionAt(actionsAt.inspect, pt) end +function turtle.inspectDownAt(pt) return _actionDownAt(actionsAt.inspect, pt) end +function turtle.inspectForwardAt(pt) return _actionForwardAt(actionsAt.inspect, pt) end +function turtle.inspectUpAt(pt) return _actionUpAt(actionsAt.inspect, pt) end + +turtle.reset() diff --git a/common/canvasClient.lua b/turtle/canvasClient.lua similarity index 100% rename from common/canvasClient.lua rename to turtle/canvasClient.lua