200 lines
6.1 KiB
Text
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
|