diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..2e663c2 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,9 @@ +{ + "diagnostics.globals": [ + "printError", + "turtle", + "shell", + "peripheral", + "parallel" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index aef4dea..cab4fd3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ -{ - "Lua.diagnostics.globals": [ - "printError", - "turtle", - "shell", - "peripheral", - "parallel" - ] +{ + "Lua.diagnostics.globals": [ + "printError", + "turtle", + "shell", + "peripheral", + "parallel" + ] } \ No newline at end of file diff --git a/controller/audio_controller.lua b/controller/audio_controller.lua index 12a6e42..de151d4 100644 --- a/controller/audio_controller.lua +++ b/controller/audio_controller.lua @@ -1,96 +1,87 @@ -local dfpwm = require("cc.audio.dfpwm") -local Direction = require("lib.direction") - ----@alias AudioControllerConfig {buffer_length_seconds: number} - ----@class AudioController ----@field config AudioControllerConfig ----@field play boolean -local AudioController = {} -AudioController.__index = AudioController - - ----@return AudioController -function AudioController:Create() - local t = {} - setmetatable(t, AudioController) - - - ----------------------------------------------------------------------------------------------- - -- Fields - ----------------------------------------------------------------------------------------------- - t.config = { - buffer_length_seconds = 1 - } - t.play = false - - - return t -end - - ------------------------------------------------------------------------------------------------ --- Audio Methods ------------------------------------------------------------------------------------------------ - - -function AudioController:PlayAudio(filename) - self.play = true - - local decoder = dfpwm.make_decoder() - while self.play do - for chunk in io.lines(("audio/%s.dfpwm"):format(filename), self.config.buffer_length_seconds * 1024) do - if not self.play then - break - end - - local buffer = decoder(chunk) - - while not self:GetSpeaker().playAudio(buffer) do - ---@diagnostic disable-next-line: undefined-field - os.pullEvent("speaker_audio_empty") - end - end - end -end - - -function AudioController:PlayAudioFactory(filename) - local function play() - self:PlayAudio(filename) - end - - return play -end - - ------------------------------------------------------------------------------------------------ --- Management Methods ------------------------------------------------------------------------------------------------ - - ----@return table | nil -function AudioController:GetSpeaker() - return peripheral.find("speaker") -end - - -function AudioController:StopPlaying() - self.play = false - self:GetSpeaker().stop() -end - - ------------------------------------------------------------------------------------------------ --- Main Method ------------------------------------------------------------------------------------------------ - - -function AudioController:Run() - -- self:Configure() - - self:PlayAudio("bangarang") -end - - -return AudioController \ No newline at end of file +local dfpwm = require("cc.audio.dfpwm") + +---@alias AudioControllerConfig {buffer_length_seconds: number} + +---@class AudioController +---@field config AudioControllerConfig +---@field play boolean +local AudioController = {} +AudioController.__index = AudioController + +---@return AudioController +function AudioController:Create() + local t = {} + setmetatable(t, AudioController) + + ----------------------------------------------------------------------------------------------- + -- Fields + ----------------------------------------------------------------------------------------------- + t.config = { + buffer_length_seconds = 1, + } + t.play = false + + return t +end + +----------------------------------------------------------------------------------------------- +-- Audio Methods +----------------------------------------------------------------------------------------------- + +function AudioController:PlayAudio(filename) + self.play = true + + local decoder = dfpwm.make_decoder() + while self.play do + for chunk in io.lines(("audio/%s.dfpwm"):format(filename), self.config.buffer_length_seconds * 1024) do + if not self.play then + break + end + + local buffer = decoder(chunk) + + while not self:GetSpeaker().playAudio(buffer) do + if not self.play then + break + end + + ---@diagnostic disable-next-line: undefined-field + os.pullEvent("speaker_audio_empty") + end + end + end +end + +function AudioController:PlayAudioFactory(filename) + local function play() + self:PlayAudio(filename) + end + + return play +end + +----------------------------------------------------------------------------------------------- +-- Management Methods +----------------------------------------------------------------------------------------------- + +---@return table | nil +function AudioController:GetSpeaker() + return peripheral.find("speaker") +end + +function AudioController:StopPlaying() + self.play = false + self:GetSpeaker().stop() +end + +----------------------------------------------------------------------------------------------- +-- Main Method +----------------------------------------------------------------------------------------------- + +function AudioController:Run() + -- self:Configure() + + self:PlayAudio("bangarang") +end + +return AudioController diff --git a/controller/excavation_controller.lua b/controller/excavation_controller.lua index 9b7c546..5081cd8 100644 --- a/controller/excavation_controller.lua +++ b/controller/excavation_controller.lua @@ -11,322 +11,320 @@ local AudioController = require("controller.audio_controller") local ExcavationController = {} ExcavationController.__index = ExcavationController - ---@return ExcavationController function ExcavationController:Create() - local t = {} - setmetatable(t, ExcavationController) + local t = {} + setmetatable(t, ExcavationController) + ----------------------------------------------------------------------------------------------- + -- Fields + ----------------------------------------------------------------------------------------------- - ----------------------------------------------------------------------------------------------- - -- Fields - ----------------------------------------------------------------------------------------------- + t.controller = TurtleController:Create() + t.audio = AudioController:Create() + t.config = { + slice_length = 0, + slice_height = 0, + center_slice_width = 0, + slice_width = 0, + slice_padding = 0, + slices_left = 0, + slices_right = 0, + } - - t.controller = TurtleController:Create() - t.audio = AudioController:Create() - t.config = { - slice_length = 0, - slice_height = 0, - center_slice_width = 0, - slice_width = 0, - slice_padding = 0, - slices_left = 0, - slices_right = 0, - } - - - return t + return t end - ----------------------------------------------------------------------------------------------- -- Behavior Methods ----------------------------------------------------------------------------------------------- - ---Excavates a single 1x1/1x2 tunnel of configured length. ---Will leave the turtle wherever it ends up. ---Only unstocks/refuels if required. ---@param mine_above boolean ---@param mine_below boolean function ExcavationController:Excavate_1x1or2or3xL(mine_above, mine_below) - self.controller:EnableMiningForward() - if mine_above then - self.controller:EnableMiningAbove() - end - if mine_below then - self.controller:EnableMiningBelow() - end + self.controller:EnableMiningForward() + if mine_above then + self.controller:EnableMiningAbove() + end + if mine_below then + self.controller:EnableMiningBelow() + end - self.controller:MoveForward(self.config.slice_length) + self.controller:MoveForward(self.config.slice_length) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:DisableMiningBelow() + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:DisableMiningBelow() end - ---Excavates a single 1xH partial slice of configured length. ---Will leave the turtle in its starting position. ---Only unstocks/refuels if required. function ExcavationController:Excavate_1xHxL() - self.controller:StorePosition() + self.controller:StorePosition() - -- We can mine 2 blocks heigh in a single go - local number_of_1x3xL = math.floor(self.config.slice_height / 3) + -- We can mine 2 blocks heigh in a single go + local number_of_1x3xL = math.floor(self.config.slice_height / 3) - -- Prepare for mining the blocks below the turtle - if number_of_1x3xL > 0 then - self.controller:EnableMiningAbove() - self.controller:MoveVertical(1) - self.controller:DisableMiningAbove() - end + -- Prepare for mining the blocks below the turtle + if number_of_1x3xL > 0 then + self.controller:EnableMiningAbove() + self.controller:MoveVertical(1) + self.controller:DisableMiningAbove() + end - -- Mine 1x3xL - for i = 1,number_of_1x3xL do - self:Excavate_1x1or2or3xL(true, true) + -- Mine 1x3xL + for i = 1, number_of_1x3xL do + self:Excavate_1x1or2or3xL(true, true) - self.controller:TurnRelative(2) + self.controller:TurnRelative(2) - -- Don't move upwards in the last iteration so we can decide - -- how much to move depending on how much we still have to excavate - if i == number_of_1x3xL then - break - end + -- Don't move upwards in the last iteration so we can decide + -- how much to move depending on how much we still have to excavate + if i == number_of_1x3xL then + break + end - self.controller:EnableMiningAbove() - self.controller:MoveVertical(4) - self.controller:MoveVertical(-1) - self.controller:DisableMiningAbove() - end + self.controller:EnableMiningAbove() + self.controller:MoveVertical(4) + self.controller:MoveVertical(-1) + self.controller:DisableMiningAbove() + end - -- Mine remaining 1x2xL or 1x1xL - if self.config.slice_height % 3 == 2 then - self.controller:EnableMiningAbove() - self.controller:MoveVertical(3) - self.controller:MoveVertical(-1) - self.controller:DisableMiningAbove() + -- Mine remaining 1x2xL or 1x1xL + if self.config.slice_height % 3 == 2 then + self.controller:EnableMiningAbove() + self.controller:MoveVertical(3) + self.controller:MoveVertical(-1) + self.controller:DisableMiningAbove() - self:Excavate_1x1or2or3xL(true, false) - elseif self.config.slice_height % 3 == 1 then - self.controller:EnableMiningAbove() - self.controller:MoveVertical(3) - self.controller:MoveVertical(-1) - self.controller:DisableMiningAbove() + self:Excavate_1x1or2or3xL(true, false) + elseif self.config.slice_height % 3 == 1 then + self.controller:EnableMiningAbove() + self.controller:MoveVertical(3) + self.controller:MoveVertical(-1) + self.controller:DisableMiningAbove() - self:Excavate_1x1or2or3xL(false, false) - end + self:Excavate_1x1or2or3xL(false, false) + end - self.controller:EnableMiningBelow() - self.controller:MoveBack() - self.controller:DisableMiningBelow() + self.controller:EnableMiningBelow() + self.controller:MoveBack() + self.controller:DisableMiningBelow() end - ---Excavates a single WxH slice of configured length (EAST to WEST). ---Will leave the turtle in its starting position. ---Only unstocks/refuels if required. ---@param center_slice boolean | nil function ExcavationController:Excavate_WxHxL(center_slice) - self.controller:StorePosition() + self.controller:StorePosition() - center_slice = center_slice or false - local width = center_slice and self.config.center_slice_width or self.config.slice_width + center_slice = center_slice or false + local width = center_slice and self.config.center_slice_width or self.config.slice_width - for i = 1,width do - self:Excavate_1xHxL() + for i = 1, width do + self:Excavate_1xHxL() - if i == width then - break - end + if i == width then + break + end - self.controller:TurnToDirection(Direction.WEST) - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(1) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:TurnToDirection(Direction.NORTH) - end + self.controller:TurnToDirection(Direction.WEST) + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(1) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:TurnToDirection(Direction.NORTH) + end - self.controller:MoveBack() + self.controller:MoveBack() end - ---Excavates all slices. ---Will leave the turtle refueled and unstocked in its 0x0x0 position. function ExcavationController:Excavate() - -- Enter excavation area - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(1) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() + -- Enter excavation area + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(1) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() - -- Excavate center slice (start from right/back/bottom) - -- Uneven center widths will be centered, even center widths will have a block more on the right - print("Excavating center slice...") - local center_width_right = math.floor(self.config.center_slice_width / 2) - self.controller:StorePosition() - self.controller:TurnToDirection(Direction.EAST) - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(center_width_right) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:TurnToDirection(Direction.NORTH) - self:Excavate_WxHxL(true) - self.controller:MoveBack() -- (0, 0, 1) + -- Excavate center slice (start from right/back/bottom) + -- Uneven center widths will be centered, even center widths will have a block more on the right + print("Excavating center slice...") + local center_width_right = math.floor(self.config.center_slice_width / 2) + self.controller:StorePosition() + self.controller:TurnToDirection(Direction.EAST) + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(center_width_right) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:TurnToDirection(Direction.NORTH) + self:Excavate_WxHxL(true) + self.controller:MoveBack() -- (0, 0, 1) - if self.config.slices_right > 0 then - -- Move to right slices starting location - local padded_width_right = self.config.slices_right * (self.config.slice_width + self.config.slice_padding) - self.controller:StorePosition() - self.controller:TurnToDirection(Direction.EAST) - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(center_width_right + padded_width_right) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:TurnToDirection(Direction.NORTH) + if self.config.slices_right > 0 then + -- Move to right slices starting location + local padded_width_right = self.config.slices_right * (self.config.slice_width + self.config.slice_padding) + self.controller:StorePosition() + self.controller:TurnToDirection(Direction.EAST) + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(center_width_right + padded_width_right) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:TurnToDirection(Direction.NORTH) - -- Excavate right slices - print("Excavating right slices") - for i = 1,self.config.slices_right do - self:Excavate_WxHxL() + -- Excavate right slices + print("Excavating right slices") + for i = 1, self.config.slices_right do + self:Excavate_WxHxL() - if i == self.config.slices_right then - break - end + if i == self.config.slices_right then + break + end - -- Move to the next slice - self.controller:TurnToDirection(Direction.WEST) - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(self.config.slice_width + self.config.slice_padding) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:TurnToDirection(Direction.NORTH) - end - self.controller:MoveBack() -- (0, 0, 1) - end + -- Move to the next slice + self.controller:TurnToDirection(Direction.WEST) + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(self.config.slice_width + self.config.slice_padding) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:TurnToDirection(Direction.NORTH) + end + self.controller:MoveBack() -- (0, 0, 1) + end - if self.config.slices_left > 0 then - -- Move to left slices starting location - local center_width_left = self.config.center_slice_width - center_width_right - self.controller:StorePosition() - self.controller:TurnToDirection(Direction.WEST) - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(center_width_left) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:TurnToDirection(Direction.NORTH) + if self.config.slices_left > 0 then + -- Move to left slices starting location + local center_width_left = self.config.center_slice_width - center_width_right + self.controller:StorePosition() + self.controller:TurnToDirection(Direction.WEST) + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(center_width_left) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:TurnToDirection(Direction.NORTH) - -- Excavate left slices - print("Excavate left slices") - for i = 1,self.config.slices_left do - -- Move to the next slice - self.controller:TurnToDirection(Direction.WEST) - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(self.config.slice_padding) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:TurnToDirection(Direction.NORTH) + -- Excavate left slices + print("Excavate left slices") + for i = 1, self.config.slices_left do + -- Move to the next slice + self.controller:TurnToDirection(Direction.WEST) + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(self.config.slice_padding) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:TurnToDirection(Direction.NORTH) - self:Excavate_WxHxL() + self:Excavate_WxHxL() - if i == self.config.slices_left then - break - end + if i == self.config.slices_left then + break + end - self.controller:TurnToDirection(Direction.WEST) - self.controller:EnableMiningForward() - self.controller:EnableMiningAbove() - self.controller:MoveForward(self.config.slice_width) - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:TurnToDirection(Direction.NORTH) - end - self.controller:MoveBack() -- (0, 0, 1) - end + self.controller:TurnToDirection(Direction.WEST) + self.controller:EnableMiningForward() + self.controller:EnableMiningAbove() + self.controller:MoveForward(self.config.slice_width) + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:TurnToDirection(Direction.NORTH) + end + self.controller:MoveBack() -- (0, 0, 1) + end - -- Finish up - self.controller:MoveToPosition(0, 0, 0, self.controller.config.storage_direction) - self.controller:DropInventory() - self.controller:TurnToDirection(Direction.NORTH) - self.audio:StopPlaying() + -- Finish up + self.controller:MoveToPosition(0, 0, 0, self.controller.config.storage_direction) + self.controller:DropInventory() + self.controller:TurnToDirection(Direction.NORTH) + self.audio:StopPlaying() end - ----------------------------------------------------------------------------------------------- -- Management Methods ----------------------------------------------------------------------------------------------- - function ExcavationController:Configure() - local config_complete = false + local config_complete = false - while not config_complete do - print("How long should each slice be?") - self.config.slice_length = tonumber(io.read()) or 3 + while not config_complete do + print("How long should each slice be?") + self.config.slice_length = tonumber(io.read()) or 3 - print("How high should each slice be?") - self.config.slice_height = tonumber(io.read()) or 3 + print("How high should each slice be?") + self.config.slice_height = tonumber(io.read()) or 3 - print("How wide should the center slice be?") - self.config.center_slice_width = tonumber(io.read()) or 1 + print("How wide should the center slice be?") + self.config.center_slice_width = tonumber(io.read()) or 1 - print("How wide should the other slices be?") - self.config.slice_width = tonumber(io.read()) or 1 + print("How wide should the other slices be?") + self.config.slice_width = tonumber(io.read()) or 1 - print("How many blocks should remain between each slice?") - self.config.slice_padding = tonumber(io.read()) or 0 + print("How many blocks should remain between each slice?") + self.config.slice_padding = tonumber(io.read()) or 0 - print("How many slices should the turtle mine to its right?") - self.config.slices_right = tonumber(io.read()) or 1 + print("How many slices should the turtle mine to its right?") + self.config.slices_right = tonumber(io.read()) or 1 - print("How many slices should the turtle mine to its left?") - self.config.slices_left = tonumber(io.read()) or 1 + print("How many slices should the turtle mine to its left?") + self.config.slices_left = tonumber(io.read()) or 1 - local padded_width_left = self.config.slices_left * (self.config.slice_width + self.config.slice_padding) - local padded_width_right = self.config.slices_right * (self.config.slice_width + self.config.slice_padding) - local padded_width = padded_width_left + padded_width_right + self.config.center_slice_width - local mined_width_left = self.config.slices_left * self.config.slice_width - local mined_width_right = self.config.slices_right * self.config.slice_width - local mined_width = mined_width_left + mined_width_right + self.config.center_slice_width + local padded_width_left = self.config.slices_left * (self.config.slice_width + self.config.slice_padding) + local padded_width_right = self.config.slices_right * (self.config.slice_width + self.config.slice_padding) + local padded_width = padded_width_left + padded_width_right + self.config.center_slice_width + local mined_width_left = self.config.slices_left * self.config.slice_width + local mined_width_right = self.config.slices_right * self.config.slice_width + local mined_width = mined_width_left + mined_width_right + self.config.center_slice_width - print("Configuration complete!") - print(("Mining area spans %d x %d x %d (width x height x forward), totalling %d blocks."):format( - padded_width, self.config.slice_height, self.config.slice_length, padded_width * self.config.slice_height * self.config.slice_length - )) - print(("Turtle will mine %d block wide slices (%d wide in the center) with %d blocks of padding, totalling %d mined blocks."):format( - self.config.slice_width, self.config.center_slice_width, self.config.slice_padding, mined_width * self.config.slice_height * self.config.slice_length - )) - print("Do you want to accept the configuration (enter 1, otherwise 0)?") - config_complete = tonumber(io.read()) == 1 - end + print("Configuration complete!") + print( + ("Mining area spans %d x %d x %d (width x height x forward), totalling %d blocks."):format( + padded_width, + self.config.slice_height, + self.config.slice_length, + padded_width * self.config.slice_height * self.config.slice_length + ) + ) + print( + ("Turtle will mine %d block wide slices (%d wide in the center) with %d blocks of padding, totalling %d mined blocks."):format( + self.config.slice_width, + self.config.center_slice_width, + self.config.slice_padding, + mined_width * self.config.slice_height * self.config.slice_length + ) + ) + print("Do you want to accept the configuration (enter 1, otherwise 0)?") + config_complete = tonumber(io.read()) == 1 + end end - ----------------------------------------------------------------------------------------------- -- Main Method ----------------------------------------------------------------------------------------------- - function ExcavationController:Run() - self.controller:Configure() - self:Configure() + self.controller:Configure() + self:Configure() - -- Consume our starting fuel and refuel to the full amount - turtle.select(1) - turtle.refuel() - self.controller:RefuelIfEmpty() + -- Consume our starting fuel and refuel to the full amount + turtle.select(1) + turtle.refuel() + self.controller:RefuelIfEmpty() - parallel.waitForAll(function() self:Excavate() end, self.audio:PlayAudioFactory("bangarang")) + parallel.waitForAll(function() + self:Excavate() + end, self.audio:PlayAudioFactory("bangarang")) end - -return ExcavationController \ No newline at end of file +return ExcavationController diff --git a/controller/testing_controller.lua b/controller/testing_controller.lua index 1a552c5..a5da36b 100644 --- a/controller/testing_controller.lua +++ b/controller/testing_controller.lua @@ -1,211 +1,220 @@ -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:TestRelativeMovementWithRelativeRotation() - print("Testing relative movement with relative rotation...") - - -- N: BackCenter (Center) - self.controller:MoveForward(3) - self.controller:TurnRelative(1) -- E: FrontCenter (Center) - self.controller:MoveForward(3) - self.controller:TurnRelative(2) -- W: FrontRight (Center) - self.controller:MoveVertical(3) - self.controller:TurnRelative(8) -- W: FrontRight (Top) - self.controller:MoveForward(3) - self.controller:TurnRelative(3) -- S: FrontCenter (Top) - self.controller:MoveForward(3) - self.controller:TurnRelative(9) -- W: BackCenter (Top) - self.controller:MoveVertical(-3) - self.controller:TurnRelative(1) -- N: BackCenter (Center) - - print("The turtle should be in its original location.") - print(("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( - self.controller.position.x, self.controller.position.y, self.controller.position.z, self.controller.position.dir - )) - print("Press Enter to continue") - _ = io.read() -end - - -function TestingController:TestRelativeMovementWithAbsoluteRotation() - print("Testing relative movement with absolute rotation...") - - -- N: BackCenter (Center) - self.controller:MoveForward(3) - self.controller:TurnToDirection(Direction.EAST) -- E: FrontCenter (Center) - self.controller:MoveForward(3) - self.controller:TurnToDirection(Direction.WEST) -- W: FrontRight (Center) - self.controller:MoveVertical(3) - self.controller:TurnToDirection(Direction.WEST) -- W: FrontRight (Top) - self.controller:MoveForward(3) - self.controller:TurnToDirection(Direction.SOUTH) -- S: FrontCenter (Top) - self.controller:MoveForward(3) - self.controller:TurnToDirection(Direction.WEST) -- W: BackCenter (Top) - self.controller:MoveVertical(-3) - self.controller:TurnToDirection(Direction.NORTH) -- N: BackCenter (Center) - - print("The turtle should be in its original location.") - print(("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( - self.controller.position.x, self.controller.position.y, self.controller.position.z, self.controller.position.dir - )) - print("Press Enter to continue") - _ = io.read() -end - - -function TestingController:TestAbsoluteMovementWithoutStack() - print("Testing absolute movement without stack...") - - -- N: BackCenter (Center) - self.controller:MoveToPosition(0, 0, 3, Direction.EAST) -- E: FrontCenter (Center) - self.controller:MoveToPosition(3, 0, 3, Direction.WEST) -- W: FrontRight (Center) - self.controller:MoveToPosition(3, 3, 3, Direction.WEST) -- W: FrontRight (Top) - self.controller:MoveToPosition(0, 3, 3, Direction.SOUTH) -- S: FrontCenter (Top) - self.controller:MoveToPosition(0, 3, 0, Direction.WEST) -- W: BackCenter (Top) - self.controller:MoveToPosition(0, 0, 0, Direction.NORTH) -- N: BackCenter (Center) - - print("The turtle should be in its original location.") - print(("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( - self.controller.position.x, self.controller.position.y, self.controller.position.z, self.controller.position.dir - )) - print("Press Enter to continue") - _ = io.read() -end - - -function TestingController:TestAbsoluteMovementWithStack() - print("Testing absolute movement with stack...") - - -- N: BotCenter (Center) - self.controller:MoveToPosition(0, 0, 3, Direction.EAST) -- E: TopCenter (Center) - self.controller:MoveBack() -- N: BotCenter (Center) - self.controller:MoveToPosition(3, 3, 3, Direction.WEST) -- W: TopRight (Top) - self.controller:MoveBack() -- N: BotCenter (Center) - - print("The turtle should be in its original location.") - print(("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( - self.controller.position.x, self.controller.position.y, self.controller.position.z, self.controller.position.dir - )) - print("Press Enter to continue") - _ = io.read() -end - - -function TestingController:TestUnstocking() - print("Testing inventory unloading...") - - self.controller:MoveToPosition(3, 0, 3, Direction.WEST) - print("Please fill the entire inventory with items.") - print("Press Enter to continue") - _ = io.read() - self.controller:UnstockIfFull(true) - print("Press Enter to continue") - _ = io.read() - self.controller:MoveBack() - - print("The turtle should be in its original location.") - print("Press Enter to continue") - _ = io.read() -end - - -function TestingController:TestRefueling() - print("Testing refueling...") - - self.controller:MoveToPosition(3, 0, 3, Direction.WEST) - print("Please fill multiple inventory slots with items.") - print("Press Enter to continue") - _ = io.read() - self.controller:RefuelIfEmpty(true) - print("Press Enter to continue") - _ = io.read() - self.controller:MoveBack() - - print("The turtle should be in its original location.") - print("Press Enter to continue") - _ = io.read() -end - - ------------------------------------------------------------------------------------------------ --- Main Method ------------------------------------------------------------------------------------------------ - - -function TestingController:Run() - self.controller:Configure() - self.controller:DisableMiningForward() - self.controller:DisableMiningAbove() - self.controller:DisableMiningBelow() - - print("There are multiple tests available:") - print("1: Relative movement with relative rotation") - print("2: Relative movement with absolute rotation") - print("3: Absolute movement without stack") - print("4: Absolute movement with stack") - print("5: All movement tests") - print("6: Unloading") - print("7: Refueling") - print("8: All non-movement tests") - print("9: All tests") - - local choice = 0 - while choice < 1 or choice > 9 do - print("Choose a test by entering its number:") - choice = tonumber(io.read()) or 0 - end - - if choice == 1 then - self:TestRelativeMovementWithRelativeRotation() - elseif choice == 2 then - self:TestRelativeMovementWithAbsoluteRotation() - elseif choice == 3 then - self:TestAbsoluteMovementWithoutStack() - elseif choice == 4 then - self:TestAbsoluteMovementWithStack() - elseif choice == 5 then - self:TestRelativeMovementWithRelativeRotation() - self:TestRelativeMovementWithAbsoluteRotation() - self:TestAbsoluteMovementWithoutStack() - self:TestAbsoluteMovementWithStack() - elseif choice == 6 then - self:TestUnstocking() - elseif choice == 7 then - self:TestRefueling() - elseif choice == 8 then - self:TestUnstocking() - self:TestRefueling() - elseif choice == 9 then - self:TestRelativeMovementWithRelativeRotation() - self:TestRelativeMovementWithAbsoluteRotation() - self:TestAbsoluteMovementWithoutStack() - self:TestAbsoluteMovementWithStack() - self:TestUnstocking() - self:TestRefueling() - end -end - - -return TestingController \ No newline at end of file +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:TestRelativeMovementWithRelativeRotation() + print("Testing relative movement with relative rotation...") + + -- N: BackCenter (Center) + self.controller:MoveForward(3) + self.controller:TurnRelative(1) -- E: FrontCenter (Center) + self.controller:MoveForward(3) + self.controller:TurnRelative(2) -- W: FrontRight (Center) + self.controller:MoveVertical(3) + self.controller:TurnRelative(8) -- W: FrontRight (Top) + self.controller:MoveForward(3) + self.controller:TurnRelative(3) -- S: FrontCenter (Top) + self.controller:MoveForward(3) + self.controller:TurnRelative(9) -- W: BackCenter (Top) + self.controller:MoveVertical(-3) + self.controller:TurnRelative(1) -- N: BackCenter (Center) + + print("The turtle should be in its original location.") + print( + ("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( + self.controller.position.x, + self.controller.position.y, + self.controller.position.z, + self.controller.position.dir + ) + ) + print("Press Enter to continue") + _ = io.read() +end + +function TestingController:TestRelativeMovementWithAbsoluteRotation() + print("Testing relative movement with absolute rotation...") + + -- N: BackCenter (Center) + self.controller:MoveForward(3) + self.controller:TurnToDirection(Direction.EAST) -- E: FrontCenter (Center) + self.controller:MoveForward(3) + self.controller:TurnToDirection(Direction.WEST) -- W: FrontRight (Center) + self.controller:MoveVertical(3) + self.controller:TurnToDirection(Direction.WEST) -- W: FrontRight (Top) + self.controller:MoveForward(3) + self.controller:TurnToDirection(Direction.SOUTH) -- S: FrontCenter (Top) + self.controller:MoveForward(3) + self.controller:TurnToDirection(Direction.WEST) -- W: BackCenter (Top) + self.controller:MoveVertical(-3) + self.controller:TurnToDirection(Direction.NORTH) -- N: BackCenter (Center) + + print("The turtle should be in its original location.") + print( + ("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( + self.controller.position.x, + self.controller.position.y, + self.controller.position.z, + self.controller.position.dir + ) + ) + print("Press Enter to continue") + _ = io.read() +end + +function TestingController:TestAbsoluteMovementWithoutStack() + print("Testing absolute movement without stack...") + + -- N: BackCenter (Center) + self.controller:MoveToPosition(0, 0, 3, Direction.EAST) -- E: FrontCenter (Center) + self.controller:MoveToPosition(3, 0, 3, Direction.WEST) -- W: FrontRight (Center) + self.controller:MoveToPosition(3, 3, 3, Direction.WEST) -- W: FrontRight (Top) + self.controller:MoveToPosition(0, 3, 3, Direction.SOUTH) -- S: FrontCenter (Top) + self.controller:MoveToPosition(0, 3, 0, Direction.WEST) -- W: BackCenter (Top) + self.controller:MoveToPosition(0, 0, 0, Direction.NORTH) -- N: BackCenter (Center) + + print("The turtle should be in its original location.") + print( + ("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( + self.controller.position.x, + self.controller.position.y, + self.controller.position.z, + self.controller.position.dir + ) + ) + print("Press Enter to continue") + _ = io.read() +end + +function TestingController:TestAbsoluteMovementWithStack() + print("Testing absolute movement with stack...") + + -- N: BotCenter (Center) + self.controller:MoveToPosition(0, 0, 3, Direction.EAST) -- E: TopCenter (Center) + self.controller:MoveBack() -- N: BotCenter (Center) + self.controller:MoveToPosition(3, 3, 3, Direction.WEST) -- W: TopRight (Top) + self.controller:MoveBack() -- N: BotCenter (Center) + + print("The turtle should be in its original location.") + print( + ("The turtle has internal position (%d, %d, %d) and internal direction %d"):format( + self.controller.position.x, + self.controller.position.y, + self.controller.position.z, + self.controller.position.dir + ) + ) + print("Press Enter to continue") + _ = io.read() +end + +function TestingController:TestUnstocking() + print("Testing inventory unloading...") + + self.controller:MoveToPosition(3, 0, 3, Direction.WEST) + print("Please fill the entire inventory with items.") + print("Press Enter to continue") + _ = io.read() + self.controller:UnstockIfFull(true) + print("Press Enter to continue") + _ = io.read() + self.controller:MoveBack() + + print("The turtle should be in its original location.") + print("Press Enter to continue") + _ = io.read() +end + +function TestingController:TestRefueling() + print("Testing refueling...") + + self.controller:MoveToPosition(3, 0, 3, Direction.WEST) + print("Please fill multiple inventory slots with items.") + print("Press Enter to continue") + _ = io.read() + self.controller:RefuelIfEmpty(true) + print("Press Enter to continue") + _ = io.read() + self.controller:MoveBack() + + print("The turtle should be in its original location.") + print("Press Enter to continue") + _ = io.read() +end + +----------------------------------------------------------------------------------------------- +-- Main Method +----------------------------------------------------------------------------------------------- + +function TestingController:Run() + self.controller:Configure() + self.controller:DisableMiningForward() + self.controller:DisableMiningAbove() + self.controller:DisableMiningBelow() + + print("There are multiple tests available:") + print("1: Relative movement with relative rotation") + print("2: Relative movement with absolute rotation") + print("3: Absolute movement without stack") + print("4: Absolute movement with stack") + print("5: All movement tests") + print("6: Unloading") + print("7: Refueling") + print("8: All non-movement tests") + print("9: All tests") + + local choice = 0 + while choice < 1 or choice > 9 do + print("Choose a test by entering its number:") + choice = tonumber(io.read()) or 0 + end + + if choice == 1 then + self:TestRelativeMovementWithRelativeRotation() + elseif choice == 2 then + self:TestRelativeMovementWithAbsoluteRotation() + elseif choice == 3 then + self:TestAbsoluteMovementWithoutStack() + elseif choice == 4 then + self:TestAbsoluteMovementWithStack() + elseif choice == 5 then + self:TestRelativeMovementWithRelativeRotation() + self:TestRelativeMovementWithAbsoluteRotation() + self:TestAbsoluteMovementWithoutStack() + self:TestAbsoluteMovementWithStack() + elseif choice == 6 then + self:TestUnstocking() + elseif choice == 7 then + self:TestRefueling() + elseif choice == 8 then + self:TestUnstocking() + self:TestRefueling() + elseif choice == 9 then + self:TestRelativeMovementWithRelativeRotation() + self:TestRelativeMovementWithAbsoluteRotation() + self:TestAbsoluteMovementWithoutStack() + self:TestAbsoluteMovementWithStack() + self:TestUnstocking() + self:TestRefueling() + end +end + +return TestingController diff --git a/controller/turtle_controller.lua b/controller/turtle_controller.lua index 3e44521..b45330e 100644 --- a/controller/turtle_controller.lua +++ b/controller/turtle_controller.lua @@ -1,462 +1,457 @@ -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_forward boolean ----@field mine_above boolean ----@field mine_below boolean -local TurtleController = {} -TurtleController.__index = TurtleController - - ----@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_forward = false - t.mine_above = false - t.mine_below = 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 % 4 == 0 then - return - end - - -- Turn turtle - local turns = number_of_turns % 4 - if turns == 3 then - -- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, -1)) - - -- If we're rotating by 3, we could turn faster by rotating by 1 in the opposite direction - turtle.turnLeft() - else - -- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, turns)) - - for _ = 1,turns do - turtle.turnRight() - 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 - - self:TurnRelative(direction - self.position.dir) -end - - ----Move forward by a number of blocks depending on the current rotation ----@param number_of_blocks number ----@param skip_unstocking boolean | nil ----@param skip_refueling boolean | nil -function TurtleController:MoveForward(number_of_blocks, skip_unstocking, skip_refueling) - skip_unstocking = skip_unstocking or false - skip_refueling = skip_refueling or false - 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 - if not skip_refueling then - self:RefuelIfEmpty() - end - if not skip_unstocking then - self:UnstockIfFull() - end - - -- Mine/Move - if self.mine_forward then - while not turtle.forward() do - turtle.dig() - end - elseif not turtle.forward() then - error("Turtle failed to move forward!") - end - if self.mine_above then - turtle.digUp() - end - if self.mine_below then - turtle.digDown() - 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 - ----Positive numbers move up, negative numbers move down. ----@param number_of_blocks number ----@param skip_unstocking boolean | nil ----@param skip_refueling boolean | nil -function TurtleController:MoveVertical(number_of_blocks, skip_unstocking, skip_refueling) - skip_unstocking = skip_unstocking or false - skip_refueling = skip_refueling or false - if number_of_blocks == 0 then - return - end - - -- print(("Turtle is moving vertically by %d blocks..."):format(number_of_blocks)) - - local move_function = number_of_blocks > 0 and turtle.up or turtle.down - local mine_function = number_of_blocks > 0 and turtle.digUp or turtle.digDown - local mine_enabled = number_of_blocks > 0 and self.mine_above or self.mine_below - - for _ = 1,math.abs(number_of_blocks) do - if not skip_refueling then - self:RefuelIfEmpty() - end - if not skip_unstocking then - self:UnstockIfFull() - end - - -- Mine - if mine_enabled then - mine_function() - end - - -- Move - if not move_function() then - error("Turtle failed to move vertically!") - end - - -- Update current position - if number_of_blocks > 0 then - self.position.y = self.position.y + 1 - elseif number_of_blocks < 0 then - self.position.y = self.position.y - 1 - end - end -end - - ----Stores the current position on the stack so we can return using TurtleController:MoveBack() -function TurtleController:StorePosition() - self.last_positions:Push(Position:Copy(self.position)) -end - - ----Move to an absolute position. Stores the current position on the stack so we can return using TurtleController:MoveBack() ----@param x number The EAST/WEST axis (grows from WEST to EAST) ----@param y number The UP/DOWN axis (grows from DOWN to UP) ----@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH) ----@param dir Direction ----@param skip_unstocking boolean | nil ----@param skip_refueling boolean | nil -function TurtleController:MoveToPosition(x, y, z, dir, skip_unstocking, skip_refueling) - -- print(("Turtle is moving to (%d, %d, %d)..."):format(x, y, z)) - - -- Store the current position on the stack, so we can return using TurtleController:MoveBack() - self.last_positions:Push(Position:Copy(self.position)) - - -- EAST/WEST axis (do first to not interfere with chests) - if self.position.x > x then - self:TurnToDirection(Direction.WEST) - elseif self.position.x < x then - self:TurnToDirection(Direction.EAST) - end - self:MoveForward(math.abs(x - self.position.x), skip_unstocking, skip_refueling) - - -- 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:MoveForward(math.abs(z - self.position.z), skip_unstocking, skip_refueling) - - -- UP/DOWN axis - self:MoveVertical(y - self.position.y, skip_unstocking, skip_refueling) - - -- 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 - error("TurtleController:MoveToPosition failed to move to target position!") - end -end - - ----Move by a vector. Stores the current position on the stack so we can return using TurtleController:MoveBack() ----@param x number The EAST/WEST axis (grows from WEST to EAST) ----@param y number The UP/DOWN axis (grows from DOWN to UP) ----@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH) ----@param dir Direction ----@param skip_unstocking boolean | nil ----@param skip_refueling boolean | nil -function TurtleController:MoveByVector(x, y, z, dir, skip_unstocking, skip_refueling) - self:MoveToPosition(self.position.x + x, self.position.y + y, self.position.z + z, dir, skip_unstocking, skip_refueling) -end - - ----Move to the previously stored position and pop this position from the stack. ----@param skip_unstocking boolean | nil ----@param skip_refueling boolean | nil -function TurtleController:MoveBack(skip_unstocking, skip_refueling) - local last_position = self.last_positions:Pop() - - if last_position == nil then - error("Failed to obtain last_position to move back!") - else - self:MoveToPosition(last_position.x, last_position.y, last_position.z, last_position.dir, skip_unstocking, skip_refueling) - - -- Pop the stack because MoveToPosition pushes our current position - self.last_positions:Pop() - end -end - - ------------------------------------------------------------------------------------------------ --- Inventory Methods ------------------------------------------------------------------------------------------------ - - ----@return boolean -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 - - ----@return boolean -function TurtleController:HasInventorySpace() - for slot = 1,16 do - if turtle.getItemDetail(slot) == nil then - return true - end - end - - return false -end - - ----@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 - - ----@param skip_inventory_check boolean | nil -function TurtleController:UnstockIfFull(skip_inventory_check) - skip_inventory_check = skip_inventory_check or false - if not skip_inventory_check and self:HasInventorySpace() then - return - end - if skip_inventory_check then - print(("HasInventorySpace() returned %s"):format(self:HasInventorySpace())) - end - - print("Turtle is unstocking...") - - self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true) - self:DropInventory() - self:RefuelIfEmpty() - - self:MoveBack(true, true) -end - - ----@param skip_fuel_check boolean | nil -function TurtleController:RefuelIfEmpty(skip_fuel_check) - skip_fuel_check = skip_fuel_check or false - if not skip_fuel_check and self:HasFuel() then - return - end - if skip_fuel_check then - print(("HasFuel() returned %s"):format(self:HasFuel())) - end - - print("Turtle is refueling...") - - -- Clear our inventory into the storage chest - self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true) - 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 target_fuel_level = self.config.refuel_amount - local last_position = self.last_positions:Peek() - if last_position == nil then - error("Failed to obtain last_position while refueling!") - else - target_fuel_level = target_fuel_level + last_position.x + last_position.y + last_position.z - end - - -- Refuel until we hit the refuel_amount - local before_level = turtle.getFuelLevel() - repeat - if not self:SuckItem(1, 1) then - error("Failed to suck fuel out of fuel chest!") - end - turtle.refuel() - until turtle.getFuelLevel() >= target_fuel_level - local after_level = turtle.getFuelLevel() - - self:MoveBack(true, true) - - 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:EnableMiningForward() - self.mine_forward = true -end - - -function TurtleController:DisableMiningForward() - self.mine_forward = false -end - - -function TurtleController:EnableMiningAbove() - self.mine_above = true -end - - -function TurtleController:DisableMiningAbove() - self.mine_above = false -end - - -function TurtleController:EnableMiningBelow() - self.mine_below = true -end - - -function TurtleController:DisableMiningBelow() - self.mine_below = false -end - - -return TurtleController \ No newline at end of file +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_forward boolean +---@field mine_above boolean +---@field mine_below boolean +local TurtleController = {} +TurtleController.__index = TurtleController + +---@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_forward = false + t.mine_above = false + t.mine_below = 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 % 4 == 0 then + return + end + + -- Turn turtle + local turns = number_of_turns % 4 + if turns == 3 then + -- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, -1)) + + -- If we're rotating by 3, we could turn faster by rotating by 1 in the opposite direction + turtle.turnLeft() + else + -- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, turns)) + + for _ = 1, turns do + turtle.turnRight() + 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 + + self:TurnRelative(direction - self.position.dir) +end + +---Move forward by a number of blocks depending on the current rotation +---@param number_of_blocks number +---@param skip_unstocking boolean | nil +---@param skip_refueling boolean | nil +function TurtleController:MoveForward(number_of_blocks, skip_unstocking, skip_refueling) + skip_unstocking = skip_unstocking or false + skip_refueling = skip_refueling or false + 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 + if not skip_refueling then + self:RefuelIfEmpty() + end + if not skip_unstocking then + self:UnstockIfFull() + end + + -- Mine/Move + if self.mine_forward then + while not turtle.forward() do + turtle.dig() + end + elseif not turtle.forward() then + error("Turtle failed to move forward!") + end + if self.mine_above then + turtle.digUp() + end + if self.mine_below then + turtle.digDown() + 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 + +---Positive numbers move up, negative numbers move down. +---@param number_of_blocks number +---@param skip_unstocking boolean | nil +---@param skip_refueling boolean | nil +function TurtleController:MoveVertical(number_of_blocks, skip_unstocking, skip_refueling) + skip_unstocking = skip_unstocking or false + skip_refueling = skip_refueling or false + if number_of_blocks == 0 then + return + end + + -- print(("Turtle is moving vertically by %d blocks..."):format(number_of_blocks)) + + local move_function = number_of_blocks > 0 and turtle.up or turtle.down + local mine_function = number_of_blocks > 0 and turtle.digUp or turtle.digDown + local mine_enabled = number_of_blocks > 0 and self.mine_above or self.mine_below + + for _ = 1, math.abs(number_of_blocks) do + if not skip_refueling then + self:RefuelIfEmpty() + end + if not skip_unstocking then + self:UnstockIfFull() + end + + -- Mine + if mine_enabled then + mine_function() + end + + -- Move + if not move_function() then + error("Turtle failed to move vertically!") + end + + -- Update current position + if number_of_blocks > 0 then + self.position.y = self.position.y + 1 + elseif number_of_blocks < 0 then + self.position.y = self.position.y - 1 + end + end +end + +---Stores the current position on the stack so we can return using TurtleController:MoveBack() +function TurtleController:StorePosition() + self.last_positions:Push(Position:Copy(self.position)) +end + +---Move to an absolute position. Stores the current position on the stack so we can return using TurtleController:MoveBack() +---@param x number The EAST/WEST axis (grows from WEST to EAST) +---@param y number The UP/DOWN axis (grows from DOWN to UP) +---@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH) +---@param dir Direction +---@param skip_unstocking boolean | nil +---@param skip_refueling boolean | nil +function TurtleController:MoveToPosition(x, y, z, dir, skip_unstocking, skip_refueling) + -- print(("Turtle is moving to (%d, %d, %d)..."):format(x, y, z)) + + -- Store the current position on the stack, so we can return using TurtleController:MoveBack() + self.last_positions:Push(Position:Copy(self.position)) + + -- EAST/WEST axis (do first to not interfere with chests) + if self.position.x > x then + self:TurnToDirection(Direction.WEST) + elseif self.position.x < x then + self:TurnToDirection(Direction.EAST) + end + self:MoveForward(math.abs(x - self.position.x), skip_unstocking, skip_refueling) + + -- 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:MoveForward(math.abs(z - self.position.z), skip_unstocking, skip_refueling) + + -- UP/DOWN axis + self:MoveVertical(y - self.position.y, skip_unstocking, skip_refueling) + + -- 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 + error("TurtleController:MoveToPosition failed to move to target position!") + end +end + +---Move by a vector. Stores the current position on the stack so we can return using TurtleController:MoveBack() +---@param x number The EAST/WEST axis (grows from WEST to EAST) +---@param y number The UP/DOWN axis (grows from DOWN to UP) +---@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH) +---@param dir Direction +---@param skip_unstocking boolean | nil +---@param skip_refueling boolean | nil +function TurtleController:MoveByVector(x, y, z, dir, skip_unstocking, skip_refueling) + self:MoveToPosition( + self.position.x + x, + self.position.y + y, + self.position.z + z, + dir, + skip_unstocking, + skip_refueling + ) +end + +---Move to the previously stored position and pop this position from the stack. +---@param skip_unstocking boolean | nil +---@param skip_refueling boolean | nil +function TurtleController:MoveBack(skip_unstocking, skip_refueling) + local last_position = self.last_positions:Pop() + + if last_position == nil then + error("Failed to obtain last_position to move back!") + else + self:MoveToPosition( + last_position.x, + last_position.y, + last_position.z, + last_position.dir, + skip_unstocking, + skip_refueling + ) + + -- Pop the stack because MoveToPosition pushes our current position + self.last_positions:Pop() + end +end + +----------------------------------------------------------------------------------------------- +-- Inventory Methods +----------------------------------------------------------------------------------------------- + +---@return boolean +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 + +---@return boolean +function TurtleController:HasInventorySpace() + for slot = 1, 16 do + if turtle.getItemDetail(slot) == nil then + return true + end + end + + return false +end + +---@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 + +---@param skip_inventory_check boolean | nil +function TurtleController:UnstockIfFull(skip_inventory_check) + skip_inventory_check = skip_inventory_check or false + if not skip_inventory_check and self:HasInventorySpace() then + return + end + if skip_inventory_check then + print(("HasInventorySpace() returned %s"):format(self:HasInventorySpace())) + end + + print("Turtle is unstocking...") + + self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true) + self:DropInventory() + self:RefuelIfEmpty() + + self:MoveBack(true, true) +end + +---@param skip_fuel_check boolean | nil +function TurtleController:RefuelIfEmpty(skip_fuel_check) + skip_fuel_check = skip_fuel_check or false + if not skip_fuel_check and self:HasFuel() then + return + end + if skip_fuel_check then + print(("HasFuel() returned %s"):format(self:HasFuel())) + end + + print("Turtle is refueling...") + + -- Clear our inventory into the storage chest + self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true) + 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 target_fuel_level = self.config.refuel_amount + local last_position = self.last_positions:Peek() + if last_position == nil then + error("Failed to obtain last_position while refueling!") + else + target_fuel_level = target_fuel_level + last_position.x + last_position.y + last_position.z + end + + -- Refuel until we hit the refuel_amount + local before_level = turtle.getFuelLevel() + repeat + if not self:SuckItem(1, 1) then + error("Failed to suck fuel out of fuel chest!") + end + turtle.refuel() + until turtle.getFuelLevel() >= target_fuel_level + local after_level = turtle.getFuelLevel() + + self:MoveBack(true, true) + + 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:EnableMiningForward() + self.mine_forward = true +end + +function TurtleController:DisableMiningForward() + self.mine_forward = false +end + +function TurtleController:EnableMiningAbove() + self.mine_above = true +end + +function TurtleController:DisableMiningAbove() + self.mine_above = false +end + +function TurtleController:EnableMiningBelow() + self.mine_below = true +end + +function TurtleController:DisableMiningBelow() + self.mine_below = false +end + +return TurtleController diff --git a/lib/direction.lua b/lib/direction.lua index d743911..bb57cc9 100644 --- a/lib/direction.lua +++ b/lib/direction.lua @@ -1,9 +1,9 @@ ----@enum Direction -local Direction = { - NORTH = 0, - EAST = 1, - SOUTH = 2, - WEST = 3 -} - -return Direction \ No newline at end of file +---@enum Direction +local Direction = { + NORTH = 0, + EAST = 1, + SOUTH = 2, + WEST = 3, +} + +return Direction diff --git a/lib/position.lua b/lib/position.lua index 9476870..9edae5b 100644 --- a/lib/position.lua +++ b/lib/position.lua @@ -1,70 +1,69 @@ -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 +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 diff --git a/lib/stack.lua b/lib/stack.lua index 88cebc1..159cbe7 100644 --- a/lib/stack.lua +++ b/lib/stack.lua @@ -1,61 +1,53 @@ -local Position = require("lib.position") - ----@class Stack ----@field elements Position[] -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 Position -function Stack:Push(element) - if element == nil or element == {} then - printError("Failed to push empty element to the stack!") - return - end - - table.insert(self.elements, element) -end - - ----@return Position | nil -function Stack:Pop() - if self:Count() == 0 then - printError("Failed to pop element from the empty stack!") - return nil - end - - return table.remove(self.elements) -end - - ----@return Position | nil -function Stack:Peek() - if self:Count() == 0 then - printError("Failed to peek element from the empty stack!") - return nil - end - - return self.elements[#self.elements] -end - - ----@return number -function Stack:Count() - return #self.elements -end - - -return Stack \ No newline at end of file +---@class Stack +---@field elements Position[] +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 Position +function Stack:Push(element) + if element == nil or element == {} then + printError("Failed to push empty element to the stack!") + return + end + + table.insert(self.elements, element) +end + +---@return Position | nil +function Stack:Pop() + if self:Count() == 0 then + printError("Failed to pop element from the empty stack!") + return nil + end + + return table.remove(self.elements) +end + +---@return Position | nil +function Stack:Peek() + if self:Count() == 0 then + printError("Failed to peek element from the empty stack!") + return nil + end + + return self.elements[#self.elements] +end + +---@return number +function Stack:Count() + return #self.elements +end + +return Stack diff --git a/main.lua b/main.lua index e3050ca..b254898 100644 --- a/main.lua +++ b/main.lua @@ -1,32 +1,30 @@ -local TestingController = require("controller.testing_controller") -local ExcavationController = require("controller.excavation_controller") -local AudioController = require("controller.audio_controller") - - -local controllers = { - TestingController:Create(), - ExcavationController:Create(), - AudioController:Create(), -} - -print("Multiple controllers are available:") -print("1: Testing Mode") -print("2: Volume Excavation") -print("3: Play Bangarang") - -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() - - --- TODO: StackOverflow once the inventory is full, unstocking doesn't work... --- Is the InventoryFull check wrong? --- TODO: Test if there's a chest when dropping/sucking/mining (don't mine chests!) --- TODO: Add controller to build rooms with walls of a specified material --- TODO: Support specifying the mining height --- TODO: Add configurable trash_list with items that won't be picked up --- TODO: Add a speaker to the turtle (listen to music while mining) \ No newline at end of file +local TestingController = require("controller.testing_controller") +local ExcavationController = require("controller.excavation_controller") +local AudioController = require("controller.audio_controller") + +local controllers = { + TestingController:Create(), + ExcavationController:Create(), + AudioController:Create(), +} + +print("Multiple controllers are available:") +print("1: Testing Mode") +print("2: Volume Excavation") +print("3: Play Bangarang") + +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() + +-- TODO: StackOverflow once the inventory is full, unstocking doesn't work... +-- Is the InventoryFull check wrong? +-- TODO: Test if there's a chest when dropping/sucking/mining (don't mine chests!) +-- TODO: Add controller to build rooms with walls of a specified material +-- TODO: Support specifying the mining height +-- TODO: Add configurable trash_list with items that won't be picked up +-- TODO: Add a speaker to the turtle (listen to music while mining)