-- Systematic 3-move discrete optimization (FIRST-IMPROVEMENT)
-- Accepts a better candidate immediately (baseline updates RIGHT AWAY).
-- GUI overlay shows ORIGINAL baseline vs CURRENT baseline + neighborhood progress.
------------------------------------------------------------
-- USER SETTINGS
------------------------------------------------------------
-- Phase flag
local DOMAIN = "Main RAM"
local PHASE_ADDR = 0x294448
-- Reset point for fair evaluation (TAStudio branch index, 0-indexed)
local BASE_BRANCH_INDEX0 = 1
-- Evaluation budget
local MAX_FRAMES_PER_EVAL = 300 -- abort eval if not switched after this many total frames
-- Time bounds (non-lag hold frames)
local T_MIN = 1
local T_MAX = 60
-- Optional safety limits (set nil to disable)
local MAX_IMPROVEMENTS = nil -- e.g. 200 (stop after this many accepted improvements)
local MAX_TOTAL_EVALS = nil -- e.g. 2000000 (stop after this many candidate evals)
-- GUI
local GUI_X, GUI_Y = 2, 2
local GUI_FG = 0xFFFFFFFF
local GUI_BG = 0x80000000
------------------------------------------------------------
-- BASELINE (use this from now on)
------------------------------------------------------------
local ADDR_CX = 0x2944A4
local ADDR_CY = 0x2944CC
local DOMAIN = "Main RAM"
local function read_u32(addr) return memory.read_u32_le(addr, DOMAIN) end
local cx = read_u32(ADDR_CX)
local cy = read_u32(ADDR_CY)
local POINTS = {
{ x = cx - 15, y = cy + 13},
{ x = cx - 14, y = cy - 13},
{ x = cx + 14, y = cy + 12},
{ x = cx + 14, y = cy - 13},
}
local TIMES = { 13,10,11,13 }
------------------------------------------------------------
-- Helpers
------------------------------------------------------------
assert(tastudio and tastudio.engaged and tastudio.engaged(), "TAStudio not engaged (open TAStudio).")
local function normRect(x1, y1, x2, y2)
return math.min(x1, x2), math.min(y1, y2), math.max(x1, x2), math.max(y1, y2)
end
local function clamp(n, lo, hi)
if n < lo then return lo end
if n > hi then return hi end
return n
end
local function readPhase()
return memory.read_u32_le(PHASE_ADDR, DOMAIN)
end
local function writeTouch(frame, x, y)
tastudio.submitinputchange(frame, "Touch", true)
tastudio.submitanalogchange(frame, "Touch X", x)
tastudio.submitanalogchange(frame, "Touch Y", y)
end
local function deepCopyPoints(points)
local out = {}
for i = 1, #points do
out[i] = { x = points[i].x, y = points[i].y }
end
return out
end
local function copyTimes(times, n)
local out = {}
for i = 1, n do out[i] = times[i] end
return out
end
local function fmtPath(points, times)
local parts = {}
for i = 1, #points do
parts[#parts + 1] = string.format("(%d,%d)x%d", points[i].x, points[i].y, times[i] or 0)
end
return table.concat(parts, " -> ")
end
local function fmtShort(points, times)
local parts = {}
for i = 1, #points do
parts[#parts + 1] = string.format("(%d,%d)%d", points[i].x, points[i].y, times[i] or 0)
end
return table.concat(parts, " ")
end
local function consoleMsg(s)
if console and console.log then console.log(s) else print(s) end
end
local function getBranchFrame(index0)
local branches = tastudio.getbranches() -- 0-indexed table
local b = branches[index0]
return b and b.Frame or nil
end
local function resetToBaseBranch(index0)
tastudio.loadbranch(index0)
local bf = getBranchFrame(index0)
assert(bf ~= nil, "Could not get base branch frame for index0=" .. tostring(index0))
tastudio.setplayback(bf)
return bf
end
------------------------------------------------------------
-- GUI overlay state
------------------------------------------------------------
local guiOrigFrames = nil
local guiBestFrames = nil
local guiImprovements = 0
local guiEvals = 0
-- neighborhood progress
local TRIPLE_NEIGH_SIZE = 0
local CURRENT_ATTEMPT = 0 -- resets on improvement; counts candidates tried in current neighborhood walk
local origPointsStr = ""
local curPointsStr = ""
local lastImproveDesc = ""
local lastImproveDelta = 0
local function updateGuiStrings(origP, origT, curP, curT)
origPointsStr = fmtShort(origP, origT)
curPointsStr = fmtShort(curP, curT)
end
local function drawOverlay()
local o = guiOrigFrames
local b = guiBestFrames
local oStr = (o == nil) and "?" or ((o == math.huge) and "none" or (tostring(o) .. "f"))
local bStr = (b == nil) and "?" or ((b == math.huge) and "none" or (tostring(b) .. "f"))
local diffStr = ""
if (o ~= nil and b ~= nil and o ~= math.huge and b ~= math.huge) then
diffStr = string.format(" (saved: %d)", (o - b))
end
gui.text(GUI_X, GUI_Y + 0,
string.format("impr %d | evals %d | orig %s | best %s%s", guiImprovements, guiEvals, oStr, bStr, diffStr),
GUI_FG, GUI_BG
)
gui.text(GUI_X, GUI_Y + 14,
string.format("progress: %d / %d", CURRENT_ATTEMPT, TRIPLE_NEIGH_SIZE),
GUI_FG, GUI_BG
)
gui.text(GUI_X, GUI_Y + 28, "orig: " .. origPointsStr, GUI_FG, GUI_BG)
gui.text(GUI_X, GUI_Y + 42, "best: " .. curPointsStr, GUI_FG, GUI_BG)
if lastImproveDesc ~= "" then
gui.text(GUI_X, GUI_Y + 56, string.format("last: %s (%+d)", lastImproveDesc, lastImproveDelta), GUI_FG, GUI_BG)
end
end
------------------------------------------------------------
-- Evaluation (lag-aware)
-- returns: status, framesUsed, switchFrameAbs
------------------------------------------------------------
local function evalPath(points, times)
resetToBaseBranch(BASE_BRANCH_INDEX0)
local baseline = readPhase()
if baseline ~= 0 then
return "already_switched", 0, emu.framecount()
end
local n = #points
local idx = 1
local holdRemaining = times[idx] or 0
local framesUsed = 0
while framesUsed < MAX_FRAMES_PER_EVAL do
drawOverlay()
local f = emu.framecount()
local p = points[idx]
tastudio.clearinputchanges()
writeTouch(f, p.x, p.y)
tastudio.applyinputchanges()
emu.frameadvance()
framesUsed = framesUsed + 1
if readPhase() ~= 0 then
return "switched", framesUsed, f
end
if not emu.islagged() then
holdRemaining = holdRemaining - 1
if holdRemaining <= 0 then
idx = idx + 1
if idx > n then
return "exhausted_points", framesUsed, nil
end
holdRemaining = times[idx] or 0
end
end
end
return "timeout", framesUsed, nil
end
------------------------------------------------------------
-- Move set construction (systematic)
-- moveId 0 = no-op
-- moveId 1..M correspond to (point i, field, delta)
------------------------------------------------------------
local function buildMoves(N)
local moves = {}
moves[0] = {noop = true, desc = "noop"}
local id = 0
local fields = {
{k = 1, name = "x"},
{k = 2, name = "y"},
{k = 3, name = "t"},
}
local deltas = { -1, 1 }
for p = 1, N do
for _, f in ipairs(fields) do
for _, d in ipairs(deltas) do
id = id + 1
moves[id] = {p = p, field = f.k, delta = d, desc = string.format("p%d.%s%+d", p, f.name, d)}
end
end
end
return moves, id
end
local function applyMove(points, times, move, minx, miny, maxx, maxy)
if move.noop then return end
local p = move.p
local d = move.delta
local field = move.field
if field == 1 then
points[p].x = clamp(points[p].x + d, minx, maxx)
elseif field == 2 then
points[p].y = clamp(points[p].y + d, miny, maxy)
else
times[p] = clamp(times[p] + d, T_MIN, T_MAX)
end
end
------------------------------------------------------------
-- FIRST-IMPROVEMENT triple neighborhood search
-- Returns: improved(bool), newPoints, newTimes, newFrames, desc
------------------------------------------------------------
local function findFirstImprovement(basePoints, baseTimes, baseFrames, moves, M, minx, miny, maxx, maxy)
for i = 0, M do
for j = i, M do
for k = j, M do
CURRENT_ATTEMPT = CURRENT_ATTEMPT + 1
-- build candidate
local candPoints = deepCopyPoints(basePoints)
local candTimes = copyTimes(baseTimes, #baseTimes)
applyMove(candPoints, candTimes, moves[i], minx, miny, maxx, maxy)
applyMove(candPoints, candTimes, moves[j], minx, miny, maxx, maxy)
applyMove(candPoints, candTimes, moves[k], minx, miny, maxx, maxy)
-- evaluate
guiEvals = guiEvals + 1
if MAX_TOTAL_EVALS and guiEvals > MAX_TOTAL_EVALS then
return false, nil, nil, nil, nil
end
local st, fr = evalPath(candPoints, candTimes)
drawOverlay()
if st == "switched" and fr < baseFrames then
local desc = string.format("%s | %s | %s", moves[i].desc, moves[j].desc, moves[k].desc)
return true, candPoints, candTimes, fr, desc
end
end
end
end
return false, nil, nil, nil, nil
end
------------------------------------------------------------
-- Main
------------------------------------------------------------
if tastudio.setrecording then tastudio.setrecording(false) end
local minx, miny, maxx, maxy = 0,0,255,191
local N = math.min(#POINTS, #TIMES)
assert(N >= 1, "Need at least 1 point/time")
-- Clamp initial baseline into bounds
local basePoints = deepCopyPoints(POINTS)
local baseTimes = copyTimes(TIMES, N)
for i = 1, N do
basePoints[i].x = clamp(basePoints[i].x, minx, maxx)
basePoints[i].y = clamp(basePoints[i].y, miny, maxy)
baseTimes[i] = clamp(baseTimes[i], T_MIN, T_MAX)
end
-- ORIGINAL baseline copy
local origPoints = deepCopyPoints(basePoints)
local origTimes = copyTimes(baseTimes, N)
-- GUI strings
updateGuiStrings(origPoints, origTimes, basePoints, baseTimes)
-- Evaluate ORIGINAL
local st0, fr0 = evalPath(origPoints, origTimes)
local origFrames = (st0 == "switched") and fr0 or math.huge
guiOrigFrames = origFrames
-- Current best starts as original
local bestFrames = origFrames
guiBestFrames = bestFrames
consoleMsg(string.format("[ORIG] status=%s frames=%s path=%s",
st0, (origFrames == math.huge) and "inf" or tostring(origFrames), fmtPath(origPoints, origTimes)
))
local moves, M = buildMoves(N)
-- total i<=j<=k combinations when ids range 0..M inclusive:
TRIPLE_NEIGH_SIZE = math.floor((M + 3) * (M + 2) * (M + 1) / 6)
consoleMsg(string.format("Move set size: %d (plus noop). Triple neighborhood size (i<=j<=k): %d",
M, TRIPLE_NEIGH_SIZE
))
-- Optimization loop: keep accepting first improvement until none exists
while true do
updateGuiStrings(origPoints, origTimes, basePoints, baseTimes)
guiBestFrames = bestFrames
drawOverlay()
if MAX_IMPROVEMENTS and guiImprovements >= MAX_IMPROVEMENTS then
consoleMsg("[STOP] MAX_IMPROVEMENTS reached")
break
end
-- Start a fresh neighborhood walk counter for the current baseline
CURRENT_ATTEMPT = 0
local improved, newP, newT, newF, desc =
findFirstImprovement(basePoints, baseTimes, bestFrames, moves, M, minx, miny, maxx, maxy)
if not improved then
consoleMsg(string.format("[DONE] No improving neighbor found. best=%s",
(bestFrames == math.huge) and "none" or (tostring(bestFrames) .. "f")
))
break
end
-- Accept immediately
local prev = bestFrames
basePoints = newP
baseTimes = newT
bestFrames = newF
guiBestFrames = bestFrames
guiImprovements = guiImprovements + 1
lastImproveDesc = desc or "(?)"
lastImproveDelta = (prev ~= math.huge and bestFrames ~= math.huge) and (prev - bestFrames) or 0
-- IMPORTANT: reset attempt counter right away (new baseline neighborhood starts)
CURRENT_ATTEMPT = 0
updateGuiStrings(origPoints, origTimes, basePoints, baseTimes)
local msg = string.format("[NEW BEST] #%d frames=%d savedFromOrig=%d moves=%s path=%s",
guiImprovements,
bestFrames,
(origFrames ~= math.huge) and (origFrames - bestFrames) or 0,
lastImproveDesc,
fmtPath(basePoints, baseTimes)
)
consoleMsg(msg)
end
consoleMsg(string.format("[FINAL] orig=%s best=%s bestPath=%s",
(origFrames == math.huge) and "none" or tostring(origFrames),
(bestFrames == math.huge) and "none" or tostring(bestFrames),
fmtPath(basePoints, baseTimes)
))