docs: add suitbuilder algorithm documentation

Describes the full search pipeline, item loading, bucket creation,
armor reduction, scoring weights, and constraint satisfaction logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-03-07 21:01:25 +00:00
parent ede333ed2e
commit 4e73a5d07d

219
docs/suitbuilder.md Normal file
View file

@ -0,0 +1,219 @@
# Suitbuilder Algorithm
The suitbuilder finds optimal equipment loadouts across multiple characters' inventories. It fills 17 equipment slots (9 armor, 6 jewelry, 2 clothing) using a constraint satisfaction solver with depth-first search and branch pruning.
## Search Pipeline
The search runs in 5 phases, streamed to the browser via SSE:
1. **Load items** - Fetch from inventory API (armor by set, jewelry by slot type, clothing DR3-only)
2. **Create buckets** - Group items into 17 slot buckets, expand multi-slot items
3. **Apply reductions** - Generate tailored variants of multi-coverage armor pieces
4. **Sort buckets** - Order buckets and items within them for optimal pruning
5. **Recursive search** - Depth-first search with backtracking, streaming top 10 results
## Item Loading
Items are fetched from the internal inventory API (`localhost:8000/search/items`) in four batches:
| Batch | Filter | Notes |
|-------|--------|-------|
| Primary set armor | `item_set={name}` | All armor in user's primary set |
| Secondary set armor | `item_set={name}` | All armor in user's secondary set |
| Clothing | `shirt_only` / `pants_only` | Only DR3+ shirts and pants |
| Jewelry | `jewelry_only` + `slot_names={type}` | Rings, bracelets, necklaces, trinkets separately |
After loading, a **domination pre-filter** removes items that are strictly worse than another item in the same slot with the same set. Item A is "surpassed" by item B when B has equal-or-better spells (Legendary > Epic > Major), equal-or-better ratings, equal-or-better armor, and is strictly better in at least one category.
## Bucket Creation
Each of the 17 slots gets a bucket. Items are assigned to buckets with special handling:
- **Multi-slot items** (e.g., "Left Wrist, Right Wrist") are cloned into each applicable slot bucket
- **Generic jewelry** ("Ring" -> Left Ring + Right Ring, "Bracelet" -> Left Wrist + Right Wrist)
- **Robes** (6+ coverage areas) are excluded entirely - they can't be reduced to single slots
All 17 buckets are created even if empty, allowing the search to produce incomplete suits when no valid item exists for a slot.
## Armor Reduction (Tailoring)
Multi-coverage armor can be tailored to fit a single slot. Only loot-generated items (those with a `material`) are eligible. Reduction patterns follow Mag-SuitBuilder logic:
| Original Coverage | Reduces To |
|---|---|
| Upper Arms + Lower Arms | Upper Arms **or** Lower Arms |
| Upper Legs + Lower Legs | Upper Legs **or** Lower Legs |
| Lower Legs + Feet | Feet |
| Chest + Abdomen | Chest |
| Chest + Abdomen + Upper Arms | Chest |
| Chest + Upper Arms + Lower Arms | Chest |
| Chest + Upper Arms | Chest |
| Abdomen + Upper Legs + Lower Legs | Abdomen **or** Upper Legs **or** Lower Legs |
| Chest + Abdomen + Upper Arms + Lower Arms (hauberks) | Chest |
| Abdomen + Upper Legs | Abdomen |
Reduced items are added to the target slot's bucket as `"Item Name (tailored to Slot)"`.
## Bucket Sort Order
### Bucket ordering (which slot to fill first)
Buckets are searched in this priority:
1. **Core armor** - Chest, Head, Hands, Feet, Upper Arms, Lower Arms, Abdomen, Upper Legs, Lower Legs
2. **Jewelry** - Neck, Left Ring, Right Ring, Left Wrist, Right Wrist, Trinket
3. **Clothing** - Shirt, Pants
Within each category, buckets are further sorted by their position in the priority list (not by item count). This means armor slots are always filled before jewelry, and jewelry before clothing.
### Item ordering within each bucket
Items within a bucket are sorted to try the best candidates first. The sort depends on slot type:
| Slot Type | Sort Priority (highest first) |
|-----------|-------------------------------|
| **Armor** | User's primary set > secondary set > others, then crit damage rating desc, then damage rating desc, then armor level desc |
| **Jewelry** | Spell count desc, then total ratings desc |
| **Clothing** (Shirt/Pants) | Damage rating desc, then spell count desc, then other ratings desc |
All sorts include `(character_name, name)` as final tiebreakers for deterministic results.
## Recursive Search
The solver uses depth-first search with backtracking across the ordered buckets:
```
for each bucket (slot) in order:
for each item in bucket:
if item passes constraints:
add item to suit state
recurse to next bucket
remove item (backtrack)
if no items were accepted:
skip this slot (allow incomplete suits)
recurse to next bucket
```
When all buckets are processed, the suit is scored and kept if it ranks in the top N (default 10).
### Branch Pruning
Two pruning strategies cut off hopeless branches early:
1. **Mag-SuitBuilder style**: If `current_items + 1 < highest_armor_count_seen - remaining_armor_buckets`, prune. This ensures we don't explore branches that can't produce suits with enough armor pieces.
2. **Max-items pruning**: If `current_items + remaining_buckets < best_suit_item_count`, prune. The branch can't produce a suit with more items than the best found so far.
### Item Acceptance Rules (`can_add_item`)
An item must pass all of these checks:
1. **Slot available** - The slot must not already be occupied in the current suit state
2. **Item uniqueness** - The same physical item (by ID) can't appear in multiple slots
3. **Set membership** (armor only):
- Primary set items: accepted up to effective limit (5 minus locked primary pieces)
- Secondary set items: accepted up to effective limit (4 minus locked secondary pieces)
- Other set items: **rejected** for armor slots, allowed for jewelry only if they contribute required spells
- No-set items: **rejected** for armor, allowed for clothing always, allowed for jewelry only if they contribute required spells
4. **Spell contribution** (when required spells are specified):
- Items with spells must contribute at least one **new** required spell not already covered by the current suit
- Items where all spells are duplicates of already-covered spells are **rejected**, even from the target sets
- Jewelry has an additional gate: it must contribute an uncovered required spell or it's rejected (empty slot preferred over useless jewelry)
### Locked Slots
Users can lock specific slots with a predetermined set and/or spells. Locked slots are:
- Removed from the bucket list (not searched)
- Their set contributions are subtracted from set requirements (e.g., 2 locked primary pieces means only 3 more needed)
- Their spells are counted as already fulfilled
## Scoring
The scoring system determines suit ranking. Points are awarded in this priority order:
### 1. Set Completion (highest weight)
| Condition | Points |
|-----------|--------|
| Primary set complete (found pieces >= effective need) | **+1000** |
| Secondary set complete | **+1000** |
| Missing primary piece | **-200** per missing piece |
| Missing secondary piece | **-200** per missing piece |
| Excess primary pieces (beyond 5) | **-500** per excess piece |
| Excess secondary pieces (beyond 4) | **-500** per excess piece |
### 2. Crit Damage Rating (armor pieces)
| Rating | Points |
|--------|--------|
| CD1 (crit_damage_rating = 1) | **+10** per piece |
| CD2 (crit_damage_rating = 2) | **+20** per piece |
### 3. Damage Rating (clothing only - Shirt/Pants)
| Rating | Points |
|--------|--------|
| DR1 | **+10** per piece |
| DR2 | **+20** per piece |
| DR3 | **+30** per piece |
### 4. Spell Coverage
| Condition | Points |
|-----------|--------|
| Each fulfilled required spell | **+100** |
### 5. Base Item Score
| Condition | Points |
|-----------|--------|
| Each item in the suit | **+5** |
### 6. Armor Level (tiebreaker only)
| Condition | Points |
|-----------|--------|
| Total armor level | **+1 per 100 AL** (e.g., 4500 AL = +45) |
Score is floored at 0 (never negative).
### Practical Effect of Scoring Weights
The weights create this effective priority:
1. **Complete sets matter most** - A suit with both sets complete (+2000) always beats one with a missing piece, regardless of other stats
2. **Spells matter second** - Each required cantrip/ward is worth +100, so 10 spells = +1000 (equivalent to one complete set)
3. **Crit damage and damage rating are tiebreakers** - CD2 on all 9 armor pieces = +180, DR3 on both clothes = +60
4. **Armor level barely matters** - Only ~45 points for a full suit of 4500 AL; it only breaks ties between otherwise-equal suits
## Frontend Display
Results stream in as SSE events. The frontend maintains a sorted list of top 10 suits:
- New suits are inserted in score-ordered position (highest first)
- If the list is full (10 suits) and the new suit scores lower than all existing ones, it's discarded
- Medals are assigned by position: gold/silver/bronze for top 3
### Score Display Classes
| Score Range | CSS Class |
|-------------|-----------|
| >= 90 | `excellent` |
| >= 75 | `good` |
| >= 60 | `fair` |
| < 60 | `poor` |
### Item Display
Each suit shows a table with all 17 slots. Per item:
- **Armor pieces**: Show CD (crit damage) and CDR (crit damage resist) ratings
- **Clothing pieces**: Show DR (damage rating) and DRR (damage resist rating)
- **Spells**: Show up to 2 Legendary/Epic spells, then "+N more"
- **Multi-slot items** that need tailoring are marked with an asterisk (*)
### Suit Selection
Clicking a suit populates the right-panel equipment slots visual. Users can then:
- Lock slots (preserving set/spell info for re-searches)
- Copy suit summary to clipboard
- Clear individual slots