-- How a KarisFramework game and its Forge port are driven for comparison (FORGE.md section -- 14.1): the same policy, read off the same state in either. A run presses START on the -- attract loop, then answers each move a few frames into its window -- the right answer, or a -- wrong one for every fifth move so deaths and continues are exercised -- and takes the one -- continue offered. Both scripts hand this file a view of their state (karisView) and a way -- to press and release a switch (karisPress); this file does the deciding. -- -- The trace both print: every disc seek, and every change of the framework's screen, step, -- level, scene, move, score, and lives, in the framework's numbers. KARIS_SEED = 20260914 local lastLine = nil local pressed = nil -- The switch held down this frame, released the next. local started = false local coined = false local continued = false local answered = {} -- Windows already answered, by "level/scene/move/lives". local tried = {} -- Moves met before, by "level/scene/move": a retry after a death. -- What the drive does with a move, so that every path of a loop is walked: most moves are -- answered right; every fifth is answered wrong (a stray input, a wrong option, a mash -- broken by a stray input); every seventh is left unanswered (the window runs out). A move -- met again after a death is answered right. PORT_WRONG_EVERY and PORT_MISS_EVERY set the -- two counts, for a run that goes deeper into a game with few lives. local WRONG_EVERY = tonumber(os.getenv("PORT_WRONG_EVERY")) or 5 local MISS_EVERY = tonumber(os.getenv("PORT_MISS_EVERY")) or 7 local function wrongMove(view) return (view.move % WRONG_EVERY == 0) and not view.retry end local function missedMove(view) return (view.move % MISS_EVERY == 0) and (view.move % WRONG_EVERY ~= 0) and not view.retry end local seeks = 0 local plan = nil -- Kimmy: the presses planned for the window being answered. local held = {} -- Kimmy: the switches the plan holds down, by switch. local menuAt = 0 -- Kimmy: the frame a menu was last answered on. KARIS_FRAMES = tonumber(os.getenv("PORT_FRAMES")) or 60000 -- The framework's move numbers as switches; Kimmy's diagonals are two of them. local SWITCH_OF = { [1] = SWITCH_UP, [2] = SWITCH_DOWN, [3] = SWITCH_LEFT, [4] = SWITCH_RIGHT, [5] = SWITCH_BUTTON1, [6] = SWITCH_BUTTON2, [7] = SWITCH_BUTTON3 } local PAIR_OF = { [9] = { 1, 3 }, [10] = { 1, 4 }, [11] = { 2, 3 }, [12] = { 2, 4 } } local KIMMY = { COMBO = 20, MULTI = 21, LOOP = 22, MASH = 23, HOLD = 24, LETGO = 25, PATH = 51, YESNO = 52, TIMED = 53, ANYTHING = 54, NOTHING = 55, UD = 151, LR = 153, B1B2 = 155 } -- The KarisFramework's compound kinds, and the MazescaterFramework's (LINEA), as what they -- take, by the framework's numbers: a sequence's inputs in turn (a circle is five, back to -- its start), a mash's input (or the two it alternates), a hold's input, and the inputs a -- combination wants together. local SEQ_OF = { [35] = { 3, 2, 4, 1, 3 }, [36] = { 4, 2, 3, 1, 4 }, [156] = { 1, 3 }, [157] = { 1, 4 }, [158] = { 2, 3 }, [159] = { 2, 4 }, [160] = { 3, 1 }, [161] = { 4, 1 }, [162] = { 3, 2 }, [163] = { 4, 2 }, [170] = { 4, 3 }, [171] = { 3, 4 }, [172] = { 1, 2 }, [173] = { 2, 1 }, [149] = { 2, 3, 5 }, [150] = { 2, 4, 5 }, [151] = { 1, 3, 5 }, [152] = { 1, 4, 5 }, [164] = { 3, 1, 5 }, [165] = { 4, 1, 5 }, [166] = { 4, 2, 5 }, [167] = { 3, 2, 5 }, [168] = { 2, 3, 6 }, [169] = { 2, 4, 6 }, [174] = { 1, 3, 6 }, [175] = { 1, 4, 6 }, [176] = { 3, 1, 6 }, [177] = { 4, 1, 6 }, [178] = { 4, 2, 6 }, [179] = { 3, 2, 6 }, [180] = { 2, 3, 1 }, [181] = { 2, 4, 1 }, [182] = { 3, 2, 4 }, [183] = { 3, 1, 4 }, [184] = { 4, 2, 3 }, [185] = { 4, 1, 3 }, [186] = { 1, 3, 2 }, [187] = { 1, 4, 2 }, [216] = { 3, 2, 4, 1, 3 }, [219] = { 2, 4, 1, 3, 2 }, [218] = { 4, 1, 3, 2, 4 }, [217] = { 1, 3, 2, 4, 1 }, [212] = { 3, 1, 4, 2, 3 }, [213] = { 1, 4, 2, 3, 1 }, [214] = { 4, 2, 3, 1, 4 }, [215] = { 2, 3, 1, 4, 2 } } local MASH_OF = { [17] = { 5 }, [18] = { 5 }, [19] = { 5 }, [27] = { 3, 4 }, [28] = { 3, 4 }, [29] = { 3, 4 }, [30] = { 5, 6 }, [31] = { 5, 6 }, [32] = { 5, 6 }, [153] = { 5, 7 }, [154] = { 5, 7 }, [155] = { 5, 7 }, [188] = { 6 }, [189] = { 6 }, [190] = { 6 }, [192] = { 7 }, [193] = { 7 }, [194] = { 7 }, [196] = { 3 }, [197] = { 3 }, [198] = { 3 }, [200] = { 4 }, [201] = { 4 }, [202] = { 4 }, [204] = { 1 }, [205] = { 1 }, [206] = { 1 }, [208] = { 2 }, [209] = { 2 }, [210] = { 2 } } local HOLD_OF = { [21] = 1, [22] = 2, [23] = 3, [24] = 4, [25] = 5 } local TOGETHER = { [9] = { 1, 3 }, [10] = { 1, 4 }, [11] = { 2, 3 }, [12] = { 2, 4 }, [13] = { 5, 1 }, [14] = { 5, 2 }, [15] = { 5, 3 }, [16] = { 5, 4 }, [220] = { 6, 1 }, [221] = { 6, 2 }, [222] = { 6, 3 }, [223] = { 6, 4 }, [224] = { 7, 1 }, [225] = { 7, 2 }, [226] = { 7, 3 }, [227] = { 7, 4 }, [228] = { 5, 1, 3 }, [229] = { 5, 1, 4 }, [230] = { 5, 2, 3 }, [231] = { 5, 2, 4 }, [232] = { 6, 1, 3 }, [233] = { 6, 1, 4 }, [234] = { 6, 2, 3 }, [235] = { 6, 2, 4 }, [236] = { 7, 1, 3 }, [237] = { 7, 1, 4 }, [238] = { 7, 2, 3 }, [239] = { 7, 2, 4 } } local KARIS_LETGO, KARIS_MULTI = 20, 33 -- The switches an input number stands for. local function switchesOf(kind) if PAIR_OF[kind] then return { SWITCH_OF[PAIR_OF[kind][1]], SWITCH_OF[PAIR_OF[kind][2]] } end return { SWITCH_OF[kind] } end -- A plan's steps: what to press and let go on which frame. local function planPress(steps, frame, switches) steps[frame] = steps[frame] or { press = {}, release = {} } for _, s in ipairs(switches) do table.insert(steps[frame].press, s) end end local function planRelease(steps, frame, switches) steps[frame] = steps[frame] or { press = {}, release = {} } for _, s in ipairs(switches) do table.insert(steps[frame].release, s) end end local function planTap(steps, frame, switches) planPress(steps, frame, switches) planRelease(steps, frame + 1, switches) end -- A switch a move does not accept, for a wrong answer. local function wrongFor(accepted) for _, candidate in ipairs({ 2, 1, 3, 4, 6 }) do local ok = true for _, a in ipairs(accepted) do if a == candidate then ok = false end if PAIR_OF[a] and ((PAIR_OF[a][1] == candidate) or (PAIR_OF[a][2] == candidate)) then ok = false end end if ok then return { SWITCH_OF[candidate] } end end return { SWITCH_BUTTON2 } end -- A plan: what to press and let go on which frames, for one window. local function planFor(view) local w = view.window local at = w.from + 3 local steps = {} local wrong = wrongMove(view) local kind = w.kind if missedMove(view) then -- Left unanswered: the window runs out. elseif ((kind >= 1) and (kind <= 7)) or ((kind >= 9) and (kind <= 12)) then if wrong then planTap(steps, at, wrongFor({ kind, w.alt })) else planTap(steps, at, switchesOf(kind)) end elseif (kind == KIMMY.MASH) and wrong then -- A mash broken by a stray input. planTap(steps, at, wrongFor({ w.alt or 5 })) elseif kind == KIMMY.COMBO then local both = switchesOf(w.alt) for _, s in ipairs(switchesOf(w.alt2)) do both[#both + 1] = s end planTap(steps, at, both) elseif kind == KIMMY.HOLD then planPress(steps, at, switchesOf(w.alt)) planRelease(steps, w.to, switchesOf(w.alt)) elseif kind == KIMMY.LETGO then planPress(steps, w.from - 4, { SWITCH_BUTTON1 }) planRelease(steps, w.from + 2, { SWITCH_BUTTON1 }) elseif kind == KIMMY.MASH then local turn = 0 for frame = at, w.to - 2, 2 do local which if w.alt == KIMMY.UD then which = (turn % 2 == 0) and { SWITCH_UP } or { SWITCH_DOWN } elseif w.alt == KIMMY.LR then which = (turn % 2 == 0) and { SWITCH_LEFT } or { SWITCH_RIGHT } elseif w.alt == KIMMY.B1B2 then which = (turn % 2 == 0) and { SWITCH_BUTTON1 } or { SWITCH_BUTTON2 } else which = switchesOf(w.alt or 5) end planTap(steps, frame, which) turn = turn + 1 end elseif (kind == KIMMY.MULTI) or (kind == KIMMY.LOOP) then local start = at + ((kind == KIMMY.MULTI) and (w.alt2 or 0) or 0) for i, step in ipairs(view.multi or {}) do planTap(steps, start + 2 * (i - 1), switchesOf(step)) end elseif kind == KIMMY.PATH then local p = view.path or {} if wrong then planTap(steps, at, wrongFor({ p[1], p[3], p[5], p[7] })) elseif p[1] and (p[1] > 0) then planTap(steps, at, switchesOf(p[1])) end elseif kind == KIMMY.YESNO then if view.move % 2 == 0 then planTap(steps, at, { SWITCH_BUTTON1 }) end elseif kind == KIMMY.TIMED then local first = (view.timed or {})[w.alt or view.move] if first and (first[2] + 1 >= w.from) and (first[2] + 1 <= w.to) then planTap(steps, first[2] + 1, switchesOf(first[1])) end elseif kind == KIMMY.ANYTHING then planTap(steps, at, { SWITCH_BUTTON1 }) elseif kind == KIMMY.NOTHING then if wrong then planTap(steps, at, { SWITCH_BUTTON1 }) end end return { steps = steps, from = w.from - 6, to = w.to + 2 } end -- A map-mode plan: the ancestor's kinds. A single input tapped; MASH (8) tapped every other -- frame; an ACT combo (9 to 12) as the button with a direction; a diagonal (13 to 16) as two -- directions; SKIP (17) left alone. Every fifth move is answered wrong. local RDG_ACT = { [9] = { 5, 1 }, [10] = { 5, 2 }, [11] = { 5, 3 }, [12] = { 5, 4 }, [13] = { 1, 3 }, [14] = { 1, 4 }, [15] = { 2, 3 }, [16] = { 2, 4 } } local function planForRdg(view) local w = view.window local at = w.from + 3 local steps = {} local wrong = wrongMove(view) local kind = w.kind if missedMove(view) then -- Left unanswered: the window runs out. elseif (kind >= 1) and (kind <= 7) then if wrong then planTap(steps, at, { (kind == 1) and SWITCH_DOWN or SWITCH_UP }) else planTap(steps, at, { SWITCH_OF[kind] }) end elseif (kind == (view.mashKind or 8)) and wrong then -- A mash broken by a stray direction. planTap(steps, at, { SWITCH_UP }) elseif kind == (view.mashKind or 8) then for frame = at, w.to - 2, 2 do planTap(steps, frame, { SWITCH_BUTTON1 }) end elseif (view.pairs or RDG_ACT)[kind] and view.zone then -- A shot: the pointer into the zone (or well outside it) the frame before the button, -- in the raw coordinates the game scales by its ratios. local z = view.zone local r = view.mouse or { 1, 1, 0, 0 } local mx = wrong and (z[3] + 200) or ((z[1] + z[3]) / 2) local my = wrong and (z[4] + 200) or ((z[2] + z[4]) / 2) steps[at - 1] = steps[at - 1] or { press = {}, release = {} } steps[at - 1].mouse = { math.floor((mx + r[3]) / r[1]), math.floor((my + r[4]) / r[2]) } planTap(steps, at, { SWITCH_BUTTON1 }) elseif (view.pairs or RDG_ACT)[kind] then local pair = (view.pairs or RDG_ACT)[kind] if wrong then planTap(steps, at, { SWITCH_BUTTON2 }) else planTap(steps, at, { SWITCH_OF[pair[1]], SWITCH_OF[pair[2]] }) end end return { steps = steps, from = w.from - 6, to = w.to + 2 } end -- A KarisFramework plan for a compound move: a hold pressed from three frames into the -- window to its end; a let-go held from before the window and released within it; a mash -- tapped every other frame (a run, MASH2, and MASH3 alternating their two); a sequence or a -- circle a step every other frame; a multi tapped as many times as its row asks; a -- combination pressed together. Every fifth move is answered wrong (a stray input, or -- nothing for a let-go); every seventh is left unanswered. local function planForKaris(view) local w = view.window local at = w.from + 3 local steps = {} local wrong = wrongMove(view) local kind = w.kind local function switches(list) local result = {} for _, m in ipairs(list) do result[#result + 1] = SWITCH_OF[m] end return result end if missedMove(view) then -- Left unanswered: the window runs out. elseif HOLD_OF[kind] then if wrong then planTap(steps, at, wrongFor({ HOLD_OF[kind] })) else planPress(steps, at, { SWITCH_OF[HOLD_OF[kind]] }) planRelease(steps, w.to, { SWITCH_OF[HOLD_OF[kind]] }) end elseif kind == KARIS_LETGO then if not wrong then planPress(steps, w.from - 4, { SWITCH_BUTTON1 }) planRelease(steps, w.from + 2, { SWITCH_BUTTON1 }) end elseif MASH_OF[kind] then if wrong then planTap(steps, at, wrongFor(MASH_OF[kind])) else local turn = 0 for frame = at, w.to - 2, 2 do planTap(steps, frame, { SWITCH_OF[MASH_OF[kind][(turn % #MASH_OF[kind]) + 1]] }) turn = turn + 1 end end elseif SEQ_OF[kind] then if wrong then planTap(steps, at, wrongFor(SEQ_OF[kind])) else for i, m in ipairs(SEQ_OF[kind]) do planTap(steps, at + 2 * (i - 1), { SWITCH_OF[m] }) end end elseif kind == KARIS_MULTI then if wrong then planTap(steps, at, wrongFor({ w.alt })) else for i = 1, (w.alt2 or 1) do planTap(steps, at + 2 * (i - 1), { SWITCH_OF[w.alt] }) end end elseif TOGETHER[kind] then if wrong then planTap(steps, at, wrongFor(TOGETHER[kind])) else planTap(steps, at, switches(TOGETHER[kind])) end end return { steps = steps, from = w.from - 6, to = w.to + 2 } end -- A choice: step down to the right option (a wrong one every fifth move), then take it. function answerChoice(view, key, w) if (view.frame >= w.from + 3) and (view.frame <= w.to) then local want = nil local wrong = wrongMove(view) for at, index in ipairs(view.order or {}) do if view.choices[index] and (view.choices[index].right ~= wrong) and (want == nil or not wrong) then want = at end end if want and (view.choice < want) then pressed = SWITCH_DOWN karisPress(pressed, true) elseif want and (view.choice == want) and not answered[key] then answered[key] = true pressed = SWITCH_BUTTON1 karisPress(pressed, true) end end end -- Runs the plan for this frame. local function followPlan(frame) local step = plan and plan.steps[frame] if step then if step.mouse then karisMouse(step.mouse[1], step.mouse[2]) end for _, s in ipairs(step.press) do if not held[s] then held[s] = true karisPress(s, true) end end for _, s in ipairs(step.release) do if held[s] then held[s] = nil karisPress(s, false) end end end if plan and (frame > plan.to) then for s in pairs(held) do karisPress(s, false) end held = {} plan = nil end end function karisTraceSeek(frame) seeks = seeks + 1 debugPrint(string.format("TRACE seek %d", frame)) end -- Presses what the policy says this frame. view: { screen, step, level, scene, move, frame, -- window = { from, to, kind, death, alt }, choices = { { right }, ... }, order, choice }. function karisDrive(view) if pressed then karisPress(pressed, false) pressed = nil end local line = string.format("TRACE screen %s step %s level %s scene %s move %s score %s lives %s", tostring(view.screen), tostring(view.step), tostring(view.level), tostring(view.scene), tostring(view.move), tostring(view.score), tostring(view.lives)) if line ~= lastLine then lastLine = line debugPrint(line .. " disc " .. tostring(view.frame)) end -- A coin on the attract loop when the game is not free play, then START once it has begun -- playing. if (view.screen == 0) and not started and (view.frame >= 150) and (not view.freeplay) and (view.credits == 0) and not coined then coined = true pressed = SWITCH_COIN1 karisPress(pressed, true) return end if (view.screen == 0) and not started and (view.frame >= 160) and (view.freeplay or (view.credits > 0)) then started = true pressed = SWITCH_START1 karisPress(pressed, true) return end -- Kimmy's menus: the new-game menu (a level the framework never numbered, so nil) takes -- the button for NEW GAME; the level select takes the button for the level shown. if view.kimmy and started and ((view.screen == nil) or (view.screen == 117)) and (view.frame ~= menuAt) and (view.frames > menuAt + 30) then menuAt = view.frames pressed = SWITCH_BUTTON1 karisPress(pressed, true) return end -- The continue screen: taken once. if (view.screen == 100) and (view.step == 1) and not continued then continued = true pressed = SWITCH_START1 karisPress(pressed, true) return end if (view.screen ~= 103) or (view.window == nil) then if plan then followPlan(view.frame) end return end -- A move is answered once per life: a game that rewinds to the move after a death -- (the later map-mode copies) asks it again, and the drive answers it right that time. local moveKey = tostring(view.level) .. "/" .. tostring(view.scene) .. "/" .. tostring(view.move) local key = moveKey .. "/" .. tostring(view.lives) local w = view.window view.retry = (tried[moveKey] ~= nil) if view.kimmy or view.rdg then -- A Kimmy or map-mode move: a plan of presses for the window, made when it comes into -- view. if (view.step == 1) and (plan == nil) and not answered[key] and (view.frame >= w.from - 6) and (view.frame <= w.to) then answered[key] = true tried[moveKey] = true plan = view.rdg and planForRdg(view) or planFor(view) elseif view.rdg and (view.step == ((view.timegal and 12) or 11)) and view.choices then answerChoice(view, key, w) end followPlan(view.frame) elseif view.step == 1 then local kind = w.kind if (kind >= 1) and (kind <= 7) then -- Waiting for a single input: answer three frames into the window. if (view.frame >= w.from + 3) and (view.frame <= w.to) and not answered[key] then answered[key] = true tried[moveKey] = true if not missedMove(view) then local wrong = wrongMove(view) local press = wrong and ((kind == 1) and SWITCH_DOWN or SWITCH_UP) or SWITCH_OF[kind] pressed = press karisPress(press, true) end end elseif (plan == nil) and not answered[key] and (view.frame >= w.from - 6) and (view.frame <= w.to) and (HOLD_OF[kind] or MASH_OF[kind] or SEQ_OF[kind] or TOGETHER[kind] or (kind == KARIS_LETGO) or (kind == KARIS_MULTI)) then -- A compound move: a plan of presses over the window, made as it comes into view. answered[key] = true tried[moveKey] = true plan = planForKaris(view) end followPlan(view.frame) elseif (view.step == 19) and view.choices then answerChoice(view, key, w) elseif plan then -- The move is judged: what the plan still holds is let go, and the rest of it dropped. for s in pairs(held) do karisPress(s, false) end held, plan = {}, nil end end -- The run is long enough when the game has been played through a continue and out. function karisDone(view) return continued and (view.screen == 0) and (seeks > 0) end