// Provenance: own design — shared run scripts (spec §3.6) using System.Globalization; using System.Text; namespace Godosa.Core.Runs.Scripts; public interface IConditionContext { bool HasProbe(string name); RunValue Probe(string name, IReadOnlyList args); long? Constant(string name); } /// Condition AST: or / and / not / comparison / atom. A bare identifier is a probe without arguments when the /// game has one by that name, else a game constant. public abstract record Cond { public abstract RunValue Eval(IConditionContext c); public bool Test(IConditionContext c) => Truthy(Eval(c)); public abstract IEnumerable<(string Name, IReadOnlyList Args)> Probes(); public abstract IEnumerable Bare(); public static string Describe(Cond cond, IConditionContext c) => string.Join(" ", cond.Probes().Where(p => c.HasProbe(p.Name)).Distinct().Select(p => { var label = p.Args.Count == 0 ? p.Name : $"{p.Name}({string.Join(",", p.Args)})"; return $"{label}={c.Probe(p.Name, p.Args)}"; })); internal static bool Truthy(RunValue v) => v.IsNumber ? v.Number != 0 : !string.IsNullOrEmpty(v.Text); } public sealed record OrCond(Cond A, Cond B) : Cond { public override RunValue Eval(IConditionContext c) => RunValue.Of(A.Test(c) || B.Test(c)); public override IEnumerable<(string, IReadOnlyList)> Probes() => A.Probes().Concat(B.Probes()); public override IEnumerable Bare() => A.Bare().Concat(B.Bare()); } public sealed record AndCond(Cond A, Cond B) : Cond { public override RunValue Eval(IConditionContext c) => RunValue.Of(A.Test(c) && B.Test(c)); public override IEnumerable<(string, IReadOnlyList)> Probes() => A.Probes().Concat(B.Probes()); public override IEnumerable Bare() => A.Bare().Concat(B.Bare()); } public sealed record NotCond(Cond A) : Cond { public override RunValue Eval(IConditionContext c) => RunValue.Of(!A.Test(c)); public override IEnumerable<(string, IReadOnlyList)> Probes() => A.Probes(); public override IEnumerable Bare() => A.Bare(); } public sealed record CompareCond(Cond A, string Op, Cond B) : Cond { public override RunValue Eval(IConditionContext c) { var (a, b) = (A.Eval(c), B.Eval(c)); if (a.IsNumber != b.IsNumber) throw new ScriptFault($"cannot compare {a} with {b}"); var order = a.IsNumber ? a.Number!.Value.CompareTo(b.Number!.Value) : string.CompareOrdinal(a.Text, b.Text); if (!a.IsNumber && Op is "<" or "<=" or ">" or ">=") throw new ScriptFault($"cannot order strings {a} and {b}"); return RunValue.Of(Op switch { "=" => order == 0, "!=" => order != 0, "<" => order < 0, "<=" => order <= 0, ">" => order > 0, _ => order >= 0, }); } public override IEnumerable<(string, IReadOnlyList)> Probes() => A.Probes().Concat(B.Probes()); public override IEnumerable Bare() => A.Bare().Concat(B.Bare()); } public sealed record LiteralCond(RunValue Value) : Cond { public override RunValue Eval(IConditionContext c) => Value; public override IEnumerable<(string, IReadOnlyList)> Probes() => []; public override IEnumerable Bare() => []; } /// A probe call name(args), or a bare name (probe if the game has it, else a constant). public sealed record NameCond(string Name, IReadOnlyList Args, bool HasParens) : Cond { public override RunValue Eval(IConditionContext c) { if (HasParens || c.HasProbe(Name)) return c.Probe(Name, Args); return c.Constant(Name) is { } n ? RunValue.Of(n) : throw new ScriptFault($"unknown probe or constant {Name}"); } public override IEnumerable<(string, IReadOnlyList)> Probes() => [(Name, Args)]; public override IEnumerable Bare() => HasParens ? [] : [Name]; } public static class ConditionParser { public static Cond Parse(string text) { var tokens = Lex(text); var pos = 0; var cond = Or(tokens, ref pos); if (pos != tokens.Count) throw new FormatException($"unexpected '{tokens[pos]}' in condition '{text}'"); return cond; } private static Cond Or(List t, ref int pos) { var a = And(t, ref pos); while (pos < t.Count && t[pos] == "or") { pos++; a = new OrCond(a, And(t, ref pos)); } return a; } private static Cond And(List t, ref int pos) { var a = Not(t, ref pos); while (pos < t.Count && t[pos] == "and") { pos++; a = new AndCond(a, Not(t, ref pos)); } return a; } private static Cond Not(List t, ref int pos) { if (pos < t.Count && t[pos] == "not") { pos++; return new NotCond(Not(t, ref pos)); } return Compare(t, ref pos); } private static Cond Compare(List t, ref int pos) { var a = Atom(t, ref pos); if (pos < t.Count && t[pos] is "=" or "!=" or "<" or "<=" or ">" or ">=") { var op = t[pos++]; return new CompareCond(a, op, Atom(t, ref pos)); } return a; } private static Cond Atom(List t, ref int pos) { if (pos >= t.Count) throw new FormatException("condition ends early"); var tok = t[pos++]; if (tok == "(") { var inner = Or(t, ref pos); if (pos >= t.Count || t[pos] != ")") throw new FormatException("missing ')'"); pos++; return inner; } if (tok.StartsWith('"')) return new LiteralCond(RunValue.Of(tok[1..^1])); if (long.TryParse(tok, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var n)) return new LiteralCond(RunValue.Of(n)); if (!char.IsLetter(tok[0]) && tok[0] != '_') throw new FormatException($"unexpected '{tok}'"); var paren = tok.IndexOf('('); if (paren < 0) return new NameCond(tok, [], false); var args = tok[(paren + 1)..^1].Split(',', StringSplitOptions.TrimEntries).Where(a => a.Length > 0).ToArray(); return new NameCond(tok[..paren], args, true); } private static List Lex(string text) { var tokens = new List(); var i = 0; while (i < text.Length) { var c = text[i]; if (c == ' ') { i++; continue; } if (c is '(' or ')') { tokens.Add(c.ToString()); i++; continue; } if (c == '"') { var end = text.IndexOf('"', i + 1); if (end < 0) throw new FormatException("unterminated quote"); tokens.Add(text[i..(end + 1)]); i = end + 1; continue; } if (c is '=' or '!' or '<' or '>') { var two = i + 1 < text.Length && text[i + 1] == '='; var op = text.Substring(i, two ? 2 : 1); if (op == "!") throw new FormatException("use != or not"); tokens.Add(op); i += op.Length; continue; } var sb = new StringBuilder(); var depth = 0; while (i < text.Length && (depth > 0 || (text[i] != ' ' && text[i] != ')' && !(text[i] is '=' or '!' or '<' or '>')))) { if (text[i] == '(' && sb.Length == 0) break; if (text[i] == '(') depth++; else if (text[i] == ')') depth--; sb.Append(text[i++]); } if (sb.Length == 0) throw new FormatException($"unexpected '{text[i]}'"); tokens.Add(sb.ToString()); } return tokens; } }