// Package enemyevents owns canonical ordering and identity policy for D&D // enemy-event artifacts. package enemyevents import ( "encoding/json" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) // NormalizeDisplay returns the durable display form for an enemy subject. func NormalizeDisplay(value string) string { return identity.NormalizeDisplay(value) } // ComparisonKey returns the shared D&D identity key for an enemy subject. func ComparisonKey(value string) string { return identity.ComparisonKey(NormalizeDisplay(value)) } // SupportedKind reports whether kind belongs to the durable enemy-event vocabulary. func SupportedKind(kind dnd.EnemyEventKind) bool { _, ok := KindRank(kind) return ok } // KindRank returns the explicit chronological tie-break order for event kinds. func KindRank(kind dnd.EnemyEventKind) (int, bool) { switch kind { case dnd.EnemyEventKindEngaged: return 0, true case dnd.EnemyEventKindIncapacitated: return 1, true case dnd.EnemyEventKindCaptured: return 2, true case dnd.EnemyEventKindFled: return 3, true case dnd.EnemyEventKindKilled: return 4, true default: return 0, false } } // SourceRefsEqual reports whether two reference sequences are equal after // canonical ordering and exact duplicate removal. func SourceRefsEqual(order shared.SourceRefOrder, left, right []source.SourceRef) bool { left = order.Canonicalize(left) right = order.Canonicalize(right) if (left == nil) != (right == nil) || len(left) != len(right) { return false } for index := range left { if left[index] != right[index] { return false } } return true } // ExactEqual reports whether events have the same subject identity, kind, and // complete canonical evidence sequence. func ExactEqual(order shared.SourceRefOrder, left, right dnd.EnemyEvent) bool { left.SourceRefs = order.Canonicalize(left.SourceRefs) right.SourceRefs = order.Canonicalize(right.SourceRefs) return CanonicalExactEqual(left, right) } type canonicalIdentity struct { Subject string `json:"subject"` Kind dnd.EnemyEventKind `json:"kind"` SourceRefs []source.SourceRef `json:"source_refs"` } // CanonicalIdentity returns the complete duplicate-lookup key for an event // whose source references are already canonical. func CanonicalIdentity(event dnd.EnemyEvent) string { encoded, err := json.Marshal(canonicalIdentity{ Subject: ComparisonKey(event.Name), Kind: event.Kind, SourceRefs: event.SourceRefs, }) if err != nil { panic("encode enemy event identity") } return string(encoded) } // CanonicalExactEqual reports whether events have the same complete duplicate // identity without modifying or canonicalizing their source references. func CanonicalExactEqual(left, right dnd.EnemyEvent) bool { return ComparisonKey(left.Name) == ComparisonKey(right.Name) && left.Kind == right.Kind && canonicalSourceRefsEqual(left.SourceRefs, right.SourceRefs) } func canonicalSourceRefsEqual(left, right []source.SourceRef) bool { if (left == nil) != (right == nil) || len(left) != len(right) { return false } for index := range left { if left[index] != right[index] { return false } } return true } // Less defines the canonical stable event order. Invalid source references // remain comparable through SourceRefOrder's literal fallback so malformed // candidates can still be sorted for later validation. func Less(order shared.SourceRefOrder, left, right dnd.EnemyEvent) bool { leftPosition, leftHasEvidence := order.EarliestValid(left.SourceRefs) rightPosition, rightHasEvidence := order.EarliestValid(right.SourceRefs) if leftHasEvidence != rightHasEvidence { return leftHasEvidence } if leftHasEvidence && leftPosition != rightPosition { return leftPosition < rightPosition } if leftKey, rightKey := ComparisonKey(left.Name), ComparisonKey(right.Name); leftKey != rightKey { return leftKey < rightKey } if leftName, rightName := NormalizeDisplay(left.Name), NormalizeDisplay(right.Name); leftName != rightName { return leftName < rightName } if leftRank, leftKnown := KindRank(left.Kind); leftKnown { if rightRank, rightKnown := KindRank(right.Kind); rightKnown && leftRank != rightRank { return leftRank < rightRank } else if !rightKnown { return true } } else if _, rightKnown := KindRank(right.Kind); rightKnown { return false } if left.Kind != right.Kind { return left.Kind < right.Kind } return sourceRefsLess(order, order.Canonicalize(left.SourceRefs), order.Canonicalize(right.SourceRefs)) } func sourceRefsLess(order shared.SourceRefOrder, left, right []source.SourceRef) bool { for index := 0; index < len(left) && index < len(right); index++ { if left[index] == right[index] { continue } return order.Less(left[index], right[index]) } return len(left) < len(right) }