-- 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