From 7155055072686cc8e8a6a05ddf2d64fdbd30b088 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 25 Jun 2026 20:35:20 +0200 Subject: [PATCH] feat(suitbuilder): CD-tier filter helpers + tests; gate inventory-go build on go test Co-Authored-By: Claude Opus 4.8 --- go-services/inventory-go/Dockerfile | 1 + go-services/inventory-go/suit_cd.go | 74 +++++++++++++++++++++ go-services/inventory-go/suit_cd_test.go | 82 ++++++++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 go-services/inventory-go/suit_cd.go create mode 100644 go-services/inventory-go/suit_cd_test.go diff --git a/go-services/inventory-go/Dockerfile b/go-services/inventory-go/Dockerfile index 67c15ee3..3a9d8097 100644 --- a/go-services/inventory-go/Dockerfile +++ b/go-services/inventory-go/Dockerfile @@ -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 . diff --git a/go-services/inventory-go/suit_cd.go b/go-services/inventory-go/suit_cd.go new file mode 100644 index 00000000..d0d4f843 --- /dev/null +++ b/go-services/inventory-go/suit_cd.go @@ -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 +} diff --git a/go-services/inventory-go/suit_cd_test.go b/go-services/inventory-go/suit_cd_test.go new file mode 100644 index 00000000..a366b218 --- /dev/null +++ b/go-services/inventory-go/suit_cd_test.go @@ -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) + } + } +}