User File #639053031973604935

Upload All User Files

#639053031973604935 (unlisted) - Emma in the Mountains - Minigame 11 discrete solution optimizer

emma_mountains_m11_clear_discroptimizer.lua
106 downloads
Uploaded 1/29/2026 5:06 PM by toca (see all 5)
-- 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)
))