From 3bb3790f1b7179670d021d99ec40a49180d0807e Mon Sep 17 00:00:00 2001 From: Christoph Date: Sun, 5 Oct 2025 19:08:45 +0200 Subject: [PATCH] Implement base TurtleController and simple VolumeExcavationController --- .vscode/settings.json | 7 + controller/testing_controller.lua | 57 ++++ controller/turtle_controller.lua | 349 ++++++++++++++++++++ controller/volume_excavation_controller.lua | 130 ++++++++ lib/direction.lua | 9 + lib/position.lua | 70 ++++ lib/stack.lua | 48 +++ main.lua | 20 ++ 8 files changed, 690 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 controller/testing_controller.lua create mode 100644 controller/turtle_controller.lua create mode 100644 controller/volume_excavation_controller.lua create mode 100644 lib/direction.lua create mode 100644 lib/position.lua create mode 100644 lib/stack.lua create mode 100644 main.lua diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fbf8b76 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "Lua.diagnostics.globals": [ + "printError", + "turtle", + "shell" + ] +} \ No newline at end of file diff --git a/controller/testing_controller.lua b/controller/testing_controller.lua new file mode 100644 index 0000000..869bd21 --- /dev/null +++ b/controller/testing_controller.lua @@ -0,0 +1,57 @@ +local Direction = require("lib.direction") +local TurtleController = require("controller.turtle_controller") + +---@class TestingController +---@field controller TurtleController +local TestingController = {} +TestingController.__index = TestingController + + +---@return TestingController +function TestingController:Create() + local t = {} + setmetatable(t, TestingController) + + t.controller = TurtleController:Create() + + return t +end + + +----------------------------------------------------------------------------------------------- +-- Behavior Methods +----------------------------------------------------------------------------------------------- + + +function TestingController:TestSimpleMovement() +end + + +function TestingController:TestComplexMovement() +end + + +function TestingController:TestUnloading() +end + + +function TestingController:TestRefueling() +end + + +----------------------------------------------------------------------------------------------- +-- Main Method +----------------------------------------------------------------------------------------------- + + +function TestingController:Run() + self.controller:Configure() + + self:TestSimpleMovement() + self:TestComplexMovement() + self:TestUnloading() + self:TestRefueling() +end + + +return TestingController \ No newline at end of file diff --git a/controller/turtle_controller.lua b/controller/turtle_controller.lua new file mode 100644 index 0000000..347794c --- /dev/null +++ b/controller/turtle_controller.lua @@ -0,0 +1,349 @@ +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 \ No newline at end of file diff --git a/controller/volume_excavation_controller.lua b/controller/volume_excavation_controller.lua new file mode 100644 index 0000000..2570f4b --- /dev/null +++ b/controller/volume_excavation_controller.lua @@ -0,0 +1,130 @@ +local Direction = require("lib.direction") +local TurtleController = require("controller.turtle_controller") + +---@alias VolumeExcavationControllerConfig {mine_forward: number, mine_left: number, mine_right: number} + +---@class VolumeExcavationController +---@field controller TurtleController +---@field config VolumeExcavationControllerConfig +local VolumeExcavationController = {} +VolumeExcavationController.__index = VolumeExcavationController + + +---@return VolumeExcavationController +function VolumeExcavationController:Create() + local t = {} + setmetatable(t, VolumeExcavationController) + + + ----------------------------------------------------------------------------------------------- + -- Fields + ----------------------------------------------------------------------------------------------- + + + t.controller = TurtleController:Create() + t.config = { + mine_forward = 0, + mine_left = 0, + mine_right = 0, + } + + + return t +end + + +----------------------------------------------------------------------------------------------- +-- Behavior Methods +----------------------------------------------------------------------------------------------- + + +function VolumeExcavationController:Excavate() + print(("Turtle is mining layer at y=%d..."):format(self.controller.position.y)) + + -- Enter the excavation area + self.controller:EnableMining() + self.controller:MoveRelative(1) + + -- Move to the bottom right corner of the layer + self.controller:TurnToDirection(Direction.EAST) + self.controller:MoveRelative(self.config.mine_right) + self.controller:TurnToDirection(Direction.WEST) + + -- Zig zag mine back to front + -- The direction is important so we can travel to start without colliding with unminded walls + local turn_dir = 1 + for i = 1,self.config.mine_forward do + self.controller:MoveRelative(self.config.mine_left + self.config.mine_right) + + -- Skip the last turn, as we won't mine that slice + -- We want to stay in the mined area so we can move back freely + if i == self.config.mine_forward then + break + end + + self.controller:TurnRelative(turn_dir) + self.controller:MoveRelative(1) + self.controller:TurnRelative(turn_dir) + + turn_dir = -1 * turn_dir + end + + self.controller:MoveToPosition(0, 0, 0, Direction.NORTH) + + -- Unload before doing the next layer + self.controller:TurnToDirection(self.controller.config.storage_direction) + self.controller:DropInventory() + self.controller:TurnToDirection(Direction.NORTH) +end + + +----------------------------------------------------------------------------------------------- +-- Management Methods +----------------------------------------------------------------------------------------------- + + +function VolumeExcavationController:Configure() + local config_complete = false + + while not config_complete do + print("How many blocks should the turtle mine forward?") + self.config.mine_forward = tonumber(io.read()) or 3 + + print("How many blocks should the turtle to its right?") + self.config.mine_right = tonumber(io.read()) or 3 + + print("How many blocks should the turtle mine to its left?") + self.config.mine_left = tonumber(io.read()) or 3 + + local width = self.config.mine_left + self.config.mine_right + 1 + local height = 3 + + print("Configuration complete!") + print(("Turtle will mine an area of %d x %d x %d (forward x width x height) totalling %d blocks."):format( + self.config.mine_forward, width, height, self.config.mine_forward * width * height + )) + print("Do you want to accept the configuration (enter 1, otherwise 0)?") + config_complete = tonumber(io.read()) == 1 + end +end + + +----------------------------------------------------------------------------------------------- +-- Main Method +----------------------------------------------------------------------------------------------- + + +function VolumeExcavationController:Run() + self.controller:Configure() + self:Configure() + + -- Consume our starting fuel and refuel to the full amount + turtle.select(1) + turtle.refuel() + self.controller:RefuelIfEmpty() + + self:Excavate() +end + + +return VolumeExcavationController \ No newline at end of file diff --git a/lib/direction.lua b/lib/direction.lua new file mode 100644 index 0000000..d743911 --- /dev/null +++ b/lib/direction.lua @@ -0,0 +1,9 @@ +---@enum Direction +local Direction = { + NORTH = 0, + EAST = 1, + SOUTH = 2, + WEST = 3 +} + +return Direction \ No newline at end of file diff --git a/lib/position.lua b/lib/position.lua new file mode 100644 index 0000000..9476870 --- /dev/null +++ b/lib/position.lua @@ -0,0 +1,70 @@ +local Direction = require("lib.direction") + +---@class Position +---@field x number +---@field y number +---@field z number +---@field dir Direction +local Position = {} +Position.__index = Position + + +---@return Position +function Position:Empty() + local t = {} + setmetatable(t, Position) + + t.x = 0 + t.y = 0 + t.z = 0 + t.dir = Direction.NORTH + + return t +end + +---@param x number +---@param y number +---@param z number +---@param dir Direction +---@return Position +function Position:Create(x, y, z, dir) + local t = {} + setmetatable(t, Position) + + t.x = x + t.y = y + t.z = z + t.dir = dir or Direction.NORTH + + return t +end + +---@param other Position +---@return Position +function Position:Copy(other) + local t = {} + setmetatable(t, Position) + + t.x = other.x + t.y = other.y + t.z = other.z + t.dir = other.dir + + return t +end + +---@param other Position +function Position:Add(other) + self.x = self.x + other.x + self.y = self.y + other.y + self.z = self.z + other.z +end + +---@param other Position +function Position:Subtract(other) + self.x = self.x - other.x + self.y = self.y - other.y + self.z = self.z - other.z +end + +return Position \ No newline at end of file diff --git a/lib/stack.lua b/lib/stack.lua new file mode 100644 index 0000000..adaf245 --- /dev/null +++ b/lib/stack.lua @@ -0,0 +1,48 @@ +---@class Stack +---@field elements table[] +local Stack = {} +Stack.__index = Stack + + +---@return Stack +function Stack:Create() + -- stack table + local t = {} + setmetatable(t, Stack) + + -- entry table + t.elements = {} + + return t +end + + +---@param element table +function Stack:Push(element) + if element == nil or element == {} then + return + end + + table.insert(self.elements, element) +end + + +---@return table +function Stack:Pop() + return table.remove(self.elements) +end + + +---@return table +function Stack:Peek() + return table[#self.elements] +end + + +---@return number +function Stack:Count() + return #self.elements +end + + +return Stack \ No newline at end of file diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..936d308 --- /dev/null +++ b/main.lua @@ -0,0 +1,20 @@ +local TestingController = require("controller.testing_controller") +local VolumeExcavationController = require("controller.volume_excavation_controller") + + +local controllers = { + TestingController:Create(), + VolumeExcavationController:Create(), +} + +print("Multiple controllers are available:") +print("1: Testing Mode") +print("2: Volume Excavation") + +local choice = 0 +while choice < 1 or choice > #controllers do + print("Choose a controller by entering its number:") + choice = tonumber(io.read()) or 0 +end + +controllers[choice]:Run() \ No newline at end of file