1
Files
lua-computercraft/controller/turtle_controller.lua

364 lines
11 KiB
Lua

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
self:RefuelIfEmpty()
-- 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
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 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
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))
if self.position.z > 1 then
-- Move south once (if we're at the top) to not be blocked by the slice that is currently mined.
-- This assumes that we mine the full width back to front.
self:TurnToDirection(Direction.SOUTH)
self:MoveRelative(1)
elseif self.position.z == 0 then
-- Move north once (if we're at the bottom) to not be blocked by the chests
self:TurnToDirection(Direction.NORTH)
self:MoveRelative(1)
end
-- 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()
if target_position == nil then
shell.exit()
else
self:MoveToPosition(target_position.x, target_position.y, target_position.z, target_position.dir)
self.last_positions:Pop()
end
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
---@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
print("Turtle is unstocking...")
self:MoveToPosition(0, 0, 0, self.config.storage_direction)
self:DropInventory()
self:RefuelIfEmpty()
self:MoveBack()
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
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 target_fuel_level = self.config.refuel_amount
local last_position = self.last_positions:Peek()
if last_position == nil then
shell.exit()
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
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