Added repeated instructions and action options.

This commit is contained in:
Andrew Lalis 2022-12-23 11:37:40 +01:00
parent 821a29175d
commit 3fdc6c01f0
6 changed files with 179 additions and 65 deletions

View File

@ -22,27 +22,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
# Disabled minification for now.
# - name: Install Lua
# uses: ljmf00/setup-lua@v1.0.0
# with:
# lua-version: 5.3
# install-luarocks: true
# - name: Minify Scripts
# run: |
# script_dir=docs/src/.vuepress/public/scripts
# rm -rf $script_dir
# mkdir $script_dir
# lua minify.lua minify src/movescript.lua > $script_dir/movescript.lua
# lua minify.lua minify src/itemscript.lua > $script_dir/itemscript.lua
# - name: Clean Up Lua Artifacts
# run: |
# rm -rf .lua
# rm -rf .luarocks
# rm -rf .source
- name: Copy scripts to Docs public assets
run: |
script_dir=docs/src/.vuepress/public/scripts
@ -50,6 +29,24 @@ jobs:
mkdir $script_dir
cp src/* $script_dir/
- name: Install Lua
uses: ljmf00/setup-lua@v1.0.0
with:
lua-version: 5.3
install-luarocks: true
- name: Minify Scripts
run: |
script_dir=docs/src/.vuepress/public/scripts
lua minify.lua minify src/movescript.lua > $script_dir/movescript-min.lua
lua minify.lua minify src/itemscript.lua > $script_dir/itemscript-min.lua
- name: Clean Up Lua Artifacts
run: |
rm -rf .lua
rm -rf .luarocks
rm -rf .source
- name: Setup Node
uses: actions/setup-node@v3

View File

@ -7,6 +7,10 @@ local ms = require("movescript")
ms.run("2F")
```
## `parse(script, settings)`
Parses the given `script` string and returns a table containing the parsed instructions to be executed. This is mostly useful for debugging your scripts.
## `run(script, settings)`
Runs the given `script` string as a movescript, and optionally a `settings` table can be provided. Otherwise, [default settings](settings.md) will be used.

View File

@ -2,7 +2,7 @@
Every movescript must follow the outline defined in this specification.
Each script consists of zero or more **instructions**, separated by zero or more whitespace characters.
Each script consists of zero or more **instructions** or **repeated instructions**, separated by zero or more whitespace characters.
## Instructions
@ -10,11 +10,23 @@ An instruction consists of an optional positive integer number, followed by a re
```lua
-- The regex used to parse instructions.
instruction = string.find(script, "%W*(%d*%u%l*)%W*")
instruction = string.find(script, "%s*(%d*%u%l*)%s*")
```
Each instruction can be split into two parts: the **action**, and the **count**. The action is the textual part of the instruction, and maps to a turtle behavior. The count is the optional numerical part of the instruction, and defaults to `1` if no number is provided.
Here are some examples of valid instructions: `3F`, `U`, `1R`
Some instructions may allow you to specify additional options. These can be defined as key-value pairs in parentheses after the action part.
For example: `4A(delay=0.25, file=tmp.txt)`
## Repeated Instructions
A repeated instruction is a grouping of instructions that are repeated a specified number of times. It's denoted as a positive integer number, followed by a series of [instructions](#instructions) within parentheses.
For example: `22(AF)` - We execute the instructions `A` and `F` 22 times.
## Actions
The following table lists all actions that are available in Movescript. Attempting to invoke an action not listed here will result in an error that will terminate your script.

View File

@ -1,24 +0,0 @@
--[[
Installation script for installing all libraries.
Run `wget run https://raw.githubusercontent.com/andrewlalis/movescript/main/install.lua`
to run the installer on your device.
]]--
BASE_URL = "https://raw.githubusercontent.com/andrewlalis/movescript/main/"
SCRIPTS = {
"movescript.lua",
"itemscript.lua"
}
-- Create a local executable to re-install, instead of having to run this file via wget.
local f = io.open("install-movescript.lua", "w")
for _, script in pairs(SCRIPTS) do
url = BASE_URL .. script
cmd = "wget " .. url .. " " .. script
shell.run(cmd)
f:write("if fs.exists(\"" .. script .. "\") then fs.delete(\"" .. script .. "\") end")
f:write("shell.run(\"" .. cmd .. "\")")
end
f:close()

View File

@ -10,6 +10,10 @@ 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 = {}
@ -167,25 +171,121 @@ local function executeInstruction(instruction, settings)
end
end
local INSTRUCTION_TYPES = {
repeated = 1,
instruction = 2
}
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.
local function parseScript(script, settings)
--[[
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 = {}
for instruction in string.gfind(script, "%W*(%d*%u%l*)%W*") do
local countIdx, countIdxEnd = string.find(instruction, "%d+")
local actionIdx, actionIdxEnd = string.find(instruction, "%u%l*")
local count = 1
if countIdx ~= nil then
count = tonumber(string.sub(instruction, countIdx, countIdxEnd))
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
table.insert(instructions, instruction)
scriptIdx = instructionMatchEndIdx + 1
else
error("Invalid script characters found at index " .. scriptIdx)
end
local action = string.sub(instruction, actionIdx, actionIdxEnd)
if count < 1 or count > t.getFuelLimit() then
error("Instruction at index " .. actionIdx .. " has an invalid count of " .. count .. ". It should be >= 1 and <= " .. t.getFuelLimit())
end
if actionMap[action] == nil then
error("Instruction at index " .. actionIdx .. ", \"" .. action .. "\", does not refer to a valid action.")
end
table.insert(instructions, {action = action, count = count})
debug("Parsed instruction: " .. instruction, settings)
end
return instructions
end
@ -194,7 +294,7 @@ function movescript.run(script, settings)
settings = settings or movescript.defaultSettings
script = script or ""
debug("Executing script: " .. script, settings)
local instructions = parseScript(script, settings)
local instructions = movescript.parse(script, settings)
for idx, instruction in pairs(instructions) do
executeInstruction(instruction, settings)
end
@ -208,7 +308,7 @@ function movescript.runFile(filename, settings)
end
function movescript.validate(script, settings)
return pcall(function () parseScript(script, settings) end)
return pcall(function () movescript.parse(script, settings) end)
end
return movescript

25
tester.lua Normal file
View File

@ -0,0 +1,25 @@
-- http://lua-users.org/wiki/TableSerialization
function print_r (t, name, indent)
local tableList = {}
function table_r (t, name, indent, full)
local serial=string.len(full) == 0 and name
or type(name)~="number" and '["'..tostring(name)..'"]' or '['..name..']'
io.write(indent,serial,' = ')
if type(t) == "table" then
if tableList[t] ~= nil then io.write('{}; -- ',tableList[t],' (self reference)\n')
else
tableList[t]=full..serial
if next(t) then -- Table not empty
io.write('{\n')
for key,value in pairs(t) do table_r(value,key,indent..'\t',full..serial) end
io.write(indent,'};\n')
else io.write('{};\n') end
end
else io.write(type(t)~="number" and type(t)~="boolean" and '"'..tostring(t)..'"'
or tostring(t),';\n') end
end
table_r(t,name or '__unnamed__',indent or '','')
end
local ms = require("src/movescript")
print_r(ms.parse("35(2F(safe=false)R 3(L(delay=0.25, file=file.txt)UB))", {debug=true}))