Skip to content

Longer solve attempts, kept per-island solves, and a faster engine - #8

Merged
OGMods merged 5 commits into
mainfrom
solver-budgets-and-kept-solves
Oct 16, 2026
Merged

OGMods merged 5 commits into
mainfrom
solver-budgets-and-kept-solves

Conversation

@OGMods

@OGMods OGMods commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Five commits, each passing npm run check and npm test on its own:

  1. Tighten the Tidal bound and carry its inland crossing - each generator is held to what its own neighbours can feed (bound drops 0.1-1.8% on a late roster), and the inland split is carried instead of re-bisected (946ms to 18.5ms across all maps, bit-identical bounds).
  2. Lengthen Deep and Max attempts, and give Quick its whole budget - Deep 5 x 20s, Max 25 x 20s at the same cost; fitBudgetsToPool moves into the solver package so the app's Quick and the CLI's pooled run both use the full budget. Copy and docs updated.
  3. Keep each island's best solve across re-runs, imports and Setup changes - per-island merge of held and fresh shortlists, solves carried over through a game import or a roster/research/anomaly change, progress coalesced to one page update per report interval, and "Discard this layout" applied when the run starts.
  4. Cut the seed, repair and distribution costs; size the seed for Singularity - memoised seed fits (3.5x), local greedy polish, a cheaper distribution repair, a Singularity-aware seed, and npm run profile.
  5. Split placementSearch.ts into one file per search stage - a pure move into solver/search/, plus the worker-safety layering test resolving imports relative to each file.

Fixtures reproduce byte for byte across the engine refactors; golden-layout optima do not go down.

The two-class bound under a terrain bonus credited every generator with
the eight-neighbour cap at the shore rating, as if each were ringed by
boosted reactors and coolers. Only tiles one step from the coast can be.
Once the cap binds - generator7 over any reactor short of the top tiers,
where a ring of eight feeds about 65% of its rating - that was slack.
Each generator is now held to what its own neighbours can feed, counted
by class (shore or inland). On a late roster (full unlocks less cooler8
and ecto_compressor, precursor_pyramid tier 3) the Tidal bound drops
0.1-1.8% across the shipped maps; full unlocks and weaker rosters do not
move. A binding-cap island with hand-built and 300 arbitrary layouts
pins it.

With intake no longer linear the inland reactor split had to be found
by bisection, for every shore split of every engine split: ~100ms a call
on Event Horizon and Entropy Isles, read several times per render. Each
shore reactor added raises the heat side and lowers the intake side, so
the crossing only moves down; it is now carried from one shore split to
the next and galloped down from there. Both sides stay monotone in
floating point, so it finds the same count a fresh bisection did and
every map's bound is bit-identical. 946ms to 18.5ms across all maps,
faster than before the cap was added.
Deep is now 5 x 20s and Max 25 x 20s, at the same cost as 10 x 10s and
50 x 10s. A walk on a large island is still climbing well past 10s on a
late roster: over all eight maps the search reaches 91.7% of the bound at
10s, 92.9% at 20s and 93.6% at 40s (late roster, no anomaly), 95.3% and
96.1% at full unlocks, and one 20s walk matches the best of three 10s
ones at two-thirds of the time. Both Tidals gain no more than the base
rules, so they take no attempt factor; Singularity keeps its 2x. The mode
dialog's note now gives each anomaly's own reason.

Islands run side by side, so a run whose every task has a worker of its
own lasts as long as its largest slice, not the 30s Quick advertises -
16-24s on boards of two large islands. fitBudgetsToPool, now in the
solver package beside islandBudgetsS, stretches the slices until the
largest is the whole budget when the run fits the pool; a run that
queues keeps the summed split. The coordinator, the run estimate and the
CLI's pooled run all use it, so the CLI's figures match what the app's
Quick gets on the same board. The survey keeps the summed split - it
measures one attempt's slices, which is what a queued Deep or Max spends.

The mode dialog, README, CLI docstring and docs/SOLVER.md are brought up
to the new figures.
"Keep the better layout" compared whole boards, so on a board of several
islands a re-run that found a better large island won on the total and
brought a worse small island with it. Islands never interact, so
composeIslandBests now builds boards from each island's best answer
across the held and fresh shortlists, and those stand in for the fresh
run before the whole-board comparison. It returns null when one board
already holds every best (and under a shared cooling pool, which is one
island). Under an upgrade plan islands are compared on the power after
the plan. Only held layouts still standing on grass are offered, so
terrain painted since cannot carry a building onto rock. A two-island
board driven through runOptimizer pins the merge.

A game import changes the roster, the research and usually the terrain,
all in a solve's signature, so every island's solve was discarded though
most were still buildable. carryOverSolves now runs before restore() on
a game import and decides per island: a solve is dropped for an island
still locked in the game or searched under another anomaly; a layout is
dropped if an imported obstacle covers one of its buildings or the
roster no longer unlocks one. Everything else is re-tiered, re-scored on
its new board and re-filed under its new signature. restore() does the
same when only the roster, research or anomaly differ, so an island
opened after a Setup change keeps its solve, re-rated. Changed terrain
is still discarded. settleOptimizer stops a run in flight and waits for
it to file, so a late result cannot land over the carried-over record.
A pre-anomaly record's unknown anomaly id now resolves to the base rules.

Every worker reports on its own clock, so a pool of eight handed the
main thread up to eight full boards a second. On a page slowed 6x a Deep
run kept the main thread busy 9.5s of 11s, and a main thread that falls
behind stops updating the board and never forwards Stop. The coordinator
now hands the page at most one update per report interval, still drawing
the best anything has reported, and cancels a pending update when the
run settles: 4.3s busy, 22 long tasks instead of 87.

"Discard this layout" now removes it from the board and from storage as
the run starts, so a run stopped before it reports cannot hand it back.
…arity

Seed construction re-simulated every generator combination for every
stale tile on every round; a shared fit depends only on the
neighbourhood's shape under a uniform or role-wide rating, so it is
memoised by that shape and the seed is 3.5x faster, bit for bit.
greedyPolish re-sweeps only tiles within two of an applied move and
re-simulates a cached winner before applying it: a wash at 15s per map,
+5-9% under Tidal and +16% on Shadowspire under Singularity at 5s, where
the polish share binds. The distribution's repair ends each search when
the first consumer with room is enqueued rather than dequeued (the same
path, the queue being FIFO) and keeps a per-component list of suppliers
with supply left: 17% off a Cryo whole-board simulation, 9% off the base
rules, bit-identical over 5054 random instances and the fixtures. The
collector's tie check drops a redundant key string, targetCompositions
keeps only its top three while enumerating, Singularity's layout copy
folds into the scan loop, and the post-deadline cleanup yields between
alternates.

Under Singularity the seed now sizes each hub at the generator's
isolated rating and claims no hub whose generator would stand beside
another, and solveIsland starts from whichever of that seed and the
plain one repairs higher: a wash at 15s, +14% and +18% on the two
largest maps at 5s, nothing lost at generator7 tier 3 or under any other
rule.

Reheating reclaim rounds to a tenth of the walk temperature and an
un-crowding move in the walk were both measured as noise and left out;
the comments record them. `npm run profile` is the stage-cost instrument
behind all of this. goldenLayoutsFullTier.test.ts is reformatted.
A pure move: every line of the old file lands verbatim in exactly one of
search/placement, stability, seed, anneal, composition, repair, rightSize
and pooledCoolers, or stays in placementSearch.ts with solveIsland, the
budget split, the deterministic replay and `internals`. That module still
exports exactly what it did, so no import site changes. Imports and
exports were derived from what each body uses, and the new modules form
no cycle. The one edit is a doc comment, "Solves one island", which sat
above replayIslandDeterministic and now sits on solveIsland. The fixtures
reproduce byte for byte and the stage profiler cannot tell the two trees
apart.

The worker-safety layering rule read `../x` as leaving src/solver/,
which holds only for a module directly inside it: a subfolder importing
`../constants` would have been refused. Each specifier is now resolved
against the importing file's own folder, and the cases a subfolder
raises are pinned.
@OGMods
OGMods merged commit 1d0778c into main Oct 16, 2026
3 checks passed
@OGMods
OGMods deleted the solver-budgets-and-kept-solves branch October 16, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant