All posts

14,641 Combinations: When Brute Force Is the Right Answer

The optimizer's search took an hour to write. The simulation took three bugs to get honest. In optimization problems, the model is where you die.

Engineers·11 min read

For engineers. The search took an hour to write. The simulation took three bugs to get honest.

The Attribute Optimizer that shipped in EveLens 1.4.1 finds the best neural remap for a skill plan. At its core is a search algorithm I'm almost embarrassed to describe: try every combination, keep the best one. No gradient descent, no simulated annealing, no cleverness.

I want to defend that choice — and then tell you about the three bugs that nearly shipped anyway, because none of them were in the search. Every optimization system has two halves: the part that explores candidates, and the model that scores them. The search is where engineers instinctively spend their effort. The model is where the bugs live.

01

The Search Space Fits in a Coffee Break

EVE's remap rules: five attributes, each with a base of 17, each raisable by at most 10, with 14 bonus points to distribute. Fix the first four allocations and the fifth is determined. That's four nested loops, bounded by the remaining budget:

for per in 0..10:
  for wil in 0..(14-per):
    for int in 0..(14-per-wil):
      for mem in 0..(14-per-wil-int):
        cha = 14 - per - wil - int - mem   # reject if > 10
        simulate_plan(per, wil, int, mem, cha)

Upper bound 11⁴ = 14,641 candidates; the budget constraint prunes it well below that. Scoring a candidate means simulating a ~50-entry skill plan — cheap arithmetic. The full search over a real plan completes in under a second with two early-exit checks: abandon a candidate the moment its running total exceeds the best-so-far, and abandon if it exceeds a max duration cap.

Could something smarter find the answer faster? Sure. But "under a second" versus "under a millisecond" is not a trade worth any code complexity when the smarter method might miss the global optimum and the dumb one provably can't. Exhaustive search over a small discrete space is not a hack. It's the correct algorithm, and it comes with a certificate: the answer is the best answer, full stop.

That certificate turned out to matter, because it's what exposed the bugs.

02

Bug One: Optimizing Against a Ghost

The optimizer's job is to say "your plan takes X now; with this remap it takes Y." My first version computed X by simulating the plan's entries on a fresh scratchpad — current attributes, implants applied.

The plan in my test had remap points already applied from a previous optimization. The plan editor's total honored them: 398 days. My baseline ignored them: 466 days. So the window compared its proposal against a character that didn't exist — you-without-your-remaps — and offered "447 days, saves 18!" for a plan already at 398.

The Apply button, at that moment, was a machine for making plans 49 days slower while displaying a green savings number.

The fix was almost insulting: use the same simulation call the editor uses, applyRemappingPoints: true. One flag. The deeper fix was a policy: Apply disables itself unless the proposal strictly beats the current state. An optimizer must be incapable of degrading the thing it optimizes — that has to be a property of the UI, not a hope about the math.

03

Bug Two: The Impossible Result

The auto-placement strategy splits a plan into segments at attribute-focus boundaries and optimizes each independently. My first implementation simulated each segment from the character's starting state.

The symptom was a gift, in retrospect: the optimizer reported a 466-day plan "optimized" to 503 days. Worse than doing nothing.

Here's why that's not just wrong but impossible for a correct implementation — and why impossibility is the best debugging signal there is. The current attribute spread is itself one of the 14,641 candidates. Exhaustive search evaluates it. So the reported optimum can never be worse than current... if the scoring model is honest. A result of 503 > 466 doesn't mean the search failed; it means the two numbers came from different models. Guaranteed.

They did. Skills have prerequisites, and training time depends on accumulated skill points. Segment three of a plan doesn't start from a fresh character; it starts from a character who trained segments one and two. Simulating it from the base state re-paid SP that earlier segments already banked — double-counting that inflated every multi-segment total.

The fix: a cumulative scratchpad threaded through the segments. Remap, train, carry the state forward, repeat. Exactly the sequence the character will live.

cumulative = scratchpad(character + implants)
for segment in segments:
    best = optimize(segment, from: cumulative)   # 14,641 candidates
    cumulative.remap(best)
    cumulative.train(segment)                     # SP carries forward
04

Bug Three: Two Truths About One Plan

Bugs one and two fixed, the optimizer said 405 days. The plan editor's panel said 398. Both numbers were now internally correct, which made this the slowest bug to see.

The editor displays entries in whatever sort order the user applied, and computes its total by training them in that order. The optimizer analyzed the raw plan in insertion order. Training order changes total time — a remap point's value depends on what trains after it, and prerequisite SP shifts between entries. Same entries, different sequence, honestly different totals.

There's no bug in either number. The bug is architectural: two components computed "the plan's duration" through two paths. The user sees one plan and expects one truth. The fix was to make the optimizer analyze the display plan — the same object, same order, same implant set the panel uses. One path, one number.

While wiring that, a fourth wrinkle fell out: the display plan only received the character's implant set when the user changed implants, never at creation. Depending on session order, even the panel could silently compute without implants. The kind of bug that survives precisely because it produces plausible numbers.

05

The Scorecard

0
Bugs in the search
3
Bugs in the model
+1
Latent (implants)
24
Golden tests added
Where the effort wentBugs found
The search (loops, pruning, early exit)0
The scoring model (baseline, state, ordering)3 (+1 latent)

Every bug produced a plausible wrong number — no crash, no exception, no red text. 405 days looks exactly as trustworthy as 398. The only reason they were caught is that a human had two windows open showing the same plan and enough stubbornness to ask why they disagreed. Three times.

06

What I'd Carry to Your Codebase

Brute force over small discrete spaces is underrated. If the space is under ~10⁶ and scoring is cheap, enumerate. You trade nothing and gain a proof of optimality — which doubles as a built-in bug detector, because "optimizer made it worse" becomes a logical impossibility instead of a shrug.

In optimization systems, audit the model, not the search. The search has a spec you can test in isolation. The model encodes your domain — baselines, state accumulation, ordering — and every simplification you make there produces numbers that look right.

Two views of one quantity must share one code path. Every "we'll just compute it here too" is a future afternoon of two windows and a seven-day discrepancy.

After the fixes, all three failure modes went into the EVE Accuracy Suite as permanent regression tests — including one that asserts the impossibility directly: optimized <= current, always, for any plan. The best invariants are the ones physics hands you.

o7,

Alia

EveLens is free and open source — the optimizer described here is RemapPlanningService.cs and AttributesOptimizer.cs in the repo. The player-facing half of this story, with an interactive remap simulator, is How EVE Online Attributes and Neural Remaps Work.