Profile
- Name
- Barrel Rebuild v1.0
- ID
- 109255
- Shared with
- Public
- Parent
- None
- Children
- Created on
- November 20, 2025 at 19:36 PM UTC
- Updated on
- November 22, 2025 at 16:03 PM UTC
- Description
Rebuilds multiple selections simultaneously. Remove all the selections to rebuild beta sheets only.
Best for
Code
--[[
Rebuilds multiple selections simultaneously. Remove all selections before starting this recipe to rebuild beta sheets only.
v1.0
- Who would’ve thought Foldit actually supports rebuilding multiple selections at once as a built-in feature?
]]
script_name = "Barrel Rebuild "
version = "1.0.13"
print(script_name .. version)
START_SLOT = 2
END_SLOT = 96
BEST_SLOT = 100 -- pre-fuse reference used for rebuild attempts
BEST_SHORT_SLOT = 99 -- pre-fuse reference used for short fuse. not used yet
FUZED_SLOT = 98 -- fused snapshot of the current best
INIT_SLOT = 1
TEMP_SLOT = 97
SOLUTION_COUNT = END_SLOT - START_SLOT + 1
SHORT_FUSE_CONFIG = "{0.05, 1, 0.05,20} {1.00, -7, 1.00,20} {1.00, 1, 1.00,20}"
DRAFT_FUSE_CONFIG = "{0.05, -7, 0.05,20}"
LONG_FUSE_CONFIG = "{0.05, 1, 0.05,20} {0.25, -7, 0.25,20} {1.00, 2, 1.00,20} {0.05, -7, 0.25,20} {1.00, 1, 1.00,20}"
options = {
topPercent = 25,
rebuildPasses = 6,
attemptPerSlot = 40,
reportLevel = 2,
seedFromFusedEvery = 1,
draftFuseEvery = 3
}
proteinLength = structure.GetCount()
duplicateRegistry = {}
solutionSubscoresArray = {}
scorePartsCache = nil
iterationCounter = 0
bestScore = nil
initialScore = nil
noGainIterations = 0
baseSegmentScores = nil
function ScoreReturn()
local x = current.GetEnergyScore()
return math.floor(x * 1000) / 1000
end
function ScoreBBReturn()
local clash = 0
for i = 1, proteinLength do
clash = clash + (current.GetSegmentEnergySubscore(i, "Clashing"))
end
return (current.GetEnergyScore()) - clash
end
function log(level, fmt, ...)
local threshold = options.reportLevel
if threshold < level then return end
if select("#", ...) > 0 then
print(string.format(fmt, ...))
else
print(fmt)
end
end
function phaseReport(label)
local currentScore = ScoreReturn()
log(1, "[Iteration %d] %s | UnfusedScore %.3f | Best %.3f", iterationCounter, label, currentScore, bestScore or currentScore)
end
function captureDigest()
local acc = {}
for i = 1, proteinLength do
acc[#acc + 1] = string.format("%.2f", current.GetSegmentEnergyScore(i))
end
return table.concat(acc, ";")
end
function acceptFusedBase(reason)
log(1, reason or "Accepting fused solution as new base")
save.Quickload(FUZED_SLOT)
save.Quicksave(BEST_SLOT)
save.Quickload(BEST_SLOT)
duplicateRegistry[captureDigest()] = true
noGainIterations = 0
end
function isDraftIteration()
local freq = options.draftFuseEvery
return freq > 0 and iterationCounter > 0 and (iterationCounter % freq == 0)
end
function captureSegmentScores()
local scores = {}
for i = 1, proteinLength do
scores[i] = current.GetSegmentEnergyScore(i)
end
return scores
end
function segmentScoreMapString(baseScores)
if not baseScores or proteinLength == 0 then
return ""
end
local diffs = {}
for i = 1, proteinLength do
diffs[i] = (current.GetSegmentEnergyScore(i)) - (baseScores[i])
end
local sortedDiffs = {}
for i = 1, proteinLength do
sortedDiffs[i] = diffs[i]
end
table.sort(sortedDiffs)
local thresholds = {}
for bucket = 1, 9 do
local rank = math.ceil(bucket * proteinLength / 10)
if rank < 1 then rank = 1 end
if rank > proteinLength then rank = proteinLength end
thresholds[bucket] = sortedDiffs[rank]
end
local builder = {}
for i = 1, proteinLength do
local delta = diffs[i]
local digit = 9
for bucket = 1, 9 do
if delta <= thresholds[bucket] then
digit = bucket - 1
break
end
end
builder[i] = string.format("%d", digit)
end
return table.concat(builder)
end
function ensureSelection()
local selected = 0
for i = 1, proteinLength do
if selection.IsSelected(i) then
selected = selected + 1
end
end
--error("Barrel Rebuild: please create a selection before starting the script.")
if selected < 2 then
selection.DeselectAll()
createAutoSelections()
end
end
function createAutoSelections()
local selected = 0
for i = 1, proteinLength do
if structure.GetSecondaryStructure(i) == "E" then
selection.Select(i)
selected = selected + 1
end
end
if selected > 1 then
log(2,"No selections found. Selecting Beta Sheets.")
return
end
selected = 0
for i = 1, proteinLength do
if structure.GetSecondaryStructure(i) == "L" then
selection.Select(i)
selected = selected + 1
end
end
if selected > 1 then
log(2,"No selections found. Selecting Loops.")
return
end
error("Barrel Rebuild: please create a selection before starting the script.")
end
function RequestOptions()
local ask = dialog.CreateDialog(script_name .. version)
ask.topPercent = dialog.AddSlider("ShortFuze %", options.topPercent, 5, 100, 0)
ask.draftFuseEvery = dialog.AddSlider("DraftFuseEvery", options.draftFuseEvery, 0, 50, 0)
ask.seedFromFusedEvery = dialog.AddSlider("AcceptFusedEvery", options.seedFromFusedEvery, 0, 50, 0)
ask.reportLevel = dialog.AddSlider("ReportLevel", options.reportLevel, 1, 4, 0)
ask.Cancel = dialog.AddButton("Cancel", 0)
ask.OK = dialog.AddButton("Start", 1)
local ret = dialog.Show(ask)
if ret ~= 1 then
error("Cancelled by user")
end
options.topPercent = math.max(1, math.min(100, ask.topPercent.value))
options.reportLevel = math.max(1, math.floor(ask.reportLevel.value + 0.5))
options.seedFromFusedEvery = math.max(0, math.floor(ask.seedFromFusedEvery.value + 0.5))
options.draftFuseEvery = math.max(0, math.floor(ask.draftFuseEvery.value + 0.5))
end
function parseInput(input)
local result = {}
for quadruplet in input:gmatch("{([^}]+)}") do
local values = {}
for value in quadruplet:gmatch("[^,%s]+") do
table.insert(values, tonumber(value))
end
table.insert(result, {
clashImportance = values[1],
shakeIter = values[2],
clashImportance2 = values[3],
wiggleIter = values[4]
})
end
return result
end
function Fuze2(inputString)
recentbest.Save()
local fuzeConfig = parseInput(inputString)
for _, step in ipairs(fuzeConfig) do
behavior.SetClashImportance(step.clashImportance or 1)
if step.shakeIter and step.shakeIter > 0 then
structure.ShakeSidechainsAll(step.shakeIter)
elseif step.shakeIter and step.shakeIter < 0 then
structure.WiggleAll(-step.shakeIter, false, true)
end
behavior.SetClashImportance(step.clashImportance2 or 1)
if step.wiggleIter and step.wiggleIter > 0 then
structure.WiggleAll(step.wiggleIter)
elseif step.wiggleIter and step.wiggleIter < 0 then
structure.WiggleAll(-step.wiggleIter, true, false)
end
end
recentbest.Restore()
end
function GetSolutionSubscores(solutionId)
if not scorePartsCache then
scorePartsCache = puzzle.GetPuzzleSubscoreNames() or {}
end
local solutionSubscores = { SolutionID = solutionId }
for _, scorePart in ipairs(scorePartsCache) do
local sum = 0
for segmentIndex = 1, proteinLength do
sum = sum + (current.GetSegmentEnergySubscore(segmentIndex, scorePart) or 0)
end
solutionSubscores[scorePart] = sum
end
return solutionSubscores
end
function GetHighestSolutionIDs(subscoreArray)
local highest = {}
if not subscoreArray or #subscoreArray == 0 then
return highest
end
for scorePart, _ in pairs(subscoreArray[1]) do
if scorePart ~= "SolutionID" then
local maxVal = -math.huge
local minVal = math.huge
local ids = {}
for _, row in ipairs(subscoreArray) do
local value = row[scorePart]
if value then
if value > maxVal then
maxVal = value
ids = { row.SolutionID }
elseif value == maxVal then
table.insert(ids, row.SolutionID)
end
if value < minVal then
minVal = value
end
end
end
if maxVal > minVal then
highest[scorePart] = ids
end
end
end
return highest
end
function reportTopCandidates(candidates)
if not candidates or #candidates == 0 then
log(2, "No ranked candidates to report.")
return
end
if (options.reportLevel or 2) < 2 then return end
local shortCount = math.max(1, math.floor(#candidates * options.topPercent / 100 + 0.5))
local limit = math.min(shortCount, #candidates)
--local limit = math.min(5, #candidates)
log(3, "Top %d candidates:", limit)
for i = 1, limit do
local rec = candidates[i]
log(3, "%2d) Slot %3d | Score %7.3f | BB %7.0f / Rank %6.1f", i, rec.id or 0, rec.score or 0, rec.scoreBB or 0, rec.rank or 0)
end
end
function attemptUniqueSolution(slotIndex)
local attempts = 0
local duplicateHits = 0
local success = false
local finalScore = nil
local finalBB = nil
while attempts < options.attemptPerSlot do
attempts = attempts + 1
local rebuildIter = ((attempts - 1) % options.rebuildPasses) + 1
save.Quickload(BEST_SLOT)
log(4, "[Slot %d] attempt %d (RebuildIter=%d)", slotIndex, attempts, rebuildIter)
structure.RebuildSelected(rebuildIter)
finalScore = ScoreReturn()
finalBB = ScoreBBReturn()
local digest = captureDigest()
if not duplicateRegistry[digest] then
duplicateRegistry[digest] = true
save.Quicksave(slotIndex)
save.Quickload(BEST_SLOT)
success = true
break
else
duplicateHits = duplicateHits + 1
log(4, "[Slot %d] duplicate #%d", slotIndex, duplicateHits)
end
end
if success then
log(4, "[Slot %d] stored unique solution after %d attempts (%d duplicates)", slotIndex, attempts, duplicateHits)
else
log(3, "[Slot %d] attempt cap reached (%d). Keeping last candidate", slotIndex, attempts)
save.Quicksave(slotIndex)
save.Quickload(BEST_SLOT)
end
return success, duplicateHits, attempts, finalScore, finalBB
end
function fillSolutionSlots()
undo.SetUndo(false)
log(2, "Generating %d solutions (slots %d-%d)", SOLUTION_COUNT, START_SLOT, END_SLOT)
local duplicatesRejected = 0
for slotIndex = START_SLOT, END_SLOT do
local success, duplicateHits, attempts, slotScore, slotBB = attemptUniqueSolution(slotIndex)
duplicatesRejected = duplicatesRejected + duplicateHits
log(3, "[Slot %d] BB %.0f | score %.3f | attempts %d", slotIndex, slotScore or 0, slotBB or 0, attempts)
end
undo.SetUndo(true)
log(1, "Solution slots ready: %d candidates, duplicate hits %d", SOLUTION_COUNT, duplicatesRejected)
return SOLUTION_COUNT, duplicatesRejected
end
function collectCandidates(count)
log(2, "Loading %d slots for ranking", count)
local candidates = {}
solutionSubscoresArray = {}
for offset = 0, count - 1 do
local slot = START_SLOT + offset
save.Quickload(slot)
local record = {
id = slot,
score = ScoreReturn(),
scoreBB = ScoreBBReturn(),
rank = 0
}
table.insert(candidates, record)
table.insert(solutionSubscoresArray, GetSolutionSubscores(slot))
log(3, "[Slot %d] score %7.3f | BB %7.0f", slot, record.score or 0, record.scoreBB or 0)
end
save.Quickload(BEST_SLOT)
log(2, "Collected %d candidates", #candidates)
return candidates
end
function assignRank(list, field)
table.sort(list, function(a, b)
if a[field] == b[field] then
return a.id < b.id
end
return (a[field] or -1e18) > (b[field] or -1e18)
end)
local bonus = #list + 1
for index, rec in ipairs(list) do
rec.rank = (rec.rank or 0) + (bonus - index)
end
end
function rankCandidates(candidates)
log(2, "Ranking %d candidates", #candidates)
for _, rec in ipairs(candidates) do
rec.rank = 0
end
assignRank(candidates, "score")
assignRank(candidates, "scoreBB")
local highest = GetHighestSolutionIDs(solutionSubscoresArray)
local subscoreBonus = math.floor(#candidates / 12 * 10 + 0.5) / 10
for _, rec in ipairs(candidates) do
for _, ids in pairs(highest) do
for _, id in ipairs(ids) do
if id == rec.id then
rec.rank = rec.rank + subscoreBonus
break
end
end
end
end
table.sort(candidates, function(a, b)
if a.rank == b.rank then
if a.score == b.score then
return a.id < b.id
end
return (a.score or 0) > (b.score or 0)
end
return (a.rank or 0) > (b.rank or 0)
end)
reportTopCandidates(candidates)
end
function updateBestFromCurrent(candidateSlot, startScore)
local fusedScore = ScoreReturn()
if not bestScore or fusedScore > bestScore then
local previous = bestScore or fusedScore
bestScore = fusedScore
save.Quicksave(FUZED_SLOT)
local fusedDigest = captureDigest()
duplicateRegistry[fusedDigest] = true
save.Quickload(TEMP_SLOT)
save.Quicksave(BEST_SLOT)
local bestDigest = captureDigest()
duplicateRegistry[bestDigest] = true
save.Quickload(BEST_SLOT)
log(1, "New best slot %d score %.3f (Δ%.3f)", candidateSlot, fusedScore, fusedScore - previous)
log(2, "Total gain: %.2f", bestScore - initialScore)
else
save.Quickload(BEST_SLOT)
if startScore then
log(3, "Slot %d: %.3f → %.3f (no improvement; best %.3f)", candidateSlot, startScore, fusedScore, bestScore or fusedScore)
else
log(3, "Slot %d: no improvement (%.3f ≤ %.3f)", candidateSlot, fusedScore, bestScore or fusedScore)
end
end
end
function runDraftFusePass(candidates)
log(2, "Running draft fuse pass for %d candidates", #candidates)
for index, rec in ipairs(candidates) do
save.Quickload(rec.id)
save.Quicksave(TEMP_SLOT)
local before = ScoreReturn()
Fuze2(DRAFT_FUSE_CONFIG)
local afterDraft = ScoreReturn()
log(3, "[Draft %2d/%2d] slot %2d: %6.3f → %6.3f / Rank %6.1f", index, #candidates, rec.id, before, afterDraft , rec.rank or 0)
--save.Quicksave(rec.id)
updateBestFromCurrent(rec.id, before)
end
end
function runShortFuseCandidate(rec, shortIndex, shortTotal)
save.Quickload(rec.id)
save.Quicksave(TEMP_SLOT)
local startScore = ScoreReturn()
Fuze2(SHORT_FUSE_CONFIG)
local afterShort = ScoreReturn()
log(2, "[Short %2d/%2d] slot %2d: %6.3f → %6.3f / %3.1f", shortIndex, shortTotal, rec.id, startScore, afterShort , rec.rank or 0)
--save.Quicksave(rec.id) -- keep short result for potential long fuse
updateBestFromCurrent(rec.id, startScore)
return afterShort
end
function runLongFuseCandidate(rec, longIndex, longTotal)
save.Quickload(rec.id)
save.Quicksave(TEMP_SLOT)
local startScore = ScoreReturn()
Fuze2(LONG_FUSE_CONFIG)
local afterLong = ScoreReturn()
log(2, "[Long %2d/%2d] slot %2d: %6.3f → %6.3f / Rank %6.1f", longIndex, longTotal, rec.id, startScore, afterLong , rec.rank or 0)
--save.Quicksave(rec.id)
updateBestFromCurrent(rec.id, startScore)
end
function runFuzes(candidates, opts)
opts = opts or {}
local draftMode = opts.draftMode
local total = #candidates
if total == 0 then
return
end
local shortCount = math.max(1, math.floor(total * options.topPercent / 100 + 0.5))
local longPercent = math.sqrt(options.topPercent)
local longCount = math.max(1, math.floor(total * longPercent / 100 + 0.5))
if longCount > shortCount then
longCount = shortCount
end
if draftMode then
local draftLongCount = math.min(longCount, total)
log(2, "Draft fuse iteration.", draftLongCount)
for i = 1, draftLongCount do
runLongFuseCandidate(candidates[i], i, draftLongCount)
end
log(3, "Fuse complete. Current best: %.3f", bestScore or ScoreReturn())
return
end
log(2, "Short fuse: %d candidates. Long fuse: %d", shortCount, longCount)
local shortResults = {}
local maxShort = math.min(shortCount, total)
for index = 1, maxShort do
local rec = candidates[index]
local afterShort = runShortFuseCandidate(rec, index, shortCount)
table.insert(shortResults, { rec = rec, shortScore = afterShort, shortIndex = index })
end
table.sort(shortResults, function(a, b)
return (a.shortScore or -1e18) > (b.shortScore or -1e18)
end)
for i = 1, math.min(longCount, #shortResults) do
local entry = shortResults[i]
runLongFuseCandidate(entry.rec, i, longCount)
end
log(3, "Fuse complete. Current best: %.3f", bestScore)
end
function main()
ensureSelection()
RequestOptions()
save.Quicksave(INIT_SLOT)
save.Quicksave(BEST_SLOT)
save.Quicksave(FUZED_SLOT)
duplicateRegistry[captureDigest()] = true
bestScore = ScoreReturn()
initialScore = bestScore
baseSegmentScores = captureSegmentScores()
while true do
iterationCounter = iterationCounter + 1
local bestBeforeIteration = bestScore
local draftMode = isDraftIteration()
phaseReport("Generating solutions")
local _, duplicateHits = fillSolutionSlots()
log(3, "Duplicates rejected this cycle: %d", duplicateHits)
phaseReport("Ranking")
local candidates = collectCandidates(SOLUTION_COUNT)
if draftMode then
phaseReport("Draft fuse pass")
runDraftFusePass(candidates)
candidates = collectCandidates(SOLUTION_COUNT)
end
rankCandidates(candidates)
phaseReport(draftMode and "Draft long fuse" or "Fusing")
runFuzes(candidates, { draftMode = draftMode })
if bestScore > bestBeforeIteration then
local gain = bestScore - bestBeforeIteration
noGainIterations = 0
phaseReport(string.format("Gained this iteration +%.3f", gain))
else
noGainIterations = noGainIterations + 1
local freq = options.seedFromFusedEvery or 0
if freq > 0 and noGainIterations >= freq then
local streak = noGainIterations
acceptFusedBase(string.format("No gain for %d iterations — accepting fused solution", streak))
phaseReport(string.format("No gain streak hit %d → fused base accepted", streak))
else
if freq > 0 then
phaseReport(string.format("No gain (%d/%d until switching to fuzed solution)", noGainIterations, freq))
else
phaseReport(string.format("No gain (%d in a row)", noGainIterations))
end
end
end
if baseSegmentScores then
local map = segmentScoreMapString(baseSegmentScores)
if map ~= "" then
log(2, "Segment score change map: \n%s", map)
end
end
end
end
function cleanup(err)
if err and err ~= "" then
log(1, "Cleanup: %s", tostring(err))
end
save.Quickload(INIT_SLOT)
save.Quickload(BEST_SLOT)
save.Quickload(FUZED_SLOT)
if bestScore and initialScore then
log(1, "Net change: %.3f", (bestScore or initialScore) - initialScore)
end
behavior.SetClashImportance(1)
end
xpcall(main, cleanup)