MosswartOverlord/docs/plans/2026-01-30-suitbuilder-design.md
erik e0265e261c Add suitbuilder backend improvements and SSE streaming fix
- Add dedicated streaming proxy endpoint for real-time suitbuilder SSE updates
- Implement stable sorting with character_name and name tiebreakers for deterministic results
- Refactor locked items to locked slots supporting set_id and spell constraints
- Add Mag-SuitBuilder style branch pruning tracking variables
- Enhance search with phase updates and detailed progress logging
- Update design document with SSE streaming proxy fix details

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-05 19:14:07 +00:00

8.6 KiB
Raw Permalink Blame History

Suitbuilder Design Document

Date: 2026-01-30 Status: In Progress Reference: magsuitalgo.md (detailed MagSuitBuilder algorithm analysis)


Section 1: Problem Statement & Goals

What is Suitbuilder?

Suitbuilder helps players find the best equipment combination from their inventory across multiple "mule" characters. In Asheron's Call, players have many characters storing thousands of armor pieces, jewelry, and accessories. Finding the optimal combination manually is impractical.

Why is this hard?

The naive approach would check every possible combination: if you have 50 items per slot across 17 equipment slots, that's 50^17 combinations - more than atoms in the universe. MagSuitBuilder solved this with smart algorithms, and we're replicating that approach.

Main Goal - Priority Order

Given user constraints, find equipment combinations that:

  1. Complete armor set bonuses (5 pieces primary set + 4 pieces secondary set) - HIGHEST PRIORITY
  2. Cover all required spells/cantrips without duplicates
  3. Maximize armor protection - LOWEST PRIORITY (tiebreaker)

Why we're doing this

The original MagSuitBuilder is a Windows desktop app. We want this functionality in our web-based Dereth Tracker so players can search their inventories from any browser.


Section 2: Algorithm Overview

The Bucket Approach

Instead of checking all item combinations, we organize items into "buckets" by equipment slot (Head, Chest, Hands, etc.). Then we search through buckets one at a time using recursion with backtracking.

How it works

  1. Create buckets - One bucket per equipment slot (11 armor slots + jewelry slots)
  2. Sort buckets - Process smallest buckets first (fewer branches to explore)
  3. Recursive search - Try each item in bucket 1, then recurse to bucket 2, etc.
  4. Backtrack - When a branch fails constraints, undo and try next item
  5. Skip allowed - Can skip a slot entirely (incomplete suit better than constraint violation)

Why this is fast

  • Bucket with 20 items × bucket with 15 items × bucket with 10 items = 3,000 combinations
  • vs. checking every item against every other item = millions of combinations

Two-Phase Search (from MagSuitBuilder)

  1. Phase 1 - ArmorSearcher: Find optimal armor combinations (enforces set constraints)
  2. Phase 2 - AccessorySearcher: Add jewelry to complete spell coverage

Key Data Structures

  • Spell Bitmap: 32-bit integer where each bit = one spell. Overlap detection is instant via bitwise OR
  • Coverage Mask: Tracks which body areas are covered to prevent conflicts
  • Set Counter: Tracks pieces per armor set (max 5 primary, 4 secondary)

Section 3: Current Implementation State

Updated after Task 1 Audit (2026-01-30)

What's Working

Feature Location Status
Bucket creation (all 17 slots) Lines 1006-1135 Working
Bucket sorting (armor first, smallest first) Lines 1127-1130 Working
ItemPreFilter (surpassing logic) Lines 503-583, used at 969 Working
Item sorting (by spell count/ratings) Lines 971-991 Working
Spell bitmap system SpellBitmapIndex class Working
Set constraints (5+4 logic) can_add_item() Working
Coverage mask with reductions Lines 1137-1240 Working
API Endpoints /search, /characters, /sets Working
Streaming Results (SSE) recursive_search() Working
Frontend suitbuilder.html, suitbuilder.js Working

What's Broken

Bug Location Issue
Early termination Lines 1326-1329 "TEMPORARY FIX" stops after finding 1 suit
Armor level not scored _calculate_score() Not included as tiebreaker

Current Scoring Formula

score = set_completion_bonus (1000 per complete set)
      + set_missing_penalty (-200 per missing piece)
      + crit_damage (10-20 per item)
      + damage_rating on clothes (10-30)
      + spell_coverage (100 per fulfilled spell)
      + base_item_score (5 per item)

MISSING: + armor_level as lowest-priority tiebreaker

Code Structure

  • suitbuilder.py: 1,847 lines
  • Main solver class (ConstraintSatisfactionSolver): ~1,130 lines

Note: magsuitalgo.md Analysis Was Outdated

The analysis document incorrectly stated:

  • "Only creates 2 buckets" → Actually creates all 17
  • "No item pre-filtering" → ItemPreFilter exists and is used
  • "No item sorting" → Items are sorted by spell count/ratings

The code is in much better shape than documented. Only 2 bugs need fixing.


Section 4: Implementation Plan

Updated after audit - most tasks already done!


Task 1: Audit & Document Current Search Flow COMPLETE

Findings:

  • Bucket creation: All 17 slots created correctly
  • Pre-filtering: ItemPreFilter.remove_surpassed_items() used at line 969
  • Sorting: Items sorted by spell count (armor/jewelry) and damage rating (clothes)
  • Set constraints: 5+4 logic in can_add_item()

Bugs Found:

  1. Early termination at lines 1326-1329 ("TEMPORARY FIX")
  2. Armor level missing from scoring

Task 2: Fix Bucket Creation ALREADY WORKING

No changes needed - all 17 slots are created at lines 1006-1135.


Task 3: Fix Search Completion COMPLETE

Goal: Remove early termination so search finds multiple suits

The Bug (was at lines 1326-1329):

# TEMPORARY FIX: Stop search after finding first suit to test completion
if len(self.best_suits) >= 1:
    logger.info(f"[DEBUG] EARLY TERMINATION...")
    return

Fix Applied: Removed this block entirely.


Task 4: Add Item Sorting ALREADY WORKING

No changes needed - items sorted at lines 971-991.


Task 5: Add Armor Level to Scoring COMPLETE

Goal: Add armor_level as lowest-priority tiebreaker

Location: _calculate_score() method

Fix Applied: Added armor level scoring after base item score:

# 6. Armor level as tiebreaker (LOWEST PRIORITY)
# Scale down significantly so it only matters when other scores are equal
armor_score = state.total_armor // 100  # ~5 points per 500 AL
score += armor_score

Task 6: Add Item Pre-Filtering ALREADY WORKING

No changes needed - ItemPreFilter used at line 969.


Task 7: Add AccessorySearcher Phase (Future)

Goal: After armor search, optimize jewelry slots

Scope: Separate task, do after core search is verified working


How to Use This Document

For Claude (AI assistant):

  • Read this document at the start of each session
  • Work on ONE task at a time
  • Verify before moving to next task
  • If confused, re-read the relevant section
  • Do NOT make changes outside the current task scope

For the developer:

  • Update "Status" at top when tasks complete
  • Add notes under each task as we learn things
  • Reference magsuitalgo.md for detailed algorithm explanations

Progress Tracking

Task Status Notes
Task 1: Audit Current Flow Complete Found 2 bugs, most features working
Task 2: Fix Bucket Creation Already Working No changes needed
Task 3: Fix Search Completion Complete Removed early termination at lines 1326-1329
Task 4: Add Item Sorting Already Working No changes needed
Task 5: Add Armor Level Scoring Complete Added armor_score = total_armor // 100
Task 6: Add Item Pre-Filtering Already Working No changes needed
Task 7: AccessorySearcher Not Started Future
Task 8: Fix SSE Streaming Proxy Complete Added dedicated streaming endpoint in main.py

Bug Fixes Applied

SSE Streaming Proxy Fix (2026-01-30)

Problem: The generic inventory proxy at /inv/{path:path} used httpx.request() which buffers the entire response before returning. For SSE streams like suitbuilder search, this caused:

  • No progress updates reaching the frontend
  • "Gateway Time-out" after buffering the full 5-minute search

Solution: Added dedicated streaming proxy endpoint /inv/suitbuilder/search in main.py:

@app.post("/inv/suitbuilder/search")
async def proxy_suitbuilder_search(request: Request):
    async def stream_response():
        async with httpx.AsyncClient.stream(...) as response:
            async for chunk in response.aiter_bytes():
                yield chunk
    return StreamingResponse(
        stream_response(),
        media_type="text/event-stream",
        headers={"X-Accel-Buffering": "no"}  # Disable nginx buffering
    )

Result: Suits now stream to frontend in real-time with scores visible as they're found.