From 9666676766bf21364f4fc4d0d1d336e72f2a91b4 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 6 Oct 2026 12:57:23 -0400 Subject: refactor: split casino.go, dedup helpers, remove audit docs --- internal/validate/checks.go | 64 ++++++++++++++++++++++++++++++++++++++++++ internal/validate/grid_test.go | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) (limited to 'internal/validate') diff --git a/internal/validate/checks.go b/internal/validate/checks.go index c636b4f..a152956 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -816,6 +816,17 @@ func validateObjects(s Source) []Issue { return issues } +var validEquipSlots = map[item.EquipSlot]bool{ + item.SlotHead: true, item.SlotNeck: true, item.SlotTorso: true, item.SlotLegs: true, + item.SlotHands: true, item.SlotFeet: true, item.SlotBack: true, item.SlotAmmo: true, + item.SlotMainHand: true, item.SlotOffHand: true, item.SlotRing: true, +} + +var validEquipTypes = map[item.EquipmentType]bool{ + item.EquipMeleeWeapon: true, item.EquipRangedWeapon: true, item.EquipScienceWeapon: true, + item.EquipArmor: true, item.EquipTool: true, item.EquipAmmo: true, +} + func validateItems(s Source) []Issue { var issues []Issue itemIDs := s.Items.IDSet() @@ -842,6 +853,27 @@ func validateItems(s Source) []Issue { }) } + // Equipment fields are free-form YAML; a typo'd slot/type/attack_type + // equips into an invisible slot or grants bonuses from nowhere. + if def.Equipment != nil { + if def.Equipment.Type != "" && !validEquipTypes[def.Equipment.Type] { + issues = append(issues, Issue{Level: "ERROR", Type: "config", + Message: fmt.Sprintf("Item %q: unknown equipment.type %q", id, def.Equipment.Type)}) + } + if def.Equipment.Slot != "" && !validEquipSlots[def.Equipment.Slot] { + issues = append(issues, Issue{Level: "ERROR", Type: "config", + Message: fmt.Sprintf("Item %q: unknown equipment.slot %q", id, def.Equipment.Slot)}) + } + if def.Equipment.AttackType != "" && !combat.IsValidAttackType(def.Equipment.AttackType) { + issues = append(issues, Issue{Level: "ERROR", Type: "config", + Message: fmt.Sprintf("Item %q: invalid equipment.attack_type %q", id, def.Equipment.AttackType)}) + } + } + if def.HouseEdgePrecision != nil && *def.HouseEdgePrecision != 0 && *def.HouseEdgePrecision != 2 { + issues = append(issues, Issue{Level: "ERROR", Type: "config", + Message: fmt.Sprintf("Item %q: house_edge_precision must be 0 or 2, got %d", id, *def.HouseEdgePrecision)}) + } + if len(def.Search) > 0 { issues = append(issues, validateTriggerBlock( fmt.Sprintf("Item %q: search", id), def.Search, itemIDs, roomIndex, mobIDs, dropIDs)...) @@ -911,6 +943,16 @@ func validateCraftBlocks(s Source) []Issue { craftLabel = c.Type + "/" + c.Subtype } + // An empty ingredient list is unsatisfiable: planIngredients returns + // nil, so the craft can never produce output. + if len(c.Ingredients) == 0 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q (craft: %s): has no ingredients; the recipe can never be satisfied", item.ID, craftLabel), + }) + } + // The runtime selects the action by subtype (or type when no subtype // is set), defaulting to combine. An unknown key leaves the action // set but never advancing, so the player appears stuck. @@ -1106,6 +1148,13 @@ func validateDropTables(s Source) []Issue { } for i, d := range dt.Drops { + if d.Weight <= 0 { + issues = append(issues, Issue{ + Level: "WARN", + Type: "integrity", + Message: fmt.Sprintf("Drop table %q entry %d: weight %d is never selectable", id, i+1, d.Weight), + }) + } if d.ItemID != "" && !itemIDs[d.ItemID] { issues = append(issues, Issue{ Level: "ERROR", @@ -1314,6 +1363,21 @@ func validateFlagTriggerFiles(s Source) []Issue { if t.Condition != nil { issues = append(issues, validateCondition(rel, t.Condition, roomIndex, itemIDs)...) } + // Flag triggers fire only on Condition/Steps/Lock; item_id is dead here. + if t.ItemID != "" { + issues = append(issues, Issue{ + Level: "WARN", + Type: "trigger", + Message: fmt.Sprintf("trigger %s: item_id %q is ignored for flag triggers", rel, t.ItemID), + }) + if !itemIDs[t.ItemID] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("trigger %s: item_id %q does not exist", rel, t.ItemID), + }) + } + } for si := range t.Steps { issues = append(issues, validateStep(fmt.Sprintf("%s: step[%d]", rel, si), &t.Steps[si], itemIDs, roomIndex, mobIDs, dropIDs)...) } diff --git a/internal/validate/grid_test.go b/internal/validate/grid_test.go index 8bb8950..1b1755d 100644 --- a/internal/validate/grid_test.go +++ b/internal/validate/grid_test.go @@ -111,7 +111,7 @@ func TestGrid3DCleanViaUpDown(t *testing.T) { 2: "exits:\n east: {room: 3}\n down: {room: 1}\n", 3: "exits:\n down: {room: 4}\n west: {room: 2}\n", 4: "exits:\n south: {room: 5}\n up: {room: 3}\n", - 5: "name: five\n exits:\n north: {room: 4}\n", + 5: "name: five\nexits:\n north: {room: 4}\n", } if issues := runGridCheck(t, rooms, 1); len(issues) != 0 { t.Errorf("expected no grid issues, got: %+v", issues) -- cgit v1.2.3