-- Forge (FORGE.md): compiles all four sample descriptions and plays the platformer. -- -- The descriptions share nothing -- physics with a character, disc frames and time windows, a -- shoot-em-up of spawned types, a gun over video -- so compiling all of them here is the test -- that the core has no genre baked into it. -- Nothing in this scene knows what a platformer is; it drives keys and looks at the result. dofile("Forge/AuthorCompile.singe") local font = fontLoad("Singe/FreeSansBold.ttf", 18) local built = {} local out = singeGetDataPath() fontSelect(font) fontQuality(FONT_QUALITY_BLENDED) overlaySetResolution(720, 480) for _, name in ipairs({ "platformer", "qte", "shmup", "gunvideo" }) do local game = authorLoad("testScripts/author/" .. name .. ".game") local problems = authorCheck(game) built[name] = authorBuild("testScripts/author/" .. name .. ".game", out .. name .. ".singe") debugPrint("AUTHOR built " .. built[name] .. " with " .. #problems .. " problems") for _, problem in ipairs(problems) do debugPrint("AUTHOR FAIL " .. name .. ": " .. problem) end end -- Both must at least load as Lua; a compiler that emits something unparseable is the failure this -- catches earliest, and it costs nothing to check both even though only one is played. for name, path in pairs(built) do local chunk, problem = loadfile(path) if chunk == nil then debugPrint("AUTHOR FAIL " .. name .. " does not parse: " .. tostring(problem)) else debugPrint("AUTHOR parsed " .. name) end end -- Now run the platformer, as the engine would if it had been launched directly. dofile(built.platformer) local gameUpdate = onOverlayUpdate local frames = 0 -- Driving by what the game is doing, not by a clock or a frame count. -- -- Both of those were tried and both are wrong. A frame count is wrong because the authored game -- moves in seconds. A clock is wrong because physics steps at most MAX_STEPS_PER_FRAME (4, so -- 66 ms) per frame and deliberately falls behind wall clock on a machine that cannot keep up -- -- which the sanitizer build, with a disc to decode, does. A test that watches the hero instead -- gives the same answer at any frame rate, and exercises onGround from this side as well. local JUMP_AT = 340 -- Where the hero leaves the ground to reach the ledge. local GIVE_UP = 20.0 -- Seconds. Generous: this is a stuck detector, not a schedule. local held = false local jumped = false local won = 0 local shots = 0 function onOverlayUpdate() local t = authorTime() local hero = authorEntity("hero") local hx, hy = authorPosition(hero) frames = frames + 1 if not held then authorKeyDown(0, SCANCODE.RIGHT.value) held = true end -- Jump once, when the hero is under way and has ground beneath it. if (not jumped) and (hx >= JUMP_AT) and playerIsOnGround(hero.node) then authorKeyDown(0, SCANCODE.SPACE.value) jumped = true elseif jumped then authorKeyUp(0, SCANCODE.SPACE.value) end local r = gameUpdate() -- The engine's own text over the authored game, so a screenshot says what it is looking at. colorForeground(255, 255, 255, 255) fontPrint(12, 450, string.format("authored platformer, %.1fs, score %d", t, AUTHOR_VARS.score)) -- Four shots at the moments that matter, found by watching rather than by timing: the start, -- the run, the jump, and the frame after the prize was taken. if (shots == 0) or (shots == 1 and hx > 250) or (shots == 2 and jumped and not playerIsOnGround(hero.node)) or (shots == 3 and won > 0 and t > won + 0.3) then shots = shots + 1 singeScreenshot() end if AUTHOR_VARS.won and won == 0 then won = t end if (won > 0 and t > won + 0.6) or (t > GIVE_UP) then debugPrint("AUTHOR RESULT score=" .. AUTHOR_VARS.score .. " flagWon=" .. tostring(AUTHOR_VARS.won) .. " after=" .. string.format("%.1fs", t)) singeQuit() end return r end