local Position = require("lib.position") local Direction = require("lib.direction") local Stack = require("lib.stack") ---@alias TurtleControllerConfig {fuel_direction: Direction, fuel_name: string, refuel_amount: number, refuel_safety_margin: number, storage_direction: Direction} ---@class TurtleController ---@field config TurtleControllerConfig ---@field position Position ---@field last_positions Stack ---@field mine boolean local TurtleController = {} TurtleController.__index = TurtleController -- TODO: Test if there's a chest when dropping/sucking/mining (don't mine chests!) ---@return TurtleController function TurtleController:Create() local t = {} setmetatable(t, TurtleController) ----------------------------------------------------------------------------------------------- -- Fields ----------------------------------------------------------------------------------------------- t.config = { fuel_direction = Direction.EAST, fuel_name = turtle.getItemDetail(1) == nil and "" or turtle.getItemDetail(1).name, refuel_amount = 500, refuel_safety_margin = 160, storage_direction = Direction.WEST, } t.position = Position:Empty() t.last_positions = Stack:Create() t.mine = false return t end ----------------------------------------------------------------------------------------------- -- Movement Methods ----------------------------------------------------------------------------------------------- ---Positive numbers turn clockwise, negative numbers turn counterclockwise ---@param number_of_turns number function TurtleController:TurnRelative(number_of_turns) if number_of_turns == 0 then return end print(("Turtle is turning by %d..."):format(number_of_turns)) self:RefuelIfEmpty() -- Turn turtle for _ = 1,math.abs(number_of_turns) do if number_of_turns > 0 then turtle.turnRight() else turtle.turnLeft() end end -- Update current_position self.position.dir = (self.position.dir + number_of_turns) % 4 end ---@param direction Direction function TurtleController:TurnToDirection(direction) if self.position.dir == direction then return end print(("Turtle is turning to direction %d..."):format(direction)) self:RefuelIfEmpty() local rotation = direction - self.position.dir if math.abs(rotation) == 3 then -- If we're rotating by 3, we could turn faster by rotating by 1 in the opposite direction rotation = -1 * (rotation - 1) end self:TurnRelative(rotation) end ---Move forward by a number of blocks depending on the current rotation ---@param number_of_blocks number function TurtleController:MoveRelative(number_of_blocks) if number_of_blocks == 0 then return end print(("Turtle is moving forward by %d blocks..."):format(number_of_blocks)) for _ = 1,math.abs(number_of_blocks) do self:RefuelIfEmpty() self:UnstockIfFull() -- Break blocks if self.mine then while not turtle.forward() do turtle.dig() end turtle.digUp() turtle.digDown() else if not turtle.forward() then printError("Turtle wants to move without mining but is blocked!") end end -- Update current_position if self.position.dir == Direction.NORTH then self.position.z = self.position.z + 1 elseif self.position.dir == Direction.SOUTH then self.position.z = self.position.z - 1 elseif self.position.dir == Direction.EAST then self.position.x = self.position.x + 1 elseif self.position.dir == Direction.WEST then self.position.x = self.position.x - 1 end end end ---@param x number ---@param y number ---@param z number ---@param dir Direction function TurtleController:MoveToPosition(x, y, z, dir) print(("Turtle is moving to (%d, %d, %d)..."):format(x, y, z)) self:DisableMining() self.last_positions:Push(Position:Copy(self.position)) -- Move south once (if possible) to not be blocked by the slice that is currently mined self:TurnToDirection(Direction.SOUTH) self:MoveRelative(1) -- EAST/WEST axis (do first to not interfere with chests or unmined walls) if self.position.x > x then self:TurnToDirection(Direction.WEST) elseif self.position.x < x then self:TurnToDirection(Direction.EAST) end self:MoveRelative(math.abs(self.position.x - x)) -- NORTH/SOUTH axis if self.position.z > z then self:TurnToDirection(Direction.SOUTH) elseif self.position.z < z then self:TurnToDirection(Direction.NORTH) end self:MoveRelative(math.abs(self.position.z - z)) -- Direction self:TurnToDirection(dir or Direction.NORTH) -- Sanity check if not(self.position.x == x and self.position.y == y and self.position.z == z and self.position.dir == dir) then printError("TurtleController:MoveToPosition failed to move to target position!") end end function TurtleController:MoveBack() local target_position = self.last_positions:Pop() self:MoveToPosition(target_position.x, target_position.y, target_position.z, target_position.dir) self.last_positions:Pop() end ----------------------------------------------------------------------------------------------- -- Inventory Methods ----------------------------------------------------------------------------------------------- ---@param slot number ---@param count number ---@return boolean function TurtleController:SuckItem(slot, count) local previous_slot = turtle.getSelectedSlot() turtle.select(slot or 1) local sucked = turtle.suck(count) turtle.select(previous_slot) return sucked end ---@param slot number ---@param count number ---@return boolean function TurtleController:DropItem(slot, count) local previous_slot = turtle.getSelectedSlot() turtle.select(slot or 1) local dropped = turtle.drop(count) turtle.select(previous_slot) return dropped end function TurtleController:DropInventory() print("Dropping inventory into chest...") for slot = 1,16 do turtle.select(slot) turtle.drop() end turtle.select(1) end function TurtleController:UnstockIfFull() if self:HasInventorySpace() then return end print("Turtle is unstocking...") self:MoveToPosition(0, 0, 0, self.config.storage_direction) self:DropInventory() self:RefuelIfEmpty() self:MoveBack() end function TurtleController:RefuelIfEmpty() if self:HasFuel() then return end print("Turtle is refueling...") -- Clear our inventory into the storage chest self:MoveToPosition(0, 0, 0, self.config.storage_direction) self:DropInventory() -- Prepare refueling self:TurnToDirection(self.config.fuel_direction) turtle.select(1) -- Include the distance to the last position when refueling -- to keep the amount of work done between refuelings constant local last_position = self.last_positions:Peek() local target_fuel_level = self.config.refuel_amount + last_position.x + last_position.y + last_position.z -- Refuel until we hit the refuel_amount local before_level = turtle.getFuelLevel() repeat if not self:SuckItem(1, 1) then printError("Failed to suck fuel out of fuel chest!") shell.exit() end turtle.refuel() until turtle.getFuelLevel() >= target_fuel_level local after_level = turtle.getFuelLevel() self:MoveBack() print(("Refuelled %d units, current level is %d (old level was %d)"):format(after_level - before_level, after_level, before_level)) end ----------------------------------------------------------------------------------------------- -- Management Methods ----------------------------------------------------------------------------------------------- function TurtleController:Configure() local config_complete = false while not config_complete do -- Check for valid fuel while self.config.fuel_name == "" do print("Please put valid fuel into the first slot and press Enter!") local _ = io.read() self.config.fuel_name = turtle.getItemDetail(1) == nil and "" or turtle.getItemDetail(1).name end print("A chest with fuel has to be placed beside the turtle's starting position. Is this chest...") print(("...right of the turtle (enter %d)?"):format(Direction.EAST)) print(("...behind the turtle (enter %d)?"):format(Direction.SOUTH)) print(("...left of the turtle (enter %d)?"):format(Direction.WEST)) print("By default, the chest is assumed on the left.") self.config.fuel_direction = tonumber(io.read()) or Direction.WEST print("A chest to store mined blocks has to be placed beside the turtle's starting position. Is this chest...") print(("...right of the turtle (enter %d)?"):format(Direction.EAST)) print(("...behind the turtle (enter %d)?"):format(Direction.SOUTH)) print(("...left of the turtle (enter %d)?"):format(Direction.WEST)) print("By default, the chest is assumed on the right.") self.config.storage_direction = tonumber(io.read()) or Direction.EAST print("Configuration complete!") print(("Turtle will consume \"%s\" for fuel. Put the desired fuel into the first slot to change."):format(self.config.fuel_name)) print("Do you want to accept the configuration (enter 1, otherwise 0)?") config_complete = tonumber(io.read()) == 1 end end function TurtleController:EnableMining() self.mine = true end function TurtleController:DisableMining() self.mine = false end function TurtleController:HasFuel() local level = turtle.getFuelLevel() local distance_home = self.position.x + self.position.y + self.position.z return level > distance_home + self.config.refuel_safety_margin end function TurtleController:HasInventorySpace() for slot = 1,16 do if turtle.getItemDetail(slot) == nil then return true end end return false end return TurtleController