feat(suitbuilder): CD-tier filter helpers + tests; gate inventory-go build on go test

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 20:35:20 +02:00
parent 593e99894f
commit 7155055072
3 changed files with 157 additions and 0 deletions

View file

@ -2,6 +2,7 @@ FROM golang:1.25-bookworm AS build
WORKDIR /src
COPY . .
RUN go mod tidy
RUN go test ./...
ARG BUILD_VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath -ldflags "-s -w -X main.buildVersion=${BUILD_VERSION}" -o /out/inventory-go .

View file

@ -0,0 +1,74 @@
package main
import "strings"
// CD-tier filtering for the suitbuilder. The allowed_crit_damage constraint
// restricts which crit-damage tiers are permitted on ARMOR pieces; jewelry and
// clothing are never affected. "Prefer the highest allowed tier" is NOT done
// here — it falls out of the existing scoring (CritDamage2 > CritDamage1) and
// the CD-descending armor sort once disallowed tiers are removed.
// critTier normalizes a raw crit_damage_rating into a tier in {0,1,2}. Rare
// high-crit gear (rating >= 2, including 3+) collapses to tier 2 so it counts
// as "CD2" rather than being silently excluded.
func critTier(rating int) int {
switch {
case rating <= 0:
return 0
case rating == 1:
return 1
default:
return 2
}
}
// isArmorSlot reports whether a slot name denotes an armor coverage slot,
// including comma-joined multi-coverage slots like "Chest, Abdomen".
func isArmorSlot(slot string) bool {
if armorSlotSet[slot] {
return true
}
if strings.Contains(slot, ", ") {
for _, p := range strings.Split(slot, ", ") {
if armorSlotSet[strings.TrimSpace(p)] {
return true
}
}
}
return false
}
// allowedCritSet normalizes the constraint's allowed crit-damage tiers into a
// set, or returns nil when the filter is INACTIVE: no values, or all three
// tiers {0,1,2} present (== default). A nil result means "no filter" and keeps
// the default search path byte-identical to the unfiltered solver.
func allowedCritSet(vals []int) map[int]bool {
if len(vals) == 0 {
return nil
}
set := map[int]bool{}
for _, v := range vals {
set[critTier(v)] = true
}
if set[0] && set[1] && set[2] {
return nil
}
return set
}
// filterArmorByCD drops armor items whose crit-damage tier is not in allowed.
// Non-armor items (jewelry, clothing, unknown) always pass through. When
// allowed is nil the input is returned unchanged.
func filterArmorByCD(items []*SuitItem, allowed map[int]bool) []*SuitItem {
if allowed == nil {
return items
}
out := make([]*SuitItem, 0, len(items))
for _, it := range items {
if isArmorSlot(it.Slot) && !allowed[critTier(it.Ratings["crit_damage_rating"])] {
continue
}
out = append(out, it)
}
return out
}

View file

@ -0,0 +1,82 @@
package main
import "testing"
func TestCritTier(t *testing.T) {
cases := []struct {
rating, want int
}{{-1, 0}, {0, 0}, {1, 1}, {2, 2}, {3, 2}, {5, 2}}
for _, c := range cases {
if got := critTier(c.rating); got != c.want {
t.Errorf("critTier(%d) = %d, want %d", c.rating, got, c.want)
}
}
}
func TestAllowedCritSet(t *testing.T) {
for _, vals := range [][]int{nil, {}, {0, 1, 2}, {0, 1, 3}} {
if allowedCritSet(vals) != nil {
t.Errorf("allowedCritSet(%v) should be nil (inactive)", vals)
}
}
if s := allowedCritSet([]int{1}); s == nil || !s[1] || s[0] || s[2] {
t.Errorf("allowedCritSet({1}) = %v, want only tier 1", s)
}
if s := allowedCritSet([]int{0, 1}); s == nil || !s[0] || !s[1] || s[2] {
t.Errorf("allowedCritSet({0,1}) = %v, want tiers 0,1", s)
}
if s := allowedCritSet([]int{3}); s == nil || !s[2] || s[0] || s[1] {
t.Errorf("allowedCritSet({3}) = %v, want only tier 2 (normalized)", s)
}
}
func TestIsArmorSlot(t *testing.T) {
for _, s := range []string{"Chest", "Head", "Feet", "Chest, Abdomen", "Upper Legs, Lower Legs"} {
if !isArmorSlot(s) {
t.Errorf("isArmorSlot(%q) = false, want true", s)
}
}
for _, s := range []string{"Neck", "Left Ring", "Left Wrist", "Trinket", "Shirt", "Pants", "Unknown", ""} {
if isArmorSlot(s) {
t.Errorf("isArmorSlot(%q) = true, want false", s)
}
}
}
func cdItem(slot string, cd int) *SuitItem {
return &SuitItem{Slot: slot, Ratings: map[string]int{"crit_damage_rating": cd}}
}
func TestFilterArmorByCD(t *testing.T) {
items := []*SuitItem{
cdItem("Chest", 0), cdItem("Head", 1), cdItem("Feet", 2),
cdItem("Chest, Abdomen", 2), // multi-coverage armor, CD2
cdItem("Neck", 0), // jewelry — never filtered
cdItem("Shirt", 0), // clothing — never filtered
}
if got := filterArmorByCD(items, nil); len(got) != len(items) {
t.Errorf("nil filter dropped items: got %d, want %d", len(got), len(items))
}
got := filterArmorByCD(items, map[int]bool{1: true})
keep := map[string]bool{"Head": true, "Neck": true, "Shirt": true}
if len(got) != 3 {
t.Fatalf("allowed{1}: got %d items, want 3", len(got))
}
for _, it := range got {
if !keep[it.Slot] {
t.Errorf("allowed{1}: unexpected slot %q survived", it.Slot)
}
}
got = filterArmorByCD(items, map[int]bool{0: true, 1: true})
if len(got) != 4 { // Chest(0), Head(1), Neck, Shirt
t.Errorf("allowed{0,1}: got %d items, want 4", len(got))
}
for _, it := range got {
if isArmorSlot(it.Slot) && it.Ratings["crit_damage_rating"] >= 2 {
t.Errorf("allowed{0,1}: CD2 armor %q should have been dropped", it.Slot)
}
}
}