Implement base TurtleController and simple VolumeExcavationController
This commit is contained in:
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Lua.diagnostics.globals": [
|
||||
"printError",
|
||||
"turtle",
|
||||
"shell"
|
||||
]
|
||||
}
|
||||
57
controller/testing_controller.lua
Normal file
57
controller/testing_controller.lua
Normal file
@ -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
|
||||
349
controller/turtle_controller.lua
Normal file
349
controller/turtle_controller.lua
Normal file
@ -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
|
||||
130
controller/volume_excavation_controller.lua
Normal file
130
controller/volume_excavation_controller.lua
Normal file
@ -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
|
||||
9
lib/direction.lua
Normal file
9
lib/direction.lua
Normal file
@ -0,0 +1,9 @@
|
||||
---@enum Direction
|
||||
local Direction = {
|
||||
NORTH = 0,
|
||||
EAST = 1,
|
||||
SOUTH = 2,
|
||||
WEST = 3
|
||||
}
|
||||
|
||||
return Direction
|
||||
70
lib/position.lua
Normal file
70
lib/position.lua
Normal file
@ -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
|
||||
48
lib/stack.lua
Normal file
48
lib/stack.lua
Normal file
@ -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
|
||||
20
main.lua
Normal file
20
main.lua
Normal file
@ -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()
|
||||
Reference in New Issue
Block a user