776 lines
29 KiB
Text
776 lines
29 KiB
Text
-- 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
|
|
AUTHOR_SEED = KARIS_SEED -- The port's runtime seeds its generator from this, as the original's os.time does.
|
|
|
|
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
|
|
-- PORT_ATTRACT=N lets the attract loop run N frames before the coin and START, so its own screens
|
|
-- (the rankings, the best percent) are played and traced too.
|
|
local ATTRACT = tonumber(os.getenv("PORT_ATTRACT")) or 0
|
|
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
|
|
-- A map-mode game's pair kinds, by the name of the game's constant for each: what is held with
|
|
-- what. A game that does not define a constant (one with no diagonals) simply has no such pair.
|
|
KARIS_PAIR_KINDS = { { "ACTUP", 5, 1 }, { "ACTDOWN", 5, 2 }, { "ACTLEFT", 5, 3 }, { "ACTRIGHT", 5, 4 }, { "UPLEFT", 1, 3 }, { "UPRIGHT", 1, 4 }, { "DOWNLEFT", 2, 3 }, { "DOWNRIGHT", 2, 4 } }
|
|
|
|
|
|
-- The pair kinds a game defines, keyed by its own numbers; constants maps a name to the number.
|
|
function karisPairs(constants)
|
|
local found = {}
|
|
|
|
for _, kind in ipairs(KARIS_PAIR_KINDS) do
|
|
local number = constants[kind[1]]
|
|
|
|
if number ~= nil then
|
|
found[number] = { kind[2], kind[3] }
|
|
end
|
|
end
|
|
|
|
return found
|
|
end
|
|
|
|
-- 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
|
|
|
|
|
|
-- PORT_SERVICE=1 walks the service screens from the attract loop before the game: SERVICE, then
|
|
-- the options' first row on to the graphics screen, its rows each changed once (RIGHT) and
|
|
-- "Default", round to its first row and on to the performance screen, the same there, on to the
|
|
-- options, the same, and EXIT; a press every WALK_EVERY frames, clear of the screens' quarter
|
|
-- second between moves. While a screen shows, the trace carries its row and the dips.
|
|
local walkService = (os.getenv("PORT_SERVICE") == "1")
|
|
local WALK_EVERY = 10
|
|
local WALK = { SWITCH_SERVICE }
|
|
local walkAt = nil
|
|
local walkEvents = {}
|
|
|
|
|
|
-- A screen's rows walked from its first: each one's value changed, the second from last
|
|
-- ("Default") used, and, unless it is the last screen, round to the first row and on.
|
|
local function walkScreen(rows, leave)
|
|
WALK[#WALK + 1] = SWITCH_DOWN
|
|
for _ = 2, rows - 2 do
|
|
WALK[#WALK + 1] = SWITCH_RIGHT
|
|
WALK[#WALK + 1] = SWITCH_DOWN
|
|
end
|
|
WALK[#WALK + 1] = SWITCH_RIGHT -- Default
|
|
WALK[#WALK + 1] = SWITCH_DOWN
|
|
if leave then
|
|
WALK[#WALK + 1] = SWITCH_RIGHT -- EXIT
|
|
else
|
|
WALK[#WALK + 1] = SWITCH_DOWN -- Round to the first row ...
|
|
WALK[#WALK + 1] = SWITCH_RIGHT -- ... and on to the next screen.
|
|
end
|
|
end
|
|
|
|
|
|
WALK[#WALK + 1] = SWITCH_RIGHT -- The options' first row, on to the graphics screen.
|
|
walkScreen(15, false) -- The graphics screen, on to the performance screen.
|
|
walkScreen(10, false) -- The performance screen, on to the options.
|
|
walkScreen(15, true) -- The options, and EXIT.
|
|
local SERVICE_DIPS = { "dip_GameType", "dip_Diffshow", "dip_PlayStyle", "dip_StartLevel", "dip_StartScene", "dip_Rewind", "dip_CoinsPerCredit", "dip_LivesPerCredit", "dip_LimitContinue", "dip_ShowAction", "dip_Hints", "dip_MashtoRun",
|
|
"dip_Extravid", "ShowTitle", "ShowLCD", "ShowLevel", "ShowTop", "ScorePos", "LivPos", "CoinPos", "fontType", "fontSize", "mycolor1", "mycolor2",
|
|
"dip_Model", "dip_Res", "dip_Display", "dip_Scanlines", "dip_ScType", "dip_LivType", "dip_FontQ" }
|
|
|
|
|
|
-- The options screen's row and dips, or the save or exit screen's row and choices, from a reader of
|
|
-- the loop's variables, while it shows.
|
|
function karisServiceState(screen, read)
|
|
local parts = {}
|
|
|
|
if (screen == 108) or (screen == 112) then
|
|
return "row " .. tostring(read("optSel")) .. " Loadsave " .. tostring(read("dip_Loadsave")) .. " Slot " .. tostring(read("dip_Slot")) .. " InPlayExit " .. tostring(read("bInPlayExit"))
|
|
end
|
|
if (screen ~= 104) and (screen ~= 113) and (screen ~= 114) then
|
|
return nil
|
|
end
|
|
parts[1] = "rows " .. tostring(read("optSel")) .. " " .. tostring(read("optSelG")) .. " " .. tostring(read("optSelP"))
|
|
for _, name in ipairs(SERVICE_DIPS) do
|
|
parts[#parts + 1] = name:sub(5) .. " " .. tostring(read(name))
|
|
end
|
|
|
|
return table.concat(parts, " ")
|
|
end
|
|
|
|
|
|
-- Switches tapped, as a walk's events: each a press, then its release.
|
|
local function tapsOf(switches)
|
|
local events = {}
|
|
|
|
for _, switch in ipairs(switches) do
|
|
events[#events + 1] = { switch, true }
|
|
events[#events + 1] = { switch, false }
|
|
end
|
|
|
|
return events
|
|
end
|
|
|
|
|
|
-- PORT_TITLETAPS=1 taps the switches that do something beyond a move while the title plays,
|
|
-- before any service walk: the original pauses through the title, so none should count; the
|
|
-- trace carries the credits.
|
|
local titleTaps = (os.getenv("PORT_TITLETAPS") == "1")
|
|
|
|
if titleTaps then
|
|
walkEvents = tapsOf({ SWITCH_COIN1, SWITCH_START1, SWITCH_SERVICE, SWITCH_TEST, SWITCH_CONSOLE, SWITCH_SKILL1 })
|
|
end
|
|
if walkService then
|
|
table.move(tapsOf(WALK), 1, #WALK * 2, #walkEvents + 1, walkEvents)
|
|
end
|
|
|
|
|
|
-- PORT_PLAYWALK=1 walks the in-play switches, each part once the game has answered so many moves
|
|
-- and out of a move's window: a quick save to slot 1 (SKILL1); TEST to the save screen, saving
|
|
-- to slot 2 and back to the game; CONSOLE to the exit screen and BACK; START held with SKILL1,
|
|
-- loading slot 1; COIN held with START, to the exit screen and BACK.
|
|
local PLAY_WALK = (os.getenv("PORT_PLAYWALK") == "1") and {
|
|
{ after = 2, events = tapsOf({ SWITCH_SKILL1 }) },
|
|
{ after = 3, events = tapsOf({ SWITCH_TEST, SWITCH_RIGHT, SWITCH_DOWN, SWITCH_RIGHT, SWITCH_DOWN, SWITCH_RIGHT }) },
|
|
{ after = 5, events = tapsOf({ SWITCH_CONSOLE, SWITCH_DOWN, SWITCH_BUTTON1 }) },
|
|
{ after = 6, events = { { SWITCH_START1, true }, { SWITCH_SKILL1, true }, { SWITCH_SKILL1, false }, { SWITCH_START1, false } } },
|
|
{ after = 8, events = { { SWITCH_COIN1, true }, { SWITCH_START1, true }, { SWITCH_START1, false }, { SWITCH_COIN1, false }, { SWITCH_DOWN, true }, { SWITCH_DOWN, false }, { SWITCH_BUTTON1, true }, { SWITCH_BUTTON1, false } } }
|
|
} or {}
|
|
local playPart = 1
|
|
|
|
-- The second of PORT_TWICE's runs first loads slot 2 (START held with SKILL2), which the first
|
|
-- run saved from the save screen: the slots' part of the Persistence row.
|
|
if (#PLAY_WALK > 0) and (os.getenv("PORT_PASS") == "2") then
|
|
table.insert(PLAY_WALK, 1, { after = 1, events = { { SWITCH_START1, true }, { SWITCH_SKILL2, true }, { SWITCH_SKILL2, false }, { SWITCH_START1, false } } })
|
|
end
|
|
|
|
|
|
-- The next part of the play walk, when the game has answered enough moves and no window is near.
|
|
local function karisPlayWalk(view, moves)
|
|
local part = PLAY_WALK[playPart]
|
|
|
|
if (#walkEvents > 0) or (part == nil) or (view.screen ~= 103) or (moves < part.after) then
|
|
return
|
|
end
|
|
if view.window and (view.frame >= view.window.from - 30) and (view.frame <= view.window.to + 5) then
|
|
return
|
|
end
|
|
playPart = playPart + 1
|
|
walkEvents = { table.unpack(part.events) }
|
|
walkAt = nil
|
|
end
|
|
|
|
|
|
-- One event of the walk each WALK_EVERY frames. True while walking.
|
|
local function walk(view)
|
|
if #walkEvents == 0 then
|
|
return false
|
|
end
|
|
walkAt = walkAt or view.frames
|
|
if view.frames - walkAt < WALK_EVERY then
|
|
return true
|
|
end
|
|
walkAt = view.frames
|
|
local event = table.remove(walkEvents, 1)
|
|
|
|
karisPress(event[1], event[2])
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
-- PORT_HUD=1 adds what the HUD shows to each trace line, a letter a flag the loop sets, so an
|
|
-- original and its port are compared on what a player sees as well as on the game's state.
|
|
local HUD_FLAGS = {
|
|
{ "S", "bShowScore" }, { "L", "bShowLives" }, { "V", "bShowLvl" }, { "T", "bShowTop" },
|
|
{ "C", "bShowCredits" }, { "A", "bShowAction" }, { "K", "bShowSkip" }, { "G", "bShowGet" },
|
|
{ "H", "bShowChoices" }, { "D", "bShowLCD" }, { "P", "bPause" }, { "1", "bShowDisk1" },
|
|
{ "2", "bShowDisk2" }, { "3", "bShowDisk3" }, { "4", "bShowDiskA" }
|
|
}
|
|
local traceHud = (os.getenv("PORT_HUD") == "1")
|
|
|
|
|
|
-- The HUD flags as letters, from a reader of the loop's variables (the original's globals, or
|
|
-- the port's q).
|
|
function karisHudFlags(read)
|
|
local letters = {}
|
|
|
|
if not traceHud then
|
|
return nil
|
|
end
|
|
for _, flag in ipairs(HUD_FLAGS) do
|
|
if read(flag[2]) then
|
|
letters[#letters + 1] = flag[1]
|
|
end
|
|
end
|
|
|
|
return table.concat(letters)
|
|
end
|
|
|
|
|
|
-- PORT_SOUND=1 traces each sound played or stopped, by its file's name, and the disc's audio
|
|
-- turned off or on, with the disc frame: the Sound row of FORGE.md 14.18. The engine's own calls
|
|
-- are wrapped before the game loads, so the original's sounds and the port's (which Forge plays
|
|
-- through the same calls) are caught the same way.
|
|
if os.getenv("PORT_SOUND") == "1" then
|
|
local names = {}
|
|
local realSoundLoad = soundLoad
|
|
local realSoundPlay = soundPlay
|
|
|
|
soundLoad = function(file, ...)
|
|
local handle = realSoundLoad(file, ...)
|
|
|
|
if handle ~= nil then
|
|
names[handle] = tostring(file):match("([^/\\]+)$")
|
|
end
|
|
|
|
return handle
|
|
end
|
|
soundPlay = function(handle, ...)
|
|
debugPrint(string.format("TRACE sound %s disc %d", tostring(names[handle]), discGetFrame()))
|
|
|
|
return realSoundPlay(handle, ...)
|
|
end
|
|
-- A sound stopped, and the disc's own audio turned off or on, change what is heard too.
|
|
local realSoundStop = soundStop
|
|
local realDiscAudio = discAudio
|
|
|
|
soundStop = function(handle, ...)
|
|
debugPrint(string.format("TRACE soundStop %s disc %d", tostring(names[handle]), discGetFrame()))
|
|
|
|
return realSoundStop(handle, ...)
|
|
end
|
|
discAudio = function(channel, enabled, ...)
|
|
debugPrint(string.format("TRACE discAudio %s %s disc %d", tostring(channel), tostring(enabled), discGetFrame()))
|
|
|
|
return realDiscAudio(channel, enabled, ...)
|
|
end
|
|
end
|
|
|
|
|
|
-- PORT_SHOTS=N takes up to N screenshots, numbered in order: one at each change the trace
|
|
-- prints, and one a frame into each move's window (the drive answers within a few). The traces
|
|
-- of an original and its port agree, so the same number is the same moment in both, and the
|
|
-- pictures can be diffed.
|
|
local SHOT_INTO_WINDOW = 1
|
|
-- A second run of PORT_TWICE names its pictures apart from the first's.
|
|
local SHOT_PREFIX = ((os.getenv("PORT_PASS") or "1") == "1") and "shot-" or ("shot-run" .. os.getenv("PORT_PASS") .. "-")
|
|
local shotsLeft = tonumber(os.getenv("PORT_SHOTS") or "0") or 0
|
|
local shotCount = 0
|
|
|
|
|
|
function karisShot(why)
|
|
if shotsLeft > 0 then
|
|
shotsLeft = shotsLeft - 1
|
|
shotCount = shotCount + 1
|
|
singeScreenshot(string.format("%s%04d-%s", SHOT_PREFIX, shotCount, why))
|
|
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 view.hud then
|
|
line = line .. " hud " .. view.hud
|
|
end
|
|
if view.service then
|
|
line = line .. " " .. view.service
|
|
end
|
|
if titleTaps then
|
|
line = line .. " credits " .. tostring(view.credits)
|
|
end
|
|
|
|
if line ~= lastLine then
|
|
lastLine = line
|
|
debugPrint(line .. " disc " .. tostring(view.frame))
|
|
karisShot("state")
|
|
end
|
|
-- A few frames into a move's window, where its prompt shows.
|
|
if view.window and (view.frame == view.window.from + SHOT_INTO_WINDOW) then
|
|
karisShot("window")
|
|
end
|
|
-- The options screen's walk, on the attract loop before anything else, and the play walk's
|
|
-- parts.
|
|
local moves = 0
|
|
|
|
for _ in pairs(answered) do
|
|
moves = moves + 1
|
|
end
|
|
karisPlayWalk(view, moves)
|
|
if ((playPart > 1) or (view.screen == 0) or (view.screen == 104) or (view.screen == 113) or (view.screen == 114)) and walk(view) then
|
|
return
|
|
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 (view.frames >= ATTRACT) 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.frames >= ATTRACT) 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
|