--[[ Branch mining program for OpenComputers robots. This program is designed to dig out branches, in a fashion that allows players to easily navigate the dug out tunnels. The primary concern was not the performance of the mining, only a good detection rate, and nice tunnels. Suggested upgrades for this are the geolyzer and inventory controller upgrade, and depending on your world gen (ravines?) a hover upgrade might be necessary. The rest is up to you (chunkloading, more inventory, battery upgrades). By Sangar, 2015 This program is licensed under the MIT license. http://opensource.org/licenses/mit-license.php ]] local component = require("component") local computer = require("computer") local robot = require("robot") local shell = require("shell") local sides = require("sides") local event = require("event") local args, options = shell.parse(...) --[[ Config ]]----------------------------------------------------------------- -- Every how many blocks to dig a side shaft. The default makes for a -- two wide wall between tunnels, making sure we don't miss anything. local shaftInterval = 3 -- Max recursion level for mining ore veins. We abort early because we -- assume we'll encounter the same vein again from an adjacent tunnel. local maxVeinRecursion = 8 -- Every how many blocks to place a torch when placing torches. local torchInverval = 11 --[[ Constants ]]-------------------------------------------------------------- -- Quick look-up table for inverting directions. local oppositeSides = { [sides.north] = sides.south, [sides.south] = sides.north, [sides.east] = sides.west, [sides.west] = sides.east, [sides.up] = sides.down, [sides.down] = sides.up } -- For pushTurn() readability. local left = false local right = not left --[[ State ]]------------------------------------------------------------------ -- Slots we don't want to drop. Filled in during initialization, based -- on items already in the inventory. Useful for stuff like /dev/null. local keepSlot = {} -- Slots that we keep torches in, updated when stocking up on torches. local torchSlots = {} --[[ "Passive" logic ]]-------------------------------------------------------- -- Keep track of moves we're away from our origin, and average energy used per -- move. This is used to compute the threshold at which we have to return to -- maintenance to recharge. local preMoveEnergy, averageMoveCost, distanceToOrigin = 0, 15, 0 -- The actual callback called in postMove(). local onMove -- Called whenever we're about to move, used to compute move cost. local function preMove() preMoveEnergy = computer.energy() end -- Called whenever we're done moving, used for automatic torch placement an digging. local function postMove() local moveCost = preMoveEnergy - computer.energy() if moveCost > 0 then averageMoveCost = (averageMoveCost + moveCost) / 2 end if onMove then onMove() end end --[[ Utility ]]---------------------------------------------------------------- local function prompt(message) io.write(message .. " [Y/n] ") local result = io.read() return result and (result == "" or result:lower() == "y") end -- Check if a block with the specified info should be mined. local function shouldMine(info) return info and info.name and (info.name:match(".*ore.*") or info.name:match(".*Ore.*")) end -- Number of stacks of torches to keep; default is 1 per inventory upgrade. local function torchStacks() return math.max(1, math.ceil(robot.inventorySize() / 16)) end -- Look for the first empty slot in our inventory. local function findEmptySlot() for slot = 1, robot.inventorySize() do if robot.count(slot) == 0 then return slot end end end -- Find the first torch slot that still contains torches. local function findTorchSlot() for _, slot in ipairs(torchSlots) do if robot.count(slot) > 0 then return slot end end end -- Since robot.select() is an indirect call, we can speed things up a bit. local selectedSlot local function cachedSelect(slot) if slot ~= selectedSlot then robot.select(slot) selectedSlot = slot end end -- Place a single torch above the robot, if there are any torches left. local function placeTorch() local slot = findTorchSlot() local result = false if slot then cachedSelect(slot) result = robot.placeUp() cachedSelect(1) end return result end -- Dig out a block on the specified side, without tool if possible. local function dig(side, callback) repeat -- Check for maintenance first, to make sure we make the return trip when -- our batteries are running low. local emptySlot = findEmptySlot() if callback then callback(not emptySlot) -- Parameter: is inventory full. emptySlot = findEmptySlot() end cachedSelect(1) local something, what = component.robot.detect(side) if not something or what == "replaceable" or what == "liquid" then return true -- We can just move into whatever is there. end local brokeSomething local info = component.isAvailable("geolyzer") and component.geolyzer.analyze(side) if info and info.name == "OpenComputers:robot" then brokeSomething = true -- Wait for other robot to go away. os.sleep(0.5) elseif component.isAvailable("inventory_controller") and emptySlot then cachedSelect(emptySlot) component.inventory_controller.equip() -- Save some tool durability. cachedSelect(1) brokeSomething = component.robot.swing(side) cachedSelect(emptySlot) component.inventory_controller.equip() cachedSelect(1) end if not brokeSomething then brokeSomething = component.robot.swing(side) end until not brokeSomething end -- Force a move towards in the specified direction. local function forceMove(side, delta) preMove() local result = component.robot.move(side) if result then distanceToOrigin = distanceToOrigin + delta postMove() else -- Obstructed, try to clear the way. if side == sides.back then -- Moving backwards, turn around. component.robot.turn(left) component.robot.turn(left) repeat dig(sides.forward) preMove() until robot.forward() distanceToOrigin = distanceToOrigin + delta component.robot.turn(left) component.robot.turn(left) postMove() -- Slightly falsifies move cost, but must ensure we're rotated -- correctly in case postMove() triggers going to maintenance. else repeat dig(side) preMove() until component.robot.move(side) distanceToOrigin = distanceToOrigin + delta postMove() end end return true end --[[ Navigation ]]------------------------------------------------------------- -- Keeps track of our moves to allow "undoing" them for returning to the -- docking station. Format is a list of moves, represented as tables -- containing the type of move and distance to move, e.g. -- {move=sides.back, count=10}, -- {turn=true, count=2} -- means we first moved back 10 blocks, then turned around. local moves = {} -- Undo a *single* move, i.e. reduce the count of the latest move type. local function undoMove(move) if move.move then local side = oppositeSides[move.move] forceMove(side, -1) else local direction = not move.turn component.robot.turn(direction) end move.count = move.count - 1 end -- Make a turn in the specified direction. local function pushTurn(direction) component.robot.turn(direction) if moves[#moves] and moves[#moves].turn == direction then moves[#moves].count = moves[#moves].count + 1 else moves[#moves + 1] = {turn=direction, count=1} end return true -- Allows for `return pushMove() and pushTurn() and pushMove()`. end -- Try to make a move towards the specified side. local function pushMove(side, force) preMove() local result, reason = (force and forceMove or component.robot.move)(side, 1) if result then if moves[#moves] and moves[#moves].move == side then moves[#moves].count = moves[#moves].count + 1 else moves[#moves + 1] = {move=side, count=1} end if not force then distanceToOrigin = distanceToOrigin + 1 end postMove() end return result, reason end -- Undo the most recent move *type*. I.e. will undo all moves of the most -- recent type (say we moved forwards twice, this will go back twice). local function popMove() -- Deep copy the move for returning it. local move = moves[#moves] and {move=moves[#moves].move, turn=moves[#moves].turn, count=moves[#moves].count} while moves[#moves] and moves[#moves].count > 0 do undoMove(moves[#moves]) end moves[#moves] = nil return move end -- Get the current top and count values, to be used as a position snapshot -- that can be restored later on by calling setTop(). local function getTop() if moves[#moves] then return #moves, moves[#moves].count else return 0, 0 end end -- Undo some moves based on a stored top and count received from getTop(). local function setTop(top, count, unsafe) assert(top >= 0) assert(top <= #moves) assert(count >= 0) assert(top < #moves or count <= moves[#moves].count) while #moves > top do if unsafe then if moves[#moves].move then distanceToOrigin = distanceToOrigin - moves[#moves].count end moves[#moves] = nil else popMove() end end local move = moves[#moves] if move then while move.count > count do if unsafe then move.count = move.count - 1 distanceToOrigin = distanceToOrigin - 1 else undoMove(move) end end if move.count < 1 then moves[#moves] = nil end end end -- Undo *all* moves made since program start, return the list of moves. local function popMoves() local result = {} local move = popMove() while move do table.insert(result, 1, move) move = popMove() end return result end -- Repeat the specified set of moves. local function pushMoves(moves) for _, move in ipairs(moves) do if move.move then for _ = 1, move.count do pushMove(move.move, true) end else for _ = 1, move.count do pushTurn(move.turn) end end end end --[[ Maintenance ]]------------------------------------------------------------ -- Energy required to return to docking bay. local function costToReturn() -- Overestimate a bit, to account for obstacles such as gravel or mobs. return 5000 + averageMoveCost * distanceToOrigin * 1.25 end -- Checks whether we need maintenance. local function needsMaintenance() return not robot.durability() or -- Tool broken? computer.energy() < costToReturn() or -- Out of juice? not findTorchSlot() -- No more torches? end -- Drops all inventory contents that are not marked for keeping. local function dropMinedBlocks() if component.isAvailable("inventory_controller") then if not component.inventory_controller.getInventorySize(sides.down) then io.write("There doesn't seem to be an inventory below me! Waiting to avoid spilling stuffs into the world.\n") end repeat os.sleep(5) until component.inventory_controller.getInventorySize(sides.down) end io.write("Dropping what I found.\n") for slot = 1, robot.inventorySize() do while not keepSlot[slot] and robot.count(slot) > 0 do cachedSelect(slot) robot.dropDown() end end cachedSelect(1) end -- Ensures we have a tool with durability. local function checkTool() if not robot.durability() then io.write("Tool is broken, getting a new one.\n") if component.isAvailable("inventory_controller") then cachedSelect(findEmptySlot()) -- Select an empty slot for working. repeat component.inventory_controller.equip() -- Drop whatever's in the tool slot. while robot.count() > 0 do robot.dropDown() end robot.suckUp(1) -- Pull something from above and equip it. component.inventory_controller.equip() until robot.durability() cachedSelect(1) else -- Can't re-equip autonomously, wait for player to give us a tool. io.write("HALP! I need a new tool.\n") repeat event.pull(10, "inventory_changed") until robot.durability() end end end -- Ensures we have some torches. local function checkTorches() -- First, clean up our list and look for empty slots. io.write("Getting my fill of torches.\n") local oldTorchSlots = torchSlots torchSlots = {} for _, slot in ipairs(oldTorchSlots) do keepSlot[slot] = nil if robot.count(slot) > 0 then torchSlots[#torchSlots + 1] = slot end end while #torchSlots < torchStacks() do local slot = findEmptySlot() if not slot then break -- This should never happen... end torchSlots[#torchSlots + 1] = slot end -- Then fill the slots with torches. robot.turnLeft() for _, slot in ipairs(torchSlots) do keepSlot[slot] = true if robot.space(slot) > 0 then cachedSelect(slot) repeat local before = robot.space() robot.suck(robot.space()) if robot.space() == before then os.sleep(5) -- Don't busy idle. end until robot.space() < 1 cachedSelect(1) end end robot.turnRight() end -- Recharge our batteries. local function recharge() io.write("Waiting until my batteries are full.\n") while computer.maxEnergy() - computer.energy() > 100 do os.sleep(1) end end -- Go back to the docking bay for general maintenance if necessary. local function gotoMaintenance(force) if not force and not needsMaintenance() then return -- No need yet. end -- Save some values for later, temporarily remove onMove callback. local returnCost = costToReturn() local moveCallback = onMove onMove = nil local top, count = getTop() io.write("Going back for maintenance!\n") local moves = popMoves() assert(distanceToOrigin == 0) dropMinedBlocks() checkTool() checkTorches() recharge() -- Last so we can charge some during the other operations. if moves and #moves > 0 then if returnCost * 2 > computer.maxEnergy() and not options.f and not prompt("Going back will cost me half my energy. There's a good chance I will not return. Do you want to send me to my doom anyway?") then os.exit() end io.write("Returning to where I left off.\n") pushMoves(moves) end local newTop, newCount = getTop() assert(top == newTop) assert(count == newCount) onMove = moveCallback end --[[ Mining ]]----------------------------------------------------------------- -- Move towards the specified direction, digging out blocks as necessary. -- This is a "soft" version of forceMove in that it will try to clear its path, -- but fail if it can't. local function move(side) local result, reason, retry repeat retry = false if side ~= sides.back then retry = dig(side, gotoMaintenance) else gotoMaintenance() end result, reason = pushMove(side) until result or not retry return result, reason end -- Turn to face the specified, relative orientation. local function turnTowards(side) if side == sides.left then pushTurn(left) elseif side == sides.right then pushTurn(right) elseif side == sides.back then pushTurn(left) pushTurn(left) end end --[[ On move callbacks ]]------------------------------------------------------ -- Start automatically placing torches in the configured interval. local function beginPlacingTorches() local counter = 2 onMove = function() if counter < 1 then if placeTorch() then counter = torchInverval end else counter = counter - 1 end end end -- Start digging out the block below us after each move. local function beginDigginTrench() onMove = function() dig(sides.down, gotoMaintenance) end end -- Stop automatically placing torches. local function clearMoveCallback() onMove = nil end --[[ Moving ]]----------------------------------------------------------------- -- Dig out any interesting ores adjacent to the current position, recursively. -- POST: back to the starting position and facing. local function digVein(maxDepth) if maxDepth < 1 then return end for _, side in ipairs(sides) do local sideIdx = sides[side] -- skip unknown side if oppositeSides[side] and shouldMine(component.geolyzer.analyze(sideIdx)) then local top, count = getTop() turnTowards(sideIdx) if sideIdx == sides.up or sideIdx == sides.down then move(sideIdx) else move(sides.forward) end digVein(maxDepth - 1) setTop(top, count) end end end -- Dig out any interesting ores adjacent to the current position, recursively. -- Also checks blocks adjacent to above block in exhaustive mode. -- POST: back at the starting position and facing. local function digVeins(exhaustive) if component.isAvailable("geolyzer") then digVein(maxVeinRecursion) if exhaustive and move(sides.up) then digVein(maxVeinRecursion) popMove() end end end -- Dig a 1x2 tunnel of the specified length. Checks for ores. -- Also checks upper row for ores in exhaustive mode. -- PRE: bottom front of tunnel to dig. -- POST: at the end of the tunnel. local function dig1x2(length, exhaustive) while length > 0 and move(sides.forward) do dig(sides.up, gotoMaintenance) digVeins(exhaustive) length = length - 1 end return length < 1 end -- Dig a 1x3 tunnel of the specified length. -- PRE: center front of tunnel to dig. -- POST: at the end of the tunnel. local function dig1x3(length) while length > 0 and move(sides.forward) do dig(sides.up, gotoMaintenance) dig(sides.down, gotoMaintenance) length = length - 1 end return length < 1 end -- Dig out a main shaft. -- PRE: bottom front of main shaft. -- POST: bottom front of main shaft. local function digMainShaft(length) io.write("Digging main shaft.\n") if not move(sides.up) then return false end local top, count = getTop() if not (dig1x3(length) and pushTurn(left) and dig1x3(1) and pushTurn(left) and dig1x3(length - 1) and (placeTorch() or true) and -- Just keep going... pushTurn(left) and dig1x3(1)) then return false end -- Create snapshot for shortcut below. local midTop, midCount = getTop() if not (dig1x3(1) and pushTurn(left) and dig1x3(length - 1)) then return false end placeTorch() -- Shortcut: manually move back to start, do an unsafe setTop. -- Otherwise we'd have to retrace all three rows. setTop(midTop, midCount) if pushTurn(left) and move(sides.back) then setTop(top, count, true) return true end return false end -- Dig all shafts in one cardinal direction (the one we're facing). -- PRE: bottom front of a main shaft. -- POST: bottom front of a main shaft. local function digShafts(length) local top, count = getTop() -- Remember start of main shaft. local ok = digMainShaft(length) setTop(top, count) if not ok then io.write("Failed digging main shaft, skipping.\n") return end io.write("Beginning work on side shafts.\n") for i = shaftInterval, length, shaftInterval do io.write("Working on shafts #" .. (i / shaftInterval) .. ".\n") if not dig1x2(shaftInterval) then -- Move to height of shaft. break end local sideTop, sideCount = getTop() -- Remember position. pushTurn(left) -- Dig left shaft. dig1x2(i + 2, true) beginPlacingTorches() setTop(sideTop, sideCount) clearMoveCallback() pushTurn(right) -- Dig right shaft. dig1x2(i + 2, true) beginPlacingTorches() setTop(sideTop, sideCount) clearMoveCallback() end -- Go back to start of main shaft. Dig out the center of the main shaft -- while we're at it, so we break through the ceiling between levels. beginDigginTrench() setTop(top, count) clearMoveCallback() end -- Moves to the next main shaft, clockwise. -- PRE: bottom front of nth main shaft. -- POST: bottom front of (n+1)th main shaft. local function gotoNextMainShaft() return pushTurn(right) and dig1x2(2) and pushTurn(right) and dig1x2(2) and pushTurn(left) end --[[ Main ]]------------------------------------------------------------------- local function main(radius, levels, full) -- We dig tunnels every three blocks, to have a spacing of -- two blocks between them (so we don't really have to follow -- veins but can just break the blocks in the wall that are -- interesting to us). So adjust the length accordingly. radius = radius - radius % shaftInterval -- Flag slots that contain something as do-not-drop and check -- that we have some free inventory space at all. local freeSlots = robot.inventorySize() for slot = 1, robot.inventorySize() do if robot.count(slot) > 0 then keepSlot[slot] = true freeSlots = freeSlots - 1 end end if freeSlots < 2 + torchStacks() then -- Place for mined blocks + torches. io.write("Sorry, but I need more empty inventory space to work.\n") os.exit() end gotoMaintenance(true) if not move(sides.forward) then io.write("Exit from docking bay obstructed, aborting.\n") os.exit() end for level = 1, levels do if level > 1 then for _ = 1, 4 do if not move(sides.down) then io.write("Access to level " .. level .. " obstructed, aborting.\n") popMoves() gotoMaintenance(true) os.exit() end end end local top, count = getTop() for shaft = 1, full and 4 or 1 do if shaft > 1 and not gotoNextMainShaft() then break end digShafts(radius) end if full then gotoNextMainShaft() -- Finish the circle. end setTop(top, count) end io.write("All done! Going home to clean up.\n") popMoves() gotoMaintenance(true) end if options.h or options.help then io.write("Usage: miner [-hsf] [radius [levels [full]]]\n") io.write(" -h: this help listing.\n") io.write(" -s: start without prompting.\n") io.write(" -f: force mining to continue even if max\n") io.write(" fuel may be insufficient to return.\n") io.write(" radius: the radius in blocks of the area to\n") io.write(" mine. Adjusted to be a multiple of\n") io.write(" three. Default: 9.\n") io.write(" levels: the number of vertical levels to mine.\n") io.write(" Default: 1.\n") io.write(" full: whether to mine a full level (all four\n") io.write(" cardinal directions). Default: false.\n") os.exit() end local radius = tonumber(args[1]) or 9 local levels = tonumber(args[2]) or 1 local full = args[3] and args[3] == "true" or args[3] == "yes" io.write("Will mine " .. levels .. " levels in a radius of " .. radius .. ".\n") if full then io.write("Will mine all four cardinal directions.\n") end if not component.isAvailable("geolyzer") then io.write("Installing a geolyzer upgrade is strongly recommended.\n") end if not component.isAvailable("inventory_controller") then io.write("Installing an inventory controller upgrade is strongly recommended.\n") end io.write("I'll drop mined out stuff below me.\n") io.write("I'll be looking for torches on my left.\n") if component.isAvailable("inventory_controller") then io.write("I'll try to get new tools from above me.\n") else io.write("You'll need to manually provide me with new tools if they break.\n") end io.write("Run with -h or --help for parameter info.\n") if options.s or prompt("Shall we begin?") then main(radius, levels, full) end