singe/testScripts/tutorialDrive.singe
2026-09-14 22:32:53 -05:00

186 lines
5.1 KiB
Text

-- What the tutorial scenes share: the keys a reader presses, pressed the same way, and the two
-- checks every tutorial ends on. A tutorial in docs/Forge.adoc names keys; its scene presses
-- them through here, so the text and the test cannot drift apart without the sweep saying so.
--
-- A scene sets TUTORIAL (its number and name) before loading this, and calls tutorialBegin to
-- open a description, tutorialShot to take a named picture, and tutorialEnd to check the result
-- against the shipped description and run the built game.
FORGE_LIBRARY = true
dofile("Forge/Forge.singe")
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
fontSelect(font)
fontQuality(FONT_QUALITY_BLENDED)
overlaySetResolution(720, 480)
TUTORIAL_OUT = singeGetDataPath()
-- The engine hands onKeyPressed two integers; so does this.
function key(code, character)
forgeKey(character or 0, code.value)
end
-- Types text as a person would, one character at a time.
function typeText(text)
for at = 1, #text do
forgeKey(string.byte(text, at), 0)
end
end
-- ENTER until the field is the one being edited. Answers whether it was reached.
local function reach(field)
local guard = 0
while (FORGE.lastField ~= field) and (guard < 40) do
key(SCANCODE.RETURN)
guard = guard + 1
end
if FORGE.lastField ~= field then
debugPrint(string.format("%s FAIL the form never reached %s", TUTORIAL.tag, field))
return false
end
return true
end
-- ESC out of the form: off a list first, then off the field it left being typed.
local function leave()
if FORGE.picker ~= nil then
key(SCANCODE.ESCAPE)
end
if FORGE.editing ~= nil then
key(SCANCODE.ESCAPE)
end
end
-- Fills in one field of the form ENTER walks: ENTER until the field is the one being edited,
-- type the value, ENTER to keep it, ESC to leave the form. Exactly the keys the text names.
-- A field with a list narrows it to the value and takes it, which moves on to the next field.
function form(field, value)
if not reach(field) then
return
end
if FORGE.picker ~= nil then
pick(value)
else
typeText(tostring(value))
key(SCANCODE.RETURN)
end
leave()
end
-- Empties one field of the form: ENTER until it is the one being edited, DELETE, ESC.
function clear(field)
if not reach(field) then
return
end
if FORGE.picker ~= nil then
pick("(none)")
else
key(SCANCODE.DELETE)
end
leave()
end
-- Narrows the picker by typing and takes the first match.
function pick(name)
typeText(name)
key(SCANCODE.RETURN)
end
-- Selects the entity with an id, in the entities panel.
function selectEntity(id)
for index, entry in ipairs(forgeRoom().entities) do
if entry.id == id then
forgeSelect(index)
return
end
end
debugPrint(string.format("%s FAIL no entity called %s", TUTORIAL.tag, id))
end
-- Puts the panel on a mode, by TAB as a reader would.
function panel(mode)
local guard = 0
while (FORGE.mode ~= mode) and (guard < 6) do
key(SCANCODE.TAB)
guard = guard + 1
end
end
-- A picture for the book, drawn now and named.
function tutorialShot(name)
singeScreenshot(string.format("%02d-%s", TUTORIAL.number, name))
end
-- Opens the tutorial's working copy: a fresh New game, or the previous tutorial's shipped
-- description, so each starts where the last left off.
function tutorialBegin(fromShipped)
local work = TUTORIAL_OUT .. TUTORIAL.name .. ".game"
if fromShipped then
if not authorCopy("Forge/tutorials/" .. fromShipped .. ".game", work) then
debugPrint(TUTORIAL.tag .. " FAIL could not copy " .. fromShipped)
singeQuit()
end
else
authorSave(forgeTemplate(), work)
end
if not forgeBegin(work) then
debugPrint(TUTORIAL.tag .. " FAIL could not open " .. work)
singeQuit()
end
return work
end
-- The two checks: what the keys made equals the shipped description, compiled; and the game it
-- makes runs. A shipped description that is not there yet is written, so a new tutorial's scene
-- produces the file the book ships, and the next run checks against it.
function tutorialEnd()
local shipped = "Forge/tutorials/" .. TUTORIAL.name .. ".game"
local built
forgeSave()
-- The compiled header names the description's file, which a release has set and a shipped
-- copy has not; the comparison is of the game, not of where it came from.
FORGE.game.source = nil
if lfs.attributes(shipped, "mode") ~= "file" then
authorCopy(FORGE.path, TUTORIAL_OUT .. TUTORIAL.name .. ".shipped.game")
debugPrint(TUTORIAL.tag .. " WROTE " .. TUTORIAL_OUT .. TUTORIAL.name .. ".shipped.game -- copy it to " .. shipped)
else
local mine = authorCompile(FORGE.game)
local theirs = authorCompile(authorLoad(shipped))
if mine == theirs then
debugPrint(TUTORIAL.tag .. " the keys made the shipped description")
else
local out = assert(io.open(TUTORIAL_OUT .. TUTORIAL.name .. ".mine.singe", "w"))
out:write(mine)
out:close()
out = assert(io.open(TUTORIAL_OUT .. TUTORIAL.name .. ".theirs.singe", "w"))
out:write(theirs)
out:close()
debugPrint(TUTORIAL.tag .. " FAIL the keys made something else; see " .. TUTORIAL_OUT .. TUTORIAL.name .. ".mine.singe")
end
end
built = forgeBuild(TUTORIAL_OUT .. TUTORIAL.name .. ".singe")
forgeClose()
return built
end