singe/testScripts/ports/timetravelerDrive.singe
2026-09-22 18:32:07 -05:00

200 lines
6.1 KiB
Text

-- Drives Hologram Time Traveler (the original or its port) from what the game shows, the same
-- way for both. The driver file defines what it takes: a view() of the game's state and
-- ttPress(switch, down) to press or let go of a switch; this file decides. The trace lines
-- it prints are what compare.sh diffs.
--
-- The policy: a coin and START at the attract loop; in a level, the move the row asks for
-- three frames into its window (a turn first where the row wants a turn and a shot), a wrong
-- one on every fifth move and none on every seventh with misses on; a cube against the first
-- death with misses on; at the map, the trader once with misses on (a coin buys a cube) and
-- the timer's own choice otherwise; a pull at the hellgate's lever; a coin and START at the
-- continue; a letter at the board.
TT_FRAMES = tonumber(os.getenv("PORT_FRAMES")) or 60000
local perfect = (os.getenv("PORT_PERFECT") == "1")
local frames = 0
local lastLine = nil
local pending = nil -- { switch } pressed this frame, let go the next.
local answered = {} -- Windows answered, by level/segment/move/lives.
local tried = {} -- Moves met before, by level/segment/move.
local coined = false
local started = false
local cubeUsed = false
local traded = false
local pulled = false
local lettered = false
local continues = 0
local turnAt = nil -- The frame the turn went in, for the shot after it.
local SWITCH_OF = nil
function ttTraceSeek(frame)
debugPrint(string.format("TRACE seek %d", frame))
end
local function tap(switch)
ttPress(switch, true)
pending = switch
end
-- view: { flow, screen, step, alt, level, seg, move, moves, frame, score, lives, cubes, credits,
-- window = { from, to }, correct (the row's move, or nil for the tutorial's choice),
-- turned (the turn is in), dying, wizard, mapWait, mapCursor, trader, hellgate,
-- continuing, board }
function ttDrive(view)
if pending then
ttPress(pending, false)
pending = nil
end
frames = frames + 1
if SWITCH_OF == nil then
SWITCH_OF = { [view.UP] = SWITCH_UP, [view.DOWN] = SWITCH_DOWN, [view.LEFT] = SWITCH_LEFT, [view.RIGHT] = SWITCH_RIGHT, [view.ACTION] = SWITCH_BUTTON1 }
end
-- (altState is left out: the original's LCD drawing steps it while the attract loop shows.)
local line = string.format("TRACE flow %s screen %s step %s level %s seg %s move %s score %s lives %s cubes %s credits %s",
tostring(view.flow), tostring(view.screen), tostring(view.step), tostring(view.level), tostring(view.seg),
tostring(view.move), tostring(view.score), tostring(view.lives), tostring(view.cubes), tostring(view.credits))
if line ~= lastLine then
debugPrint(line .. " disc " .. tostring(view.frame))
lastLine = line
end
if frames >= TT_FRAMES then
debugPrint("TRACE end after " .. frames .. " frames")
singeQuit()
return
end
-- The attract loop: a coin, then START once the credit shows.
if view.intro then
if (frames > 200) and not coined then
coined = true
tap(SWITCH_COIN1)
return
end
if coined and (frames > 300) and (frames % 40 == 0) and (view.credits > 0) then
tap(SWITCH_START1)
end
return
end
-- The continue: a coin, then START, taken once.
if view.continuing then
if (continues < 1) and (frames % 30 == 0) then
if view.credits == 0 then
tap(SWITCH_COIN1)
else
continues = continues + 1
tap(SWITCH_START1)
end
end
return
end
-- The map: the trader once (with misses on), else the timer's own choice.
if view.mapWait then
if (not perfect) and (not traded) and (view.mapCursor == view.TRADER) then
traded = true
tap(SWITCH_BUTTON1)
end
return
end
-- The trader: a coin buys a cube.
if view.trader then
if (frames % 20 == 0) then
tap(SWITCH_COIN1)
end
return
end
-- The hellgate: one pull at the lever with misses on (a roll can take every life); the
-- perfect run lets the clip pass.
if view.hellgate then
if (not perfect) and not pulled then
pulled = true
tap(SWITCH_BUTTON1)
end
return
end
pulled = false
-- The board: one letter, and the rest to the clip's end.
if view.board then
if not lettered then
lettered = true
tap(SWITCH_BUTTON1)
end
return
end
-- A death: a cube against the first, with misses on.
if view.dying then
if (not perfect) and (not cubeUsed) and (view.cubes > 0) then
cubeUsed = true
tap(SWITCH_BUTTON2)
end
return
end
-- A level's window.
if view.window then
local moveKey = tostring(view.level) .. "/" .. tostring(view.seg) .. "/" .. tostring(view.move)
local key = moveKey .. "/" .. tostring(view.lives)
local retry = (tried[moveKey] ~= nil)
local at = view.frame
if (at >= view.window[1] + 3) and (at <= view.window[2]) and not answered[key] then
local correct = view.correct
if correct == nil then
-- The tutorial's choice: the sign post's right skips it, nothing plays it.
answered[key] = true
tried[moveKey] = true
if perfect then
tap(SWITCH_RIGHT)
end
return
end
if correct == view.NOMOVE then
-- A row that wants nothing.
answered[key] = true
return
end
local missed = (not perfect) and (view.move % 7 == 0) and (view.move % 5 ~= 0) and not retry
local wrong = (not perfect) and (view.move % 5 == 0) and not retry
if missed then
answered[key] = true
tried[moveKey] = true
return
end
if (correct == view.TURNRIGHTSHOOT) or (correct == view.TURNLEFTSHOOT) then
-- A turn, then the shot two frames on.
if not view.turned then
if turnAt == nil then
turnAt = at
tap(wrong and SWITCH_UP or ((correct == view.TURNRIGHTSHOOT) and SWITCH_RIGHT or SWITCH_LEFT))
if wrong then
answered[key] = true
tried[moveKey] = true
turnAt = nil
end
end
elseif at >= turnAt + 2 then
answered[key] = true
tried[moveKey] = true
turnAt = nil
tap(SWITCH_BUTTON1)
end
return
end
answered[key] = true
tried[moveKey] = true
local switch = SWITCH_OF[correct]
if wrong then
switch = (correct == view.UP) and SWITCH_DOWN or SWITCH_UP
end
if switch then
tap(switch)
end
end
end
end