Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 28 additions & 21 deletions WorldSim.Engine/Rules/RuleBlockGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,32 @@ public static RuleBlockGraphValidationResult ToRuleDefinition(RuleBlockGraphDto
var duplicateBlockId = graph.Blocks.GroupBy(block => block.BlockId, StringComparer.Ordinal).FirstOrDefault(group => group.Count() > 1)?.Key;
if (duplicateBlockId is not null) errors.Add($"Block id `{duplicateBlockId}` is duplicated.");

var localErrors = new List<string>();

var conditionBlocks = graph.Blocks.Where(block => block.Kind == RuleBlockKind.Condition).OrderBy(block => block.BlockId, StringComparer.Ordinal).ToArray();
if (conditionBlocks.Length != 1) errors.Add("Block graph must contain exactly one condition block.");
if (conditionBlocks.Length != 1) localErrors.Add("Block graph must contain exactly one condition block.");

var effectBlocks = graph.Blocks.Where(block => block.Kind == RuleBlockKind.Effect).OrderBy(block => ReadInt(block, "effectIndex", errors, block.BlockId)).ThenBy(block => block.BlockId, StringComparer.Ordinal).ToArray();
if (effectBlocks.Length == 0) errors.Add("Block graph must contain at least one effect block.");
var effectBlocks = graph.Blocks.Where(block => block.Kind == RuleBlockKind.Effect).OrderBy(block => ReadInt(block, "effectIndex", localErrors, block.BlockId)).ThenBy(block => block.BlockId, StringComparer.Ordinal).ToArray();
if (effectBlocks.Length == 0) localErrors.Add("Block graph must contain at least one effect block.");

RuleCondition? condition = null;
if (conditionBlocks.Length == 1)
{
condition = ReadCondition(conditionBlocks[0], errors);
condition = ReadCondition(conditionBlocks[0], localErrors);
}

var effects = new List<RuleEffect>(effectBlocks.Length);
for (var expectedIndex = 0; expectedIndex < effectBlocks.Length; expectedIndex++)
{
var block = effectBlocks[expectedIndex];
var index = ReadInt(block, "effectIndex", errors, block.BlockId);
if (index != expectedIndex) errors.Add($"Effect block `{block.BlockId}` must have contiguous `effectIndex` {expectedIndex}.");
var effect = ReadEffect(block, errors);
var index = ReadInt(block, "effectIndex", localErrors, block.BlockId);
if (index != expectedIndex) localErrors.Add($"Effect block `{block.BlockId}` must have contiguous `effectIndex` {expectedIndex}.");
var effect = ReadEffect(block, localErrors);
if (effect is not null) effects.Add(effect);
}

errors.AddRange(localErrors);

ValidateWires(graph, conditionBlocks.SingleOrDefault()?.BlockId, effectBlocks.Select(block => block.BlockId).ToArray(), errors);

if (errors.Count > 0 || condition is null || effects.Count != effectBlocks.Length)
Expand Down Expand Up @@ -191,33 +195,36 @@ private static int BlockEffectIndex(RuleBlockDto block) =>

private static RuleEffect? ReadEffect(RuleBlockDto block, ICollection<string> errors)
{
if (!ReadEnum(block, "kind", errors, out RuleEffectKind kind) || kind != RuleEffectKind.AdjustTraitWeight)
var localErrors = new List<string>();
if (!ReadEnum(block, "kind", localErrors, out RuleEffectKind kind) || kind != RuleEffectKind.AdjustTraitWeight)
{
errors.Add($"Effect block `{block.BlockId}` must use `AdjustTraitWeight`.");
return null;
localErrors.Add($"Effect block `{block.BlockId}` must use `AdjustTraitWeight`.");
}

if (!ReadEnum(block, "targetKind", errors, out RuleTargetKind targetKind) || targetKind != RuleTargetKind.AllAgents)
if (!ReadEnum(block, "targetKind", localErrors, out RuleTargetKind targetKind) || targetKind != RuleTargetKind.AllAgents)
{
errors.Add($"Effect block `{block.BlockId}` must target `AllAgents`.");
return null;
localErrors.Add($"Effect block `{block.BlockId}` must target `AllAgents`.");
}

if (!ReadEnum(block, "trait", errors, out RuleTraitKind trait))
if (!ReadEnum(block, "trait", localErrors, out RuleTraitKind trait))
{
errors.Add($"Effect block `{block.BlockId}` has unsupported `trait`.");
return null;
localErrors.Add($"Effect block `{block.BlockId}` has unsupported `trait`.");
}

var deltaText = ReadParameter(block, "delta", errors);
var deltaText = ReadParameter(block, "delta", localErrors);
if (!float.TryParse(deltaText, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var delta))
{
errors.Add($"Effect block `{block.BlockId}` requires numeric `delta`.");
return null;
localErrors.Add($"Effect block `{block.BlockId}` requires numeric `delta`.");
}

if (delta is < -1f or > 1f) localErrors.Add($"Effect block `{block.BlockId}` `delta` must stay within [-1.0, 1.0].");

foreach(var err in localErrors)
{
errors.Add(err);
}

if (delta is < -1f or > 1f) errors.Add($"Effect block `{block.BlockId}` `delta` must stay within [-1.0, 1.0].");
return errors.Count > 0 ? null : new RuleEffect(kind, new RuleTarget(targetKind), trait, delta);
return localErrors.Count > 0 ? null : new RuleEffect(kind, new RuleTarget(targetKind), trait, delta);
}

private static void ValidateWires(RuleBlockGraphDto graph, string? conditionBlockId, IReadOnlyList<string> effectBlockIds, ICollection<string> errors)
Expand Down
13 changes: 10 additions & 3 deletions WorldSim.Engine/Rules/RuleValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ private static bool TryParseEnum<TEnum>(string? text, out TEnum value)
return false;
}

private static string BuildNormalizedPreview(RuleDefinition rule)
public string NormalizeToDeterministicJson(RuleDefinition rule)
{
var preview = new
{
Expand All @@ -247,13 +247,20 @@ private static string BuildNormalizedPreview(RuleDefinition rule)
},
trait = effect.Trait,
delta = effect.Delta
}).ToArray(),
runtimeStatus = "Ready: safe validation, deterministic queueing, activation, and runtime trait effects are wired into the simulation loop."
}).ToArray()
};

return JsonSerializer.Serialize(preview, PreviewSerializerOptions);
}

private string BuildNormalizedPreview(RuleDefinition rule)
{
var previewJson = NormalizeToDeterministicJson(rule);
var document = JsonSerializer.Deserialize<Dictionary<string, object>>(previewJson, SerializerOptions)!;
document["runtimeStatus"] = "Ready: safe validation, deterministic queueing, activation, and runtime trait effects are wired into the simulation loop.";
return JsonSerializer.Serialize(document, PreviewSerializerOptions);
}

private sealed record RuleJsonDocument(
int? Id,
string? Name,
Expand Down
4 changes: 2 additions & 2 deletions WorldSim.Godot/Scripts/RuleBlockGraphView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private RuleBlockGraphDto BuildEditedGraph(RuleBlockGraphDto source)
{
if (block.Kind == RuleBlockKind.Condition)
{
var copy = new SortedDictionary<string, string>(block.Parameters, StringComparer.Ordinal)
var copy = new SortedDictionary<string, string>(block.Parameters.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), StringComparer.Ordinal)
{
["nodeId"] = _nodeIdInput.Text.Trim(),
["resourceId"] = _resourceIdInput.Text.Trim(),
Expand All @@ -178,7 +178,7 @@ private RuleBlockGraphDto BuildEditedGraph(RuleBlockGraphDto source)

if (block.Kind == RuleBlockKind.Effect)
{
var copy = new SortedDictionary<string, string>(block.Parameters, StringComparer.Ordinal)
var copy = new SortedDictionary<string, string>(block.Parameters.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), StringComparer.Ordinal)
{
["trait"] = _traitInput.Text.Trim(),
["delta"] = _deltaInput.Text.Trim()
Expand Down