Smithing solver
Research toward a solver that searches crafting recipes for the best weapon per goal: what was measured, what was rejected, and the candidate design. The findings were measured on game v1.4.6. The solver design targets v1.4.8, whose smithing IL and crafting XML are identical to v1.4.6 (method-by-method comparison: Value and price). Evidence: research/smithing-solver/challenge/round{2,3}/.
The formulas the solver evaluates are in the crafting deep dives: Weapon stats, Value and price and Smithing mechanics. The game rules for part unlocking and orders are in Smithing mechanics.
Evidence labels:
- IL: read from game code. Only the order pass rule,
CanHeroCompleteOrderand unlock scope were checked twice; the other IL rows have one read. - Test: measured with the formulas outside the game, not re-run independently.
- Nothing here has run in the game.
Decisions
- Each stat is maximized or minimized.
- Goals use priority levels and show a top N list.
- Equal priority: stats scaled to 0–1 with fixed ranges per template and usage, plus an optional per-stat weight, default 1.
- Repeats are hidden only for the same piece combination.
- A tick selects unlocked or all parts. Scaling is in scope.
- Near-best results are acceptable. How much exactness a mode promises is still open.
Search-space approaches
| Approach | Result | Verdict |
|---|---|---|
| Merge physically identical pieces | 1.4× fewer recipes | Keep |
| Same-geometry blades, keep the best factor | Keeps 132/135, provable | Keep, small effect |
| Closed-form evaluation with a speed table | 127.8M recipes in about 2 minutes in numpy | Keep. Lowers cost per recipe, not recipe count |
| Apply numeric limits early | speed >= 90 leaves 79.6M of 127.8M |
Only tight limits prune much |
| Formula upper bounds per recipe group | Never run | Unknown |
| Shipped dominance lists | Failed 41/96 | Rejected |
| Dominance on the player's own pool | 40–99% fewer recipes, 3/288 losses | Candidate |
| Fixed scale patterns per weapon type | Fails with numeric limits | Rejected as the main search |
| 17 scale points, then climb the top 5 | Mean loss 0.22, max 5 | Baseline |
| Local search | Lost up to 15 with numeric limits | Instant first answer only |
| Per-piece effects, rank, exact check | About 6,700× fewer evaluations on the largest pool | Lead candidate |
Fixed patterns vs scale sampling
Winner across a whole pool, 600 cases, all 12 templates. Exhaustive baselines used small pools (320 recipes for 4-slot templates). Loss is in displayed stat points. Test.
| Method | Mean loss | Max loss | Pool winner kept | Evaluations per recipe |
|---|---|---|---|---|
| Scale 100 only | 4.45 | 53 | 71% | 1 |
| 1 fixed pattern | 2.92 | 49 | 71% | 1 |
| 8 fixed patterns | 0.41 | 37 | 96% | about 2,040 |
| 17 scale points, then climb the top 5 | 0.22 | 5 | not measured | about 3,950 |
- 17 points are the scale corners and centre, out of 194,481 scale combinations.
- 1 fixed pattern without numeric limits: mean loss 0.17, winner kept 100%.
- 1 fixed pattern with numeric limits: winner kept 52%.
- 8 fixed patterns with numeric limits, worst case: 101 vs 138 damage on a two-handed sword.
- Near-best: 97.8% of results within 1 point, 99.7% within 2, worst 5.
- Full all-parts
OneHandedSword: 2.7G evaluations, requires pruning. C# speed is unmeasured.
Dominance
- A piece is dominated when another piece of its slot is at least as good on every goal stat, for every sampled combination of the other slots.
- Failures on the player's own pool were all max-length goals.
- Goals with a maximum or a range can break dominance.
- Shipped lists fail because the dominating piece may be locked.
- A minimized stat compares in the opposite direction. Only low weight has been tried this way, and not at pool level.
- About 10% of "worse" verdicts failed on new recipes in the early one-handed sword spike.
Speed tables
- Speed is a staircase, so a table can be off by up to 3 points. Results need an exact final check.
- Build at load: 0.15 s on 16 threads.
Per-piece effects
Method:
- Measure the effect of each (piece, scale) on each stat, on a set of random base weapons.
- Predict a recipe as the mean base stat plus one effect per slot.
- Rank recipes by predicted score, with numeric limits relaxed by the 95th-percentile prediction error.
- Evaluate the top K exactly.
Fit cost = bases × (levels across all slots + 1): 13,920 evaluations for 30 bases on OneHandedSword at scale 100.
Stability of one same-slot swap across weapons, 72 small-pool cases, all 12 templates. Test.
| Stat | Spread of the % change, p5–p95 | Swaps where the better piece flips |
|---|---|---|
| Weight | 2.6–3.6 points | 0% |
| Length | 1.9 points | 0% |
| Speed | 1.9–2.0 points | 3–4% |
| Handling | 2.0 points | 5–7% |
| Thrust damage | 4.0–4.3 points | 27–35% |
| Swing damage | 5.4–6.0 points | 38–41% |
- Flips count any disagreement, so some are probably rounding between near-equal pieces.
- Additive points are exact for weight and more accurate than percents for length and speed. Damage is about equal.
- Across goals, points were generally as good or better. Percents did better on the polearm limit case below.
Loss at 10,000 exact checks, 30 bases, additive points, two fit seeds. Single-stat and limit goals lose stat points. Equal-priority goals lose points on a 0–100 score. Limits are percentiles of the pool's stat, for example speed top 5%. Test.
| Goal | OneHandedSword 160M, scale 100 |
OneHandedSword 207M, scaled |
TwoHandedSword 207M, scaled |
|---|---|---|---|
| Max damage | 0 / 0 | 1 / 1 | 0 / 0 |
| Max speed | 0 / 0 | 0 / 0 | 0 / 0 |
| Damage = speed | 1.3 / 1.1 | 2.3 / 2.9 | 1.2 / 1.6 |
| Damage = speed = handling | 0 / 0 | 0 / 0 | 0 / 0 |
| Damage = lightness | 0 / 0 | 0 / 0 | 0 / 0 |
| Damage, speed top 25% | 1 / 1 | no valid result | 2 / 2 |
| Damage, speed top 5% | 0 / 0 | 0 / 0 | 4 / 4 |
| Damage, handling top 5% | 1 / 1 | 0 / 0 | 0 / 0 |
- Scaled pools use 40 sampled pieces per slot and scales 90, 100 and 110 only.
- Total for
OneHandedSwordat scale 100: about 24,000 exact evaluations vs 160M. TwoHandedSwordat scale 100 (26M),Dagger(3.4M) andTwoHandedPolearm(1.4M) lost 0 at 10,000 checks on nearly all goals.- One base weapon is unreliable:
TwoHandedPolearmlost 1 and 8 damage at 1,000 checks, and limit goals often found nothing valid at 10,000. - Limits with scaling are the weak spot:
OneHandedSword, speed top 25%, needed 100,000 checks and still lost 3. - More checks do not always help:
TwoHandedPolearm, speed top 25%, lost 5 in one run even at 100,000 checks, because the relaxed limit filtered out the winner. - Top-10 recall for damage = speed on
OneHandedSwordwas 0 at 100,000 checks with 30 bases, while results stayed within about 1 point. - Small pools understate the difficulty: the top is crowded and usually held by one blade. Random picks reached mean loss 1.5–1.8 at 1,000 checks on max damage, vs 0.1–0.2 with 30 bases.
Not covered:
- Ranking still scored every recipe with the model. 160M lookups is cheaper than evaluation but not instant.
- Additive effects should allow walking from the best combination downward and stopping early. Untested, and limits may complicate it.
- Ranges came from a 200,000-recipe sample, not true min and max.
- No head-to-head with 17 scale points.
- Templates under about 100,000 recipes can be evaluated in full.
Candidate pipeline
Not approved or tested end to end.
- Filter to unlocked, non-hidden pieces, or all parts when ticked, marking results that can't be crafted.
- Merge identical pieces.
- Measure per-piece effects at load, with at least 30 bases.
- Walk the best predicted combinations from the top.
- Check candidates exactly, adding more until numeric limits are met.
- Refine scales on the best candidates within ±10.
- Hide repeats of the same pieces and run a final exact check.
Goal ranking
Priority order:
- Strict priority order breaks down, so each level needs a tolerance.
- A raw top 10 has a median of 2 distinct recipes.
- A top list recalled about half of the true top 10 (0.44–0.57), so it must climb more candidates than N.
Equal priority:
maximize: (value − min) ÷ (max − min)
minimize: (max − value) ÷ (max − min)
score = sum of weight × scaled value
- Each min and max is its own search. Near-best is enough, because a point of error barely moves the scale. Cost is unmeasured.
- Stats where min equals max are dropped from the score.
- With fixed ranges, unlocking a part does not change any recipe's score.
- Very weak parts stretch a range, so each point of that stat counts for less.
- Damage falls to 65 with damage = speed = handling because speed and handling correlate and outvote it 2 to 1. Weights are the fix.
Methods compared on 160,000 exported one-handed swords, one template, scale 100. Values are damage / speed / handling. The last column recalculates ranges without wooden blades (5% of the data). Test.
| Method | Damage = speed | + handling | Without wooden blades |
|---|---|---|---|
| Sum in standard deviations | 90/93/93 |
65/103/104 |
92/92/92, 65/103/104 |
| Sum, min-max range | 92/92/92 |
65/103/104 |
95/90/87, 65/103/104 |
| Sum, 5–95% range | 89/93/94 |
67/102/102 |
92/92/92, 73/98/99 |
| Sum of percentile ranks | 80/94/93 |
72/98/101 |
80/94/93, 77/96/97 |
| Geometric mean | 90/93/94 |
65/103/104 |
92/92/92, 89/93/95 |
| Closest to ideal | 90/93/94 |
65/103/104 |
90/93/94, 89/93/95 |
| Best worst stat | 73/98/97 |
73/98/99 |
90/93/94, 89/93/95 |
| Worst percentile rank | 77/96/97 |
77/96/97 |
80/94/93, 78/94/97 |
damage·(speed/100)^1.5 |
92/92/91 |
handling ignored | 92/92/91 |
- Knee of the trade-off curve picked
65/103/104for damage and speed. - The min-max shift in the last column comes from recalculating ranges. Fixed ranges avoid it.
- Fixed per-template ranges: 0/876 winner changes when a non-winning part is added. Pool-based ranges: 29/873.
- True formula ranges have not been compared with these sampled ranges.
Order solving
Test: treating orders as per-stat minimums found a valid design in 76% of cases, vs 100% for the sum rule, at higher difficulty and material use. Candidate rule: minimize material cost subject to sum and damage type, showing the success chance.
Invalidated results
| Claim | Status |
|---|---|
| Scale-pattern table from the first spike | Not reproducible: recipes were drawn from an unordered set |
| Polearm pattern caused by handle drag | Noise |
| One-handed sword with a limit loses at most 1 | No pattern met the limit on some recipes in 3 of 4 reruns |
| Pruning lost the best weapon, 103 vs 104 | Probably wrong: the 103 had no exact check |
| Fast evaluator results stated as exact | Off by up to 2 damage on 15–30% of recipes and 1 speed on 6–16% |
| Min-max is fragile | Applies only when ranges are recalculated per pool |
Held up across the scale reruns: all-90 scale was exact for maximum speed and handling, and maximum-damage patterns for swords and maces stayed near 110.
Open questions
- Whether per-piece effects become the core search. Needs an end-to-end test with full scaling, against 17 scale points.
- Whether anything is shipped precalculated, or everything is calculated at load.
- Tolerance per priority level.
- Whether numeric limits stay as optional filters.
- How much exactness a mode promises.
- Whether
coh_sword_blade_1, part of the 104-damage one-handed recipe, is craftable. - Which hidden pieces to exclude from pools. Counts including them: 177.8M vs 160.4M.
Solver design
Pools
Craftable pool = non-hidden ∩ template UsablePieces ∩ usage AvailablePieces, then unlocked pieces, then material ticks (remove pieces needing an unticked material).
- 24 live template×usage pairs, 211.6M recipes at scale 100. OneHandedSword 151.05M.
- Scale: each present slot 90..110 integer percent. Modes: 100, 90/100/110, all 21.
Architecture
| Part | Offline (CI) | Shipped | Runtime (C#, net472) |
|---|---|---|---|
| Evaluator | parity vs ev.c (bit-identical) |
nothing | step tables built at load (s; polearm lazy) |
| Fight | GPU exhaustive truth suite | nothing | exact B&B (sbb3) over filtered pool |
| Order | truth suite | nothing | exact top N by P(success) |
| Native | optional smcore.dll (AVX-512/2) |
optional | LoadLibraryW by full path, C# fallback |
- One search per (template, usage).
- Value is per item (best + second-best usage), not per usage.
- Thrown: stack 1 for
Dagger.ThrowingKnifeandTwoHandedPolearm_Thrown. Displayed type = thrust, else swing.
Results
- Top N, tie-inclusive: rows scoring ≥ N-th score, ≤ 2N rows,
ties_moreflag. Each row: recipe, scales, all display stats, value, score. UI sorts rows by any column. - No recipe under limits:
no_recipe+ nearest misses by Σ normalized limit violation. - Labels: Exact; on budget stop Near-best + gap from frontier max.
Order model
- Success ⇔ damage types match ∧ Σ_t max(v_t + δ_qt, 1) ≥ order sum over shared stats (weight excluded).
- Randomness: quality roll only (skill − difficulty sigmoid, perks, tier cap). P(success) exact over 6 qualities; thrown deterministic.
- Rank: P(success) desc, material cost asc, E[S] desc.
Evidence
- Speed monotonicity proven over the reachable set (Ls ≤ 57.36, Lt ≤ 15.45, reach ≤ 3.72); rerun
round3/monotoneif grip constants change. - Fight: ~120k solves vs exhaustive truth, 0 wrong. Order: 1,240 solves, 0 wrong.
- C#: 100.8M recipes + 587.5k boundary cases bit-identical on .NET Framework 4.8.1; B&B identical counts to C.
Open
- In-game confirmation: v1.4.8 re-export; order quality rolls and gold.
- In-process: module DLL loading, Harmony coexistence, non-AVX2 CPUs.
- Order modes at 21 scales and swords at 90/100/110.
- Sell market model not validated in game.