Simplify D&D combat turn extraction contract
This commit is contained in:
@@ -22,9 +22,7 @@ const (
|
||||
normalizationPolicy = "dnd.combat_turns.normalize.v1"
|
||||
NormalizationPolicy = normalizationPolicy
|
||||
|
||||
ReasonCodeFieldsNormalized = "combat_turn_fields_normalized"
|
||||
ReasonCodeActorCanonicalized = "combat_actor_canonicalized"
|
||||
ReasonCodeTargetCanonicalized = "combat_target_canonicalized"
|
||||
ReasonCodeSourceRefsNormalized = "source_references_normalized"
|
||||
ReasonCodeTurnsReordered = "combat_turns_reordered"
|
||||
ReasonCodeDuplicateCollapsed = "duplicate_combat_turn_collapsed"
|
||||
@@ -122,11 +120,9 @@ type normalizedRecord struct {
|
||||
hasEvidence bool
|
||||
}
|
||||
|
||||
type targetCanonicalization struct {
|
||||
actionIndex int
|
||||
targetIndex int
|
||||
from string
|
||||
to string
|
||||
type actorCanonicalization struct {
|
||||
from string
|
||||
to string
|
||||
}
|
||||
|
||||
func normalizeList(input dnd.CombatTurnList, doc *source.SourceDocument, registry *npcregistry.Registry) (dnd.CombatTurnList, []contracts.Warning) {
|
||||
@@ -137,7 +133,7 @@ func normalizeList(input dnd.CombatTurnList, doc *source.SourceDocument, registr
|
||||
records := make([]normalizedRecord, len(input.CombatTurns))
|
||||
warnings := make([]contracts.Warning, 0)
|
||||
for index, inputTurn := range input.CombatTurns {
|
||||
turn, fieldsChanged, actorChange, targetChanges, refsChanged := normalizeTurn(inputTurn, registry)
|
||||
turn, actorChange, refsChanged := normalizeTurn(inputTurn, registry)
|
||||
earliest, hasEvidence := earliestSourcePosition(doc, turn)
|
||||
records[index] = normalizedRecord{
|
||||
turn: turn,
|
||||
@@ -145,13 +141,6 @@ func normalizeList(input dnd.CombatTurnList, doc *source.SourceDocument, registr
|
||||
earliest: earliest,
|
||||
hasEvidence: hasEvidence,
|
||||
}
|
||||
if fieldsChanged {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: turnScope(index),
|
||||
ReasonCode: ReasonCodeFieldsNormalized,
|
||||
Message: fmt.Sprintf("input index %d: combat turn fields normalized", index),
|
||||
})
|
||||
}
|
||||
if actorChange != nil {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: turnScope(index),
|
||||
@@ -160,15 +149,6 @@ func normalizeList(input dnd.CombatTurnList, doc *source.SourceDocument, registr
|
||||
index, diagnostics.Quote(actorChange.from), diagnostics.Quote(actorChange.to)),
|
||||
})
|
||||
}
|
||||
for _, targetChange := range targetChanges {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: turnScope(index),
|
||||
ReasonCode: ReasonCodeTargetCanonicalized,
|
||||
Message: fmt.Sprintf("input index %d: action %d target %d canonicalized from %s to %s",
|
||||
index, targetChange.actionIndex, targetChange.targetIndex,
|
||||
diagnostics.Quote(targetChange.from), diagnostics.Quote(targetChange.to)),
|
||||
})
|
||||
}
|
||||
if refsChanged {
|
||||
warnings = append(warnings, contracts.Warning{
|
||||
Scope: turnScope(index),
|
||||
@@ -205,87 +185,27 @@ func normalizeList(input dnd.CombatTurnList, doc *source.SourceDocument, registr
|
||||
return dnd.CombatTurnList{CombatTurns: output}, warnings
|
||||
}
|
||||
|
||||
func normalizeTurn(input dnd.CombatTurn, registry *npcregistry.Registry) (dnd.CombatTurn, bool, *targetCanonicalization, []targetCanonicalization, bool) {
|
||||
func normalizeTurn(input dnd.CombatTurn, registry *npcregistry.Registry) (dnd.CombatTurn, *actorCanonicalization, bool) {
|
||||
output := cloneCombatTurn(input)
|
||||
output.Actor = identity.NormalizeDisplay(input.Actor)
|
||||
output.Summary = identity.NormalizeDisplay(input.Summary)
|
||||
|
||||
var actorChange *targetCanonicalization
|
||||
if canonical, ok := registry.Lookup(output.Actor); ok {
|
||||
canonicalName := identity.NormalizeDisplay(canonical.Name)
|
||||
if output.Actor != canonicalName {
|
||||
actorChange = &targetCanonicalization{from: output.Actor, to: canonicalName}
|
||||
output.Actor = canonicalName
|
||||
}
|
||||
output.Actor = canonicalName
|
||||
}
|
||||
var actorChange *actorCanonicalization
|
||||
if input.Actor != output.Actor {
|
||||
actorChange = &actorCanonicalization{from: input.Actor, to: output.Actor}
|
||||
}
|
||||
|
||||
targetChanges := make([]targetCanonicalization, 0)
|
||||
for actionIndex := range output.Actions {
|
||||
action := &output.Actions[actionIndex]
|
||||
action.Declaration = identity.NormalizeDisplay(action.Declaration)
|
||||
if action.Resolution != nil {
|
||||
resolution := identity.NormalizeDisplay(*action.Resolution)
|
||||
action.Resolution = &resolution
|
||||
}
|
||||
if action.Targets == nil {
|
||||
continue
|
||||
}
|
||||
targets := make([]string, 0, len(action.Targets))
|
||||
seen := make(map[string]struct{}, len(action.Targets))
|
||||
for targetIndex, target := range action.Targets {
|
||||
normalized := identity.NormalizeDisplay(target)
|
||||
if canonical, ok := registry.Lookup(normalized); ok {
|
||||
canonicalName := identity.NormalizeDisplay(canonical.Name)
|
||||
if normalized != canonicalName {
|
||||
targetChanges = append(targetChanges, targetCanonicalization{
|
||||
actionIndex: actionIndex,
|
||||
targetIndex: targetIndex,
|
||||
from: normalized,
|
||||
to: canonicalName,
|
||||
})
|
||||
}
|
||||
normalized = canonicalName
|
||||
}
|
||||
key := identity.ComparisonKey(normalized)
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
targets = append(targets, normalized)
|
||||
}
|
||||
action.Targets = targets
|
||||
}
|
||||
|
||||
fieldsChanged := input.Actor != output.Actor || input.Summary != output.Summary
|
||||
if len(input.Actions) != len(output.Actions) {
|
||||
fieldsChanged = true
|
||||
}
|
||||
for index := range output.Actions {
|
||||
if input.Actions[index].Declaration != output.Actions[index].Declaration ||
|
||||
!stringSlicesEqual(input.Actions[index].Targets, output.Actions[index].Targets) ||
|
||||
!stringPointersEqual(input.Actions[index].Resolution, output.Actions[index].Resolution) {
|
||||
fieldsChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
canonicalRefs, _, _ := canonicalizeSourceRefs(input.SourceRefs)
|
||||
output.SourceRefs = canonicalRefs
|
||||
refsChanged := !sourceRefsEqual(input.SourceRefs, output.SourceRefs)
|
||||
return output, fieldsChanged, actorChange, targetChanges, refsChanged
|
||||
return output, actorChange, refsChanged
|
||||
}
|
||||
|
||||
func cloneCombatTurn(input dnd.CombatTurn) dnd.CombatTurn {
|
||||
output := input
|
||||
if input.Round != nil {
|
||||
round := *input.Round
|
||||
output.Round = &round
|
||||
}
|
||||
if input.Actions != nil {
|
||||
output.Actions = make([]dnd.CombatAction, len(input.Actions))
|
||||
for index, action := range input.Actions {
|
||||
output.Actions[index] = cloneCombatAction(action)
|
||||
}
|
||||
}
|
||||
if input.SourceRefs != nil {
|
||||
output.SourceRefs = make([]source.SourceRef, len(input.SourceRefs))
|
||||
copy(output.SourceRefs, input.SourceRefs)
|
||||
@@ -293,38 +213,6 @@ func cloneCombatTurn(input dnd.CombatTurn) dnd.CombatTurn {
|
||||
return output
|
||||
}
|
||||
|
||||
func cloneCombatAction(input dnd.CombatAction) dnd.CombatAction {
|
||||
output := input
|
||||
if input.Targets != nil {
|
||||
output.Targets = make([]string, len(input.Targets))
|
||||
copy(output.Targets, input.Targets)
|
||||
}
|
||||
if input.Resolution != nil {
|
||||
resolution := *input.Resolution
|
||||
output.Resolution = &resolution
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func stringSlicesEqual(left, right []string) 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
|
||||
}
|
||||
|
||||
func stringPointersEqual(left, right *string) bool {
|
||||
if (left == nil) != (right == nil) {
|
||||
return false
|
||||
}
|
||||
return left == nil || *left == *right
|
||||
}
|
||||
|
||||
func sourceRefsEqual(left, right []source.SourceRef) bool {
|
||||
if (left == nil) != (right == nil) || len(left) != len(right) {
|
||||
return false
|
||||
@@ -454,12 +342,6 @@ func duplicateKey(turn dnd.CombatTurn, doc *source.SourceDocument) (string, bool
|
||||
var key strings.Builder
|
||||
writeKeyString(&key, identity.ComparisonKey(turn.Actor))
|
||||
writeKeyString(&key, string(turn.TurnKind))
|
||||
if turn.Round == nil {
|
||||
key.WriteByte('0')
|
||||
} else {
|
||||
key.WriteByte('1')
|
||||
writeKeyInt(&key, *turn.Round)
|
||||
}
|
||||
for _, ref := range turn.SourceRefs {
|
||||
writeKeyString(&key, ref.SourceID)
|
||||
writeKeyInt(&key, ref.StartUnitID)
|
||||
@@ -499,7 +381,7 @@ func turnScope(index int) string { return fmt.Sprintf("combat_turns[%d]", index)
|
||||
func referenceSlots() []contracts.ReferenceSlot {
|
||||
return []contracts.ReferenceSlot{{
|
||||
Name: NPCRegistryReferenceSlot,
|
||||
Description: "Optional normalized NPC registry used for canonical actor and target grounding.",
|
||||
Description: "Optional normalized NPC registry used for canonical actor grounding.",
|
||||
AcceptedMediaTypes: []string{"application/json"},
|
||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
||||
MaxBytes: NPCRegistryMaxBytes,
|
||||
|
||||
Reference in New Issue
Block a user