447 lines
15 KiB
Lua
447 lines
15 KiB
Lua
--[[
|
|
Movescript - A simplified robot script for ComputerCraft.
|
|
|
|
Author: Andrew Lalis <andrewlalisofficial@gmail.com>
|
|
|
|
Movescript provides a simpler, conciser way to program "turtles" (robots), so
|
|
that you don't need to get tired of typing "turtle.forward()" over and over.
|
|
|
|
]]--
|
|
VERSION = "0.0.1"
|
|
|
|
local t = turtle
|
|
-- For testing purposes, if the turtle API is not present, we inject our own.
|
|
if not t then t = {
|
|
getFuelLimit = function() return 1000000000 end
|
|
} end
|
|
|
|
-- The movescript module. Functions defined within this table are exported.
|
|
local movescript = {}
|
|
|
|
movescript.defaultSettings = {
|
|
debug = false,
|
|
safe = true,
|
|
destructive = false,
|
|
fuels = {"minecraft:coal", "minecraft:charcoal"}
|
|
}
|
|
|
|
local INSTRUCTION_TYPES = {
|
|
repeated = 1,
|
|
instruction = 2
|
|
}
|
|
|
|
local function debug(msg, settings)
|
|
if settings and settings.debug then
|
|
print("[MS] " .. msg)
|
|
end
|
|
end
|
|
|
|
-- Helper function for turtle to dig backwards.
|
|
function t.digBack(side)
|
|
t.turnRight()
|
|
t.turnRight()
|
|
t.dig(side)
|
|
t.turnRight()
|
|
t.turnRight()
|
|
end
|
|
|
|
-- Helper function for turtle to detect backwards.
|
|
function t.detectBack()
|
|
t.turnRight()
|
|
t.turnRight()
|
|
local result = t.detect()
|
|
t.turnRight()
|
|
t.turnRight()
|
|
return result
|
|
end
|
|
|
|
local function goDirection(dirFunction, digFunction, detectFunction, settings)
|
|
settings = settings or movescript.defaultSettings
|
|
safe = settings.safe or movescript.defaultSettings.safe
|
|
destructive = settings.destructive or movescript.defaultSettings.destructive
|
|
local success = dirFunction()
|
|
if not safe then return end
|
|
while not success do
|
|
debug("Unable to move.", settings)
|
|
if destructive and detectFunction() then
|
|
debug("Detected a block in the way; attempting to remove it.", settings)
|
|
digFunction()
|
|
end
|
|
success = dirFunction()
|
|
end
|
|
end
|
|
|
|
local function goUp(options, settings)
|
|
debug("Moving up.", settings)
|
|
goDirection(t.up, t.digUp, t.detectUp, settings)
|
|
end
|
|
|
|
local function goDown(options, settings)
|
|
debug("Moving down.", settings)
|
|
goDirection(t.down, t.digDown, t.detectDown, settings)
|
|
end
|
|
|
|
local function goForward(options, settings)
|
|
debug("Moving forward.", settings)
|
|
goDirection(t.forward, t.dig, t.detect, settings)
|
|
end
|
|
|
|
local function goBack(options, settings)
|
|
debug("Moving back.", settings)
|
|
goDirection(t.back, t.digBack, t.detectBack, settings)
|
|
end
|
|
|
|
local function goRight(options, settings)
|
|
debug("Turning right.", settings)
|
|
t.turnRight()
|
|
end
|
|
|
|
local function goLeft(options, settings)
|
|
debug("Turning left.", settings)
|
|
t.turnLeft()
|
|
end
|
|
|
|
local function place(options, settings)
|
|
debug("Placing.", settings)
|
|
t.place(options.text)
|
|
end
|
|
|
|
local function placeUp(options, settings)
|
|
debug("Placing up.", settings)
|
|
t.placeUp(options.text)
|
|
end
|
|
|
|
local function placeDown(options, settings)
|
|
debug("Placing down.", settings)
|
|
t.placeDown(options.text)
|
|
end
|
|
|
|
local function attack(options, settings)
|
|
debug("Attacking.", settings)
|
|
t.attack(options.side)
|
|
end
|
|
|
|
local function attackUp(options, settings)
|
|
debug("Attacking up.", settings)
|
|
t.attackUp(options.side)
|
|
end
|
|
|
|
local function attackDown(options, settings)
|
|
debug("Attacking down.", settings)
|
|
t.attackDown(options.side)
|
|
end
|
|
|
|
local function dig(options, settings)
|
|
debug("Digging.", settings)
|
|
t.dig(options.side)
|
|
end
|
|
|
|
local function digUp(options, settings)
|
|
debug("Digging up.", settings)
|
|
t.digUp(options.side)
|
|
end
|
|
|
|
local function digDown(options, settings)
|
|
debug("Digging down.", settings)
|
|
t.digDown(options.side)
|
|
end
|
|
|
|
local function suck(options, settings)
|
|
debug("Sucking.", settings)
|
|
local count = nil
|
|
if options.count ~= nil then
|
|
count = tonumber(options.count)
|
|
end
|
|
t.suck(count)
|
|
end
|
|
|
|
local function suckUp(options, settings)
|
|
debug("Sucking up.", settings)
|
|
local count = nil
|
|
if options.count ~= nil then
|
|
count = tonumber(options.count)
|
|
end
|
|
t.suckUp(count)
|
|
end
|
|
|
|
local function suckDown(options, settings)
|
|
debug("Sucking down.", settings)
|
|
local count = nil
|
|
if options.count ~= nil then
|
|
count = tonumber(options.count)
|
|
end
|
|
t.suckDown(count)
|
|
end
|
|
|
|
local function selectSlot(options, settings)
|
|
local slot = 1
|
|
if options.slot ~= nil then
|
|
slot = tonumber(options.slot)
|
|
end
|
|
debug("Selecting slot " .. slot .. ".", settings)
|
|
t.select(slot)
|
|
end
|
|
|
|
local function drop(options, settings)
|
|
debug("Dropping.", settings)
|
|
local count = nil
|
|
if options.count ~= nil then
|
|
count = tonumber(options.count)
|
|
end
|
|
t.drop(count)
|
|
end
|
|
|
|
local function dropUp(options, settings)
|
|
debug("Dropping up.", settings)
|
|
local count = nil
|
|
if options.count ~= nil then
|
|
count = tonumber(options.count)
|
|
end
|
|
t.dropUp(count)
|
|
end
|
|
|
|
local function dropDown(options, settings)
|
|
debug("Dropping down.", settings)
|
|
local count = nil
|
|
if options.count ~= nil then
|
|
count = tonumber(options.count)
|
|
end
|
|
t.dropDown(count)
|
|
end
|
|
|
|
local actionMap = {
|
|
["U"] = {f = goUp, needsFuel = true},
|
|
["D"] = {f = goDown, needsFuel = true},
|
|
["L"] = {f = goLeft, needsFuel = false},
|
|
["R"] = {f = goRight, needsFuel = false},
|
|
["F"] = {f = goForward, needsFuel = true},
|
|
["B"] = {f = goBack, needsFuel = true},
|
|
["P"] = {f = place, needsFuel = false},
|
|
["Pu"] = {f = placeUp, needsFuel = false},
|
|
["Pd"] = {f = placeDown, needsFuel = false},
|
|
["A"] = {f = attack, needsFuel = false},
|
|
["Au"] = {f = attackUp, needsFuel = false},
|
|
["Ad"] = {f = attackDown, needsFuel = false},
|
|
["Dg"] = {f = dig, needsFuel = false},
|
|
["Dgu"] = {f = digUp, needsFuel = false},
|
|
["Dgd"] = {f = digDown, needsFuel = false},
|
|
["S"] = {f = suck, needsFuel = false},
|
|
["Su"] = {f = suckUp, needsFuel = false},
|
|
["Sd"] = {f = suckDown, needsFuel = false},
|
|
["Eqr"] = {f = t.equipRight, needsFuel = false},
|
|
["Eql"] = {f = t.equipLeft, needsFuel = false},
|
|
["Sel"] = {f = selectSlot, needsFuel = false},
|
|
["Dr"] = {f = drop, needsFuel = false},
|
|
["Dru"] = {f = dropUp, needsFuel = false},
|
|
["Drd"] = {f = dropDown, needsFuel = false}
|
|
}
|
|
|
|
-- Tries to refuel the turtle from all slots that contain a valid fuel.
|
|
-- Returns a boolean indicating if at least one piece of fuel was consumed.
|
|
local function refuelAll(settings)
|
|
debug("Refueling...", settings)
|
|
local fuels = settings.fuels or movescript.defaultSettings.fuels
|
|
local refueled = false
|
|
for slot = 1, 16 do
|
|
local item = t.getItemDetail(slot)
|
|
if item ~= nil then
|
|
for _, fuelName in pairs(fuels) do
|
|
if item.name == fuelName then
|
|
t.select(slot)
|
|
if t.refuel(item.count) then refueled = true end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return refueled
|
|
end
|
|
|
|
-- Blocks until the turtle's fuel level is at least at the required level.
|
|
local function refuelToAtLeast(requiredLevel, settings)
|
|
refuelAll(settings)
|
|
while t.getFuelLevel() < requiredLevel do
|
|
print(
|
|
"[MS] Fuel level is too low. Level: " .. t.getFuelLevel() .. ". Required: " .. requiredLevel ..
|
|
". Please add some of the following fuels:"
|
|
)
|
|
local fuels = settings.fuels or movescript.defaultSettings.fuels
|
|
for _, fuelName in pairs(fuels) do
|
|
print(" - " .. fuelName)
|
|
end
|
|
local fuelUpdated = false
|
|
while not fuelUpdated do
|
|
os.pullEvent("turtle_inventory")
|
|
fuelUpdated = refuelAll(settings)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Executes a single instruction. An instruction is a table with an "action"
|
|
-- and some attributes, such as if it needs fuel or not.
|
|
local function executeInstruction(instruction, settings)
|
|
if instruction.type == INSTRUCTION_TYPES.repeated then
|
|
debug("Executing repeated instruction " .. instruction.count .. " times.", settings)
|
|
for i = 1, instruction.count do
|
|
for _, nestedInstruction in pairs(instruction.instructions) do
|
|
executeInstruction(nestedInstruction, settings)
|
|
end
|
|
end
|
|
elseif instruction.type == INSTRUCTION_TYPES.instruction then
|
|
local action = actionMap[instruction.action]
|
|
if action then
|
|
debug("Executing action \"" .. instruction.action .. "\" " .. instruction.count .. " times.", settings)
|
|
local shouldRefuel = (
|
|
(settings.safe or true) and
|
|
(action.needsFuel) and
|
|
(instruction.count > t.getFuelLevel())
|
|
)
|
|
if shouldRefuel then
|
|
local fuelRequired = instruction.count
|
|
refuelToAtLeast(fuelRequired, settings)
|
|
end
|
|
for i = 1, instruction.count do
|
|
action.f(instruction.options, settings)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function parseInstructionOptions(text, settings)
|
|
local idx, endIdx = string.find(text, "%b()")
|
|
if idx == nil or endIdx - idx < 4 then return nil end
|
|
local optionPairsText = string.sub(text, idx, endIdx)
|
|
debug("Parsing instruction options: " .. optionPairsText, settings)
|
|
local options = {}
|
|
local nextIdx = 1
|
|
while nextIdx < string.len(optionPairsText) do
|
|
idx, endIdx = string.find(optionPairsText, "%w+=[%w_-%.]+", nextIdx)
|
|
if idx == nil then break end
|
|
local pairText = string.sub(optionPairsText, idx, endIdx)
|
|
local keyIdx, keyEndIdx = string.find(pairText, "%w+")
|
|
local key = string.sub(pairText, keyIdx, keyEndIdx)
|
|
local valueIdx, valueEndIdx = string.find(pairText, "[%w_-%.]+", keyEndIdx + 2)
|
|
local value = string.sub(pairText, valueIdx, valueEndIdx)
|
|
options[key] = value
|
|
debug(" Found option: key = " .. key .. ", value = " .. value, settings)
|
|
nextIdx = endIdx + 2
|
|
end
|
|
return options
|
|
end
|
|
|
|
local function parseRepeatedInstruction(match, settings)
|
|
debug("Parsing repeated instruction: " .. match, settings)
|
|
local instruction = {}
|
|
instruction.type = INSTRUCTION_TYPES.repeated
|
|
local countIdx, countEndIdx = string.find(match, "%d+")
|
|
instruction.count = tonumber(string.sub(match, countIdx, countEndIdx))
|
|
if instruction.count < 0 then
|
|
error("Repeated instruction cannot have a negative count.")
|
|
end
|
|
local innerScriptIdx, innerScriptEndIdx = string.find(match, "%b()", countEndIdx + 1)
|
|
local innerScript = string.sub(match, innerScriptIdx + 1, innerScriptEndIdx - 1)
|
|
instruction.instructions = movescript.parse(innerScript, settings)
|
|
return instruction
|
|
end
|
|
|
|
local function parseInstruction(match, settings)
|
|
debug("Parsing instruction: " .. match, settings)
|
|
local instruction = {}
|
|
instruction.type = INSTRUCTION_TYPES.instruction
|
|
local countIdx, countEndIdx = string.find(match, "%d+")
|
|
instruction.count = 1
|
|
if countIdx ~= nil then
|
|
instruction.count = tonumber(string.sub(match, countIdx, countEndIdx))
|
|
end
|
|
if instruction.count < 1 or instruction.count > t.getFuelLimit() then
|
|
error("Instruction at index " .. actionIdx .. " has an invalid count of " .. instruction.count .. ". It should be >= 1 and <= " .. t.getFuelLimit())
|
|
end
|
|
local actionIdx, actionEndIdx = string.find(match, "%u%l*")
|
|
instruction.action = string.sub(match, actionIdx, actionEndIdx)
|
|
if actionMap[instruction.action] == nil then
|
|
error("Instruction at index " .. actionIdx .. ", \"" .. instruction.action .. "\", does not refer to a valid action.")
|
|
end
|
|
return instruction
|
|
end
|
|
|
|
-- Parses a movescript script into a series of instruction tables.
|
|
--[[
|
|
Movescript Grammar:
|
|
block: instruction | repeatedInstructions
|
|
|
|
repeatedInstructions: count '(' {instruction | repeatedInstructions} ')'
|
|
regex: %d+%s*%b()
|
|
instruction: [count] action [actionOptions] <- Not yet implemented.
|
|
regex: %d*%u%l*
|
|
count: %d+
|
|
|
|
action: %u%l*
|
|
|
|
actionOptions: '(' {optionPair ','} ')'
|
|
regex: %b()
|
|
|
|
optionPair: optionKey '=' optionValue
|
|
|
|
optionKey: %w+
|
|
|
|
optionValue: [%w_-]+
|
|
|
|
]]--
|
|
function movescript.parse(script, settings)
|
|
local instructions = {}
|
|
local scriptIdx = 1
|
|
while scriptIdx <= string.len(script) do
|
|
local instruction = {}
|
|
local repeatedMatchStartIdx, repeatedMatchEndIdx = string.find(script, "%d+%s*%b()", scriptIdx)
|
|
local instructionMatchStartIdx, instructionMatchEndIdx = string.find(script, "%d*%u%l*", scriptIdx)
|
|
-- Parse the first occurring matched pattern.
|
|
if repeatedMatchStartIdx ~= nil and (instructionMatchStartIdx == nil or repeatedMatchStartIdx < instructionMatchStartIdx) then
|
|
-- Parse repeated instructions.
|
|
local match = string.sub(script, repeatedMatchStartIdx, repeatedMatchEndIdx)
|
|
table.insert(instructions, parseRepeatedInstruction(match, settings))
|
|
scriptIdx = repeatedMatchEndIdx + 1
|
|
elseif instructionMatchStartIdx ~= nil and (repeatedMatchStartIdx == nil or instructionMatchStartIdx < repeatedMatchStartIdx) then
|
|
-- Parse single instruction.
|
|
local match = string.sub(script, instructionMatchStartIdx, instructionMatchEndIdx)
|
|
local instruction = parseInstruction(match, settings)
|
|
local optionsIdx, optionsEndIdx = string.find(script, "%s*%b()", instructionMatchEndIdx + 1)
|
|
if optionsIdx ~= nil then
|
|
-- Check that there's nothing but empty space between the instruction and the options text.
|
|
if not string.find(string.sub(script, instructionMatchEndIdx + 1, optionsIdx - 1), "%S+") then
|
|
local optionsText = string.sub(script, optionsIdx, optionsEndIdx)
|
|
instruction.options = parseInstructionOptions(optionsText, settings)
|
|
end
|
|
end
|
|
if instruction.options == nil then instruction.options = {} end
|
|
table.insert(instructions, instruction)
|
|
scriptIdx = instructionMatchEndIdx + 1
|
|
else
|
|
error("Invalid script characters found at index " .. scriptIdx)
|
|
end
|
|
end
|
|
return instructions
|
|
end
|
|
|
|
function movescript.run(script, settings)
|
|
settings = settings or movescript.defaultSettings
|
|
script = script or ""
|
|
debug("Executing script: " .. script, settings)
|
|
local instructions = movescript.parse(script, settings)
|
|
for idx, instruction in pairs(instructions) do
|
|
executeInstruction(instruction, settings)
|
|
end
|
|
end
|
|
|
|
function movescript.runFile(filename, settings)
|
|
local f = fs.open(filename, "r")
|
|
local script = f.readAll()
|
|
f.close()
|
|
movescript.run(script, settings)
|
|
end
|
|
|
|
function movescript.validate(script, settings)
|
|
return pcall(function () movescript.parse(script, settings) end)
|
|
end
|
|
|
|
return movescript
|