package main import ( "context" "net/url" "sort" "strconv" "strings" "time" ) // Solver is the Go port of suitbuilder.py ConstraintSatisfactionSolver (the live // /suitbuilder/search DFS). It streams events via emit; cancellation is checked // through cancelled (the request context). type Solver struct { s *Server c SearchConstraints spellIndex *SpellBitmapIndex bestSuits []*CompletedSuit evaluated int weights ScoringWeights neededSpellBitmap uint64 bestSuitItemCount int highestArmorCount int armorBucketsItems int allowedCD map[int]bool // nil == no CD filter (default / all tiers) lockedSetCounts map[int]int lockedSpells map[string]bool effPrimary int effSecondary int start time.Time emit func(event string, data map[string]any) cancelled func() bool stopped bool } func newSolver(s *Server, c SearchConstraints, emit func(string, map[string]any), cancelled func() bool) *Solver { w := defaultScoringWeights() if c.ScoringWeights != nil { w = *c.ScoringWeights } if c.MaxResults == 0 { c.MaxResults = 50 } sv := &Solver{ s: s, c: c, spellIndex: newSpellBitmapIndex(), weights: w, lockedSetCounts: map[int]int{}, lockedSpells: map[string]bool{}, effPrimary: 5, effSecondary: 4, start: time.Now(), emit: emit, cancelled: cancelled, } // Required spells register first, so they always get the low bits. sv.neededSpellBitmap = sv.spellIndex.getBitmap(c.RequiredSpells) sv.allowedCD = allowedCritSet(c.AllowedCritDamage) return sv } var armorSlotSet = map[string]bool{ "Head": true, "Chest": true, "Upper Arms": true, "Lower Arms": true, "Hands": true, "Abdomen": true, "Upper Legs": true, "Lower Legs": true, "Feet": true, } var jewelrySlotSet = map[string]bool{ "Neck": true, "Left Ring": true, "Right Ring": true, "Left Wrist": true, "Right Wrist": true, "Trinket": true, } func (sv *Solver) elapsed() float64 { return time.Since(sv.start).Seconds() } // Search drives the 5-phase pipeline, emitting events as it goes. func (sv *Solver) Search(ctx context.Context) { sv.emit("phase", map[string]any{"phase": "loading", "message": "Loading items from database...", "phase_number": 1, "total_phases": 5}) items, err := sv.loadItems(ctx) if err != nil { sv.emit("error", map[string]any{"message": err.Error()}) return } sv.emit("phase", map[string]any{"phase": "loaded", "message": "Loaded items", "items_count": len(items), "phase_number": 1, "total_phases": 5}) sv.emit("log", map[string]any{"level": "info", "message": "Loaded items from characters", "timestamp": sv.elapsed()}) if len(items) == 0 { sv.emit("error", map[string]any{"message": "No items found for specified characters"}) return } sv.emit("phase", map[string]any{"phase": "buckets", "message": "Creating equipment buckets...", "phase_number": 2, "total_phases": 5}) buckets := sv.createBuckets(items) summary := map[string]any{} for _, b := range buckets { summary[b.Slot] = len(b.Items) } sv.emit("phase", map[string]any{"phase": "buckets_done", "message": "Created buckets", "bucket_count": len(buckets), "bucket_summary": summary, "phase_number": 2, "total_phases": 5}) sv.emit("phase", map[string]any{"phase": "reducing", "message": "Applying armor reduction rules...", "phase_number": 3, "total_phases": 5}) buckets = sv.applyReductionOptions(buckets) sv.emit("phase", map[string]any{"phase": "sorting", "message": "Optimizing search order...", "phase_number": 4, "total_phases": 5}) buckets = sv.sortBuckets(buckets) // Locked slots: drop those buckets, accumulate locked set/spell contributions. if len(sv.c.LockedSlots) > 0 { locked := map[string]bool{} for slot := range sv.c.LockedSlots { locked[slot] = true } kept := buckets[:0] for _, b := range buckets { if !locked[b.Slot] { kept = append(kept, b) } } buckets = kept for _, info := range sv.c.LockedSlots { if info.SetID != 0 { sv.lockedSetCounts[info.SetID]++ } for _, sp := range info.Spells { sv.lockedSpells[sp] = true } } } sv.effPrimary, sv.effSecondary = 5, 4 if sv.c.PrimarySet != 0 { sv.effPrimary = max0(5 - sv.lockedSetCounts[sv.c.PrimarySet]) } if sv.c.SecondarySet != 0 { sv.effSecondary = max0(4 - sv.lockedSetCounts[sv.c.SecondarySet]) } sv.emit("phase", map[string]any{"phase": "searching", "message": "Searching for optimal suits...", "total_buckets": len(buckets), "phase_number": 5, "total_phases": 5}) sv.emit("log", map[string]any{"level": "info", "message": "Starting search", "timestamp": sv.elapsed()}) sv.recursiveSearch(buckets, 0, newSuitState()) sv.emit("complete", map[string]any{"suits_found": len(sv.bestSuits), "duration": round1(sv.elapsed())}) } // loadItems mirrors suitbuilder.load_items: fetch via the in-process search with // the exact same filter param sets, convert to SuitItem, register spell bitmaps, // pre-filter, and sort into armor+jewelry+clothing order. func (sv *Solver) loadItems(ctx context.Context) ([]*SuitItem, error) { s := sv.s primaryName, secondaryName := "", "" if sv.c.PrimarySet != 0 { primaryName = s.translateSetID(strconv.Itoa(sv.c.PrimarySet)) } if sv.c.SecondarySet != 0 { secondaryName = s.translateSetID(strconv.Itoa(sv.c.SecondarySet)) } equipmentStatus := "" if sv.c.IncludeEquipped && sv.c.IncludeInventory { equipmentStatus = "" } else if sv.c.IncludeEquipped { equipmentStatus = "equipped" } else if sv.c.IncludeInventory { equipmentStatus = "unequipped" } var apiItems []map[string]any fetch := func(extra map[string]string) error { q := url.Values{} if len(sv.c.Characters) > 0 { q.Set("characters", strings.Join(sv.c.Characters, ",")) } else { q.Set("include_all_characters", "true") } if equipmentStatus != "" { q.Set("equipment_status", equipmentStatus) } q.Set("limit", "1000") for k, v := range extra { q.Set(k, v) } res, err := s.runSearch(ctx, q) if err != nil { return err } if items, ok := res["items"].([]map[string]any); ok { apiItems = append(apiItems, items...) } return nil } if primaryName != "" { if err := fetch(map[string]string{"item_set": primaryName}); err != nil { return nil, err } } if secondaryName != "" { if err := fetch(map[string]string{"item_set": secondaryName}); err != nil { return nil, err } } // Clothing: DR3 shirts/pants only. _ = fetch(map[string]string{"shirt_only": "true", "min_damage_rating": "3"}) _ = fetch(map[string]string{"pants_only": "true", "min_damage_rating": "3"}) // Jewelry: one fetch per type via slot_names. for _, slot := range []string{"Ring", "Bracelet", "Neck", "Trinket"} { _ = fetch(map[string]string{"jewelry_only": "true", "slot_names": slot}) } items := make([]*SuitItem, 0, len(apiItems)) for _, api := range apiItems { name := toStr(api["name"]) char := toStr(api["character_name"]) coverageVal := int(toInt64(api["coverage_mask"])) slot := toStr(api["computed_slot_name"]) if slot == "" { slot = toStr(api["slot_name"]) } if slot == "" { slot = "Unknown" } if int(toInt64(api["object_class"])) == 3 { switch coverageVal { case 104: slot = "Shirt" case 19, 22: slot = "Pants" } } rg := func(k string) int { v := api[k] if v == nil { return 0 } return int(toInt64(v)) } var spellNames []string if sn, ok := api["spell_names"].([]string); ok { spellNames = sn } it := &SuitItem{ ID: char + "_" + name, Name: name, CharacterName: char, Slot: slot, Coverage: coverageVal, HasCoverage: coverageVal != 0, SetID: convertSetNameToID(toStr(api["item_set"])), ArmorLevel: int(toInt64(api["armor_level"])), Ratings: map[string]int{ "crit_damage_rating": rg("crit_damage_rating"), "damage_rating": rg("damage_rating"), "damage_resist_rating": rg("damage_resist_rating"), "crit_damage_resist_rating": rg("crit_damage_resist_rating"), "heal_boost_rating": rg("heal_boost_rating"), "vitality_rating": rg("vitality_rating"), }, SpellNames: spellNames, Material: toStr(api["material_name"]), } items = append(items, it) } for _, it := range items { if len(it.SpellNames) > 0 { it.SpellBitmap = sv.spellIndex.getBitmap(it.SpellNames) } } // Drop armor whose CD tier is disallowed BEFORE domination, so a CD2 piece // can't surpass-and-remove an allowed CD1 piece we'd then exclude. items = filterArmorByCD(items, sv.allowedCD) filtered := removeSurpassedItems(items) jewelryFallback := map[string]bool{"Ring": true, "Bracelet": true, "Jewelry": true, "Necklace": true, "Amulet": true} matches := func(slot string, set, fallback map[string]bool) bool { if set[slot] { return true } if strings.Contains(slot, ", ") { for _, p := range strings.Split(slot, ", ") { if set[strings.TrimSpace(p)] { return true } } } if fallback != nil && fallback[slot] { return true } return false } var armor, jewelry, clothing []*SuitItem for _, it := range filtered { if matches(it.Slot, armorSlotSet, nil) { armor = append(armor, it) } if matches(it.Slot, jewelrySlotSet, jewelryFallback) { jewelry = append(jewelry, it) } if matches(it.Slot, clothingSortSlots, nil) { clothing = append(clothing, it) } } sortBySpellThenName := func(list []*SuitItem) { sort.SliceStable(list, func(i, j int) bool { return descTuple( cmpInt(len(list[i].SpellNames), len(list[j].SpellNames)), cmpStr(list[i].CharacterName, list[j].CharacterName), cmpStr(list[i].Name, list[j].Name), ) }) } sortBySpellThenName(armor) sortBySpellThenName(jewelry) sort.SliceStable(clothing, func(i, j int) bool { return descTuple( cmpInt(clothing[i].Ratings["damage_rating"], clothing[j].Ratings["damage_rating"]), cmpStr(clothing[i].CharacterName, clothing[j].CharacterName), cmpStr(clothing[i].Name, clothing[j].Name), ) }) out := make([]*SuitItem, 0, len(armor)+len(jewelry)+len(clothing)) out = append(out, armor...) out = append(out, jewelry...) out = append(out, clothing...) return out, nil } var allSlots = []string{ "Head", "Chest", "Upper Arms", "Lower Arms", "Hands", "Abdomen", "Upper Legs", "Lower Legs", "Feet", "Neck", "Left Ring", "Right Ring", "Left Wrist", "Right Wrist", "Trinket", "Shirt", "Pants", } func (sv *Solver) createBuckets(items []*SuitItem) []*ItemBucket { slotItems := map[string][]*SuitItem{} inSlots := map[string]bool{} for _, slot := range allSlots { slotItems[slot] = nil inSlots[slot] = true } genericJewelry := map[string][]string{ "Ring": {"Left Ring", "Right Ring"}, "Bracelet": {"Left Wrist", "Right Wrist"}, "Jewelry": {"Neck", "Left Ring", "Right Ring", "Left Wrist", "Right Wrist", "Trinket"}, "Necklace": {"Neck"}, "Amulet": {"Neck"}, } for _, it := range items { if inSlots[it.Slot] { slotItems[it.Slot] = append(slotItems[it.Slot], it) } else if strings.Contains(it.Slot, ", ") { for _, p := range strings.Split(it.Slot, ", ") { p = strings.TrimSpace(p) if inSlots[p] { slotItems[p] = append(slotItems[p], it.clone(p, it.Name, it.Coverage, it.HasCoverage)) } } } else if targets, ok := genericJewelry[it.Slot]; ok { for _, t := range targets { slotItems[t] = append(slotItems[t], it.clone(t, it.Name, it.Coverage, it.HasCoverage)) } } else { lower := strings.ToLower(it.Slot) for _, known := range allSlots { if strings.Contains(lower, strings.ToLower(known)) { slotItems[known] = append(slotItems[known], it.clone(known, it.Name, it.Coverage, it.HasCoverage)) } } } } buckets := make([]*ItemBucket, 0, len(allSlots)) for _, slot := range allSlots { b := &ItemBucket{Slot: slot, Items: slotItems[slot], IsArmor: armorSlotSet[slot]} b.sortItems() buckets = append(buckets, b) } // armor first, then item count ascending (overridden by sortBuckets, but the // stable item order set here feeds the later stable re-sorts). sort.SliceStable(buckets, func(i, j int) bool { ai, aj := boolToInt(!buckets[i].IsArmor), boolToInt(!buckets[j].IsArmor) if ai != aj { return ai < aj } return len(buckets[i].Items) < len(buckets[j].Items) }) sv.armorBucketsItems = 0 for _, b := range buckets { if b.IsArmor && len(b.Items) > 0 { sv.armorBucketsItems++ } } return buckets } func (sv *Solver) applyReductionOptions(buckets []*ItemBucket) []*ItemBucket { var newBuckets []*ItemBucket findBucket := func(slot string) *ItemBucket { for _, b := range newBuckets { if b.Slot == slot { return b } } return nil } for _, bucket := range buckets { if !bucket.IsArmor { newBuckets = append(newBuckets, bucket) continue } var original, reducible []*SuitItem for _, it := range bucket.Items { if it.HasCoverage && it.Material != "" && len(coverageReductionOptions(it.Coverage)) > 0 { reducible = append(reducible, it) } else { original = append(original, it) } } if len(original) > 0 || len(reducible) == 0 { nb := &ItemBucket{Slot: bucket.Slot, Items: original, IsArmor: bucket.IsArmor} nb.sortItems() newBuckets = append(newBuckets, nb) } for _, it := range reducible { for _, rc := range coverageReductionOptions(it.Coverage) { reducedSlot := coverageToSlotName(rc) if reducedSlot == "" { continue } reduced := it.clone(reducedSlot, it.Name+" (tailored to "+reducedSlot+")", rc, true) target := findBucket(reducedSlot) if target == nil { target = &ItemBucket{Slot: reducedSlot, IsArmor: true} newBuckets = append(newBuckets, target) } target.Items = append(target.Items, reduced) } } } for _, b := range newBuckets { b.sortItems() } return newBuckets } var coreArmorPriority = []string{"Chest", "Head", "Hands", "Feet", "Upper Arms", "Lower Arms", "Abdomen", "Upper Legs", "Lower Legs"} var jewelryPriority = []string{"Neck", "Left Ring", "Right Ring", "Left Wrist", "Right Wrist", "Trinket"} var clothingPriority = []string{"Shirt", "Pants"} func (sv *Solver) sortBuckets(buckets []*ItemBucket) []*ItemBucket { for _, bucket := range buckets { items := bucket.Items sort.SliceStable(items, func(i, j int) bool { pi, pj := sv.setPriority(items[i].SetID), sv.setPriority(items[j].SetID) if pi != pj { return pi < pj } if c := cmpInt(items[i].Ratings["crit_damage_rating"], items[j].Ratings["crit_damage_rating"]); c != 0 { return c > 0 } if c := cmpInt(items[i].Ratings["damage_rating"], items[j].Ratings["damage_rating"]); c != 0 { return c > 0 } return items[i].ArmorLevel > items[j].ArmorLevel }) } sort.SliceStable(buckets, func(i, j int) bool { gi, ii := bucketPriority(buckets[i].Slot) gj, ij := bucketPriority(buckets[j].Slot) if gi != gj { return gi < gj } if ii != ij { return ii < ij } return len(buckets[i].Items) < len(buckets[j].Items) }) return buckets } func (sv *Solver) setPriority(setID int) int { if setID != 0 && setID == sv.c.PrimarySet { return 0 } if setID != 0 && setID == sv.c.SecondarySet { return 1 } return 2 } func bucketPriority(slot string) (int, int) { for i, s := range coreArmorPriority { if s == slot { return 0, i } } for i, s := range jewelryPriority { if s == slot { return 1, i } } for i, s := range clothingPriority { if s == slot { return 2, i } } return 3, 0 } func (sv *Solver) recursiveSearch(buckets []*ItemBucket, idx int, state *SuitState) { if sv.stopped { return } if sv.cancelled != nil && sv.cancelled() { sv.stopped = true return } if sv.highestArmorCount > 0 { currentCount := len(state.Items) remaining := sv.armorBucketsItems - minInt(idx, sv.armorBucketsItems) minRequired := sv.highestArmorCount - remaining if currentCount+1 < minRequired { return } } remainingBuckets := len(buckets) - idx maxPossible := len(state.Items) + remainingBuckets if sv.bestSuitItemCount > 0 && maxPossible < sv.bestSuitItemCount { return } if idx >= len(buckets) { suit := sv.finalizeSuit(state) if suit != nil && sv.isBetterThanExisting(suit) { sv.bestSuits = append(sv.bestSuits, suit) if len(suit.Items) > sv.bestSuitItemCount { sv.bestSuitItemCount = len(suit.Items) } armorPieces := 0 for slot := range suit.Items { if armorSlotSet[slot] { armorPieces++ } } if armorPieces > sv.highestArmorCount { sv.highestArmorCount = armorPieces } sort.SliceStable(sv.bestSuits, func(i, j int) bool { return sv.bestSuits[i].Score > sv.bestSuits[j].Score }) if len(sv.bestSuits) > sv.c.MaxResults { sv.bestSuits = sv.bestSuits[:sv.c.MaxResults] } sv.emit("suit", sv.suitData(suit)) sv.emit("log", map[string]any{"level": "success", "message": "Found suit", "timestamp": sv.elapsed()}) } return } sv.evaluated++ if sv.evaluated%100 == 0 { if sv.cancelled != nil && sv.cancelled() { sv.stopped = true return } bestScore := 0 if len(sv.bestSuits) > 0 { bestScore = sv.bestSuits[0].Score } var curBucket any if idx < len(buckets) { curBucket = buckets[idx].Slot } el := sv.elapsed() rate := 0.0 if el > 0 { rate = round1(float64(sv.evaluated) / el) } sv.emit("progress", map[string]any{ "evaluated": sv.evaluated, "found": len(sv.bestSuits), "current_depth": idx, "total_buckets": len(buckets), "current_items": len(state.Items), "elapsed": el, "rate": rate, "current_bucket": curBucket, "best_score": bestScore, }) if sv.evaluated%500 == 0 { sv.emit("log", map[string]any{"level": "info", "message": "Evaluating combinations", "timestamp": el}) } } bucket := buckets[idx] accepted := 0 for _, it := range bucket.Items { if sv.canAddItem(it, state) { accepted++ state.push(it) sv.recursiveSearch(buckets, idx+1, state) state.pop(it.Slot) if sv.stopped { return } } } if accepted == 0 { sv.recursiveSearch(buckets, idx+1, state) } } func (sv *Solver) canAddItem(it *SuitItem, state *SuitState) bool { if state.Occupied[it.Slot] { return false } for _, ex := range state.Items { if ex.ID == it.ID { return false } } if it.SetID != 0 { current := state.SetCounts[it.SetID] if it.SetID == sv.c.PrimarySet { if current >= sv.effPrimary { return false } } else if it.SetID == sv.c.SecondarySet { if current >= sv.effSecondary { return false } } else { if jewelrySlotSet[it.Slot] { if !sv.jewelryContributesRequiredSpell(it, state) { return false } } else { return false } } } else { if it.Slot == "Shirt" || it.Slot == "Pants" { // clothing allowed without set id } else if jewelrySlotSet[it.Slot] { if !sv.jewelryContributesRequiredSpell(it, state) { return false } } else { return false } } if len(sv.c.RequiredSpells) > 0 && len(it.SpellNames) > 0 { if !sv.canGetBeneficialSpellFrom(it, state) { return false } } return true } func (sv *Solver) canGetBeneficialSpellFrom(it *SuitItem, state *SuitState) bool { if len(it.SpellNames) == 0 { return true } if len(sv.c.RequiredSpells) == 0 { return true } newBeneficial := it.SpellBitmap & sv.neededSpellBitmap & ^state.SpellBitmap return newBeneficial != 0 } func (sv *Solver) jewelryContributesRequiredSpell(it *SuitItem, state *SuitState) bool { if len(sv.c.RequiredSpells) == 0 { return false } if len(it.SpellNames) == 0 { return false } for _, sp := range it.SpellNames { bit := sv.spellIndex.getBitmap([]string{sp}) if bit&sv.neededSpellBitmap != 0 && state.SpellBitmap&bit == 0 { return true } } return false } func (sv *Solver) finalizeSuit(state *SuitState) *CompletedSuit { if len(state.Items) == 0 { return nil } score := sv.calculateScore(state) var fulfilled, missing []string if len(sv.c.RequiredSpells) > 0 { fulfilled = sv.spellIndex.getSpellNames(state.SpellBitmap & sv.neededSpellBitmap) missing = sv.spellIndex.getSpellNames(sv.neededSpellBitmap & ^state.SpellBitmap) if len(sv.lockedSpells) > 0 { for sp := range sv.lockedSpells { missing = removeString(missing, sp) if !containsString(fulfilled, sp) { fulfilled = append(fulfilled, sp) } } } } items := make(map[string]*SuitItem, len(state.Items)) for k, v := range state.Items { items[k] = v } ratings := map[string]int{} for k, v := range state.TotalRatings { ratings[k] = v } setCounts := map[int]int{} for k, v := range state.SetCounts { setCounts[k] = v } return &CompletedSuit{ Items: items, Score: score, TotalArmor: state.TotalArmor, TotalRatings: ratings, SetCounts: setCounts, FulfilledSpells: fulfilled, MissingSpells: missing, } } func (sv *Solver) calculateScore(state *SuitState) int { score := 0 w := sv.weights foundPrimary, foundSecondary := 0, 0 if sv.c.PrimarySet != 0 { foundPrimary = state.SetCounts[sv.c.PrimarySet] } if sv.c.SecondarySet != 0 { foundSecondary = state.SetCounts[sv.c.SecondarySet] } if foundPrimary >= sv.effPrimary { score += w.ArmorSetComplete if foundPrimary > sv.effPrimary { score -= (foundPrimary - sv.effPrimary) * 500 } } else if sv.c.PrimarySet != 0 && foundPrimary > 0 { score += (sv.effPrimary - foundPrimary) * w.MissingSetPenalty } if foundSecondary >= sv.effSecondary { score += w.ArmorSetComplete if foundSecondary > sv.effSecondary { score -= (foundSecondary - sv.effSecondary) * 500 } } else if sv.c.SecondarySet != 0 && foundSecondary > 0 { score += (sv.effSecondary - foundSecondary) * w.MissingSetPenalty } for _, it := range state.Items { switch it.Ratings["crit_damage_rating"] { case 1: score += w.CritDamage1 case 2: score += w.CritDamage2 } } for _, it := range state.Items { if it.Slot == "Shirt" || it.Slot == "Pants" { switch it.Ratings["damage_rating"] { case 1: score += w.DamageRating1 case 2: score += w.DamageRating2 case 3: score += w.DamageRating3 } } } if len(sv.c.RequiredSpells) > 0 { score += popcount(state.SpellBitmap&sv.neededSpellBitmap) * 100 } score += len(state.Items) * 5 // Python uses floor division (//); total_armor can be negative because // non-armor items carry armor_level = -1. Go's / truncates toward zero, so a // slightly-negative total would be +1 too high. score += floorDiv(state.TotalArmor, 100) if score < 0 { return 0 } return score } func (sv *Solver) isBetterThanExisting(suit *CompletedSuit) bool { if len(sv.bestSuits) < sv.c.MaxResults { return true } lowest := sv.bestSuits[len(sv.bestSuits)-1] if len(suit.Items) > len(lowest.Items) { return true } return suit.Score > lowest.Score } // suitData builds the streamed suit payload (CompletedSuit.to_dict plus the // constraint-derived stats overrides from recursive_search). func (sv *Solver) suitData(suit *CompletedSuit) map[string]any { d := suit.toDict() stats := d["stats"].(map[string]any) primaryCount, secondaryCount := 0, 0 if sv.c.PrimarySet != 0 { primaryCount = suit.SetCounts[sv.c.PrimarySet] + sv.lockedSetCounts[sv.c.PrimarySet] } if sv.c.SecondarySet != 0 { secondaryCount = suit.SetCounts[sv.c.SecondarySet] + sv.lockedSetCounts[sv.c.SecondarySet] } var primaryName, secondaryName any if sv.c.PrimarySet != 0 { primaryName = sv.s.translateSetID(strconv.Itoa(sv.c.PrimarySet)) } if sv.c.SecondarySet != 0 { secondaryName = sv.s.translateSetID(strconv.Itoa(sv.c.SecondarySet)) } stats["primary_set_count"] = primaryCount stats["secondary_set_count"] = secondaryCount stats["primary_set"] = primaryName stats["secondary_set"] = secondaryName stats["locked_slots"] = len(sv.c.LockedSlots) stats["primary_locked"] = sv.lockedSetCounts[sv.c.PrimarySet] stats["secondary_locked"] = sv.lockedSetCounts[sv.c.SecondarySet] return d } // --- small helpers --- func max0(v int) int { if v < 0 { return 0 } return v } func minInt(a, b int) int { if a < b { return a } return b } // floorDiv matches Python's // (floor toward -inf), unlike Go's / (toward zero). func floorDiv(a, b int) int { q := a / b if a%b != 0 && (a < 0) != (b < 0) { q-- } return q } func boolToInt(b bool) int { if b { return 1 } return 0 } func popcount(v uint64) int { c := 0 for v != 0 { v &= v - 1 c++ } return c } func round1(v float64) float64 { return float64(int64(v*10+0.5)) / 10 } func containsString(list []string, s string) bool { for _, x := range list { if x == s { return true } } return false } func removeString(list []string, s string) []string { for i, x := range list { if x == s { return append(list[:i], list[i+1:]...) } } return list }