Current section

Files

Jump to
ex_ttrpg_dev lib rule_system evaluator.ex
Raw

lib/rule_system/evaluator.ex

defmodule ExTTRPGDev.RuleSystem.Evaluator do
@moduledoc """
Evaluates a rule system DAG for a given character state.
Takes the loaded system, a map of generated (base) values, and an optional
list of active effects (from equipped items, active feats, etc.),
and produces a fully-resolved map of all node values.
"""
alias ExTTRPGDev.RuleSystem.{Expression, Graph}
@doc """
Evaluates all nodes in the DAG in topological order.
- `system` — output of `Graph.build/1`
- `generated_values` — map of `{type_id, concept_id, field_name} => number` for generated nodes
- `effects` — list of `%{target: {type_id, concept_id, field_name}, value: number}`
Returns `{:ok, resolved_map}` or `{:error, reason}`.
## Examples
iex> system = ExTTRPGDev.RuleSystems.load_system!("dnd_5e_srd")
iex> attrs = ~w[strength dexterity constitution wisdom intelligence charisma]
iex> generated = Map.new(attrs, &{{"ability", &1, "base_score"}, 10})
iex> {:ok, resolved} = ExTTRPGDev.RuleSystem.Evaluator.evaluate(system, generated)
iex> resolved[{"ability", "strength", "modifier"}]
0
"""
def evaluate(system, generated_values, effects \\ []) do
order = Graph.topological_order(system)
Enum.reduce_while(order, {:ok, generated_values}, fn node_key, {:ok, resolved} ->
case evaluate_node(node_key, system.nodes, resolved, effects) do
{:ok, value} -> {:cont, {:ok, Map.put(resolved, node_key, value)}}
{:error, _} = err -> {:halt, err}
end
end)
end
@doc "Same as `evaluate/3` but raises on error."
def evaluate!(system, generated_values, effects \\ []) do
case evaluate(system, generated_values, effects) do
{:ok, resolved} -> resolved
{:error, reason} -> raise "Evaluation failed: #{inspect(reason)}"
end
end
defp evaluate_node(node_key, nodes, resolved, effects) do
case Map.fetch(nodes, node_key) do
{:ok, %{type: :generated}} ->
fetch_generated(resolved, node_key)
{:ok, %{type: :formula, formula: formula}} ->
Expression.evaluate(formula, resolved)
{:ok, %{type: :accumulator, base: base_formula}} ->
evaluate_accumulator(base_formula, node_key, resolved, effects)
{:ok, %{type: :mapping, input: input_formula, steps: steps}} ->
evaluate_mapping(input_formula, steps, resolved)
:error ->
{:error, {:unknown_node, node_key}}
end
end
defp evaluate_accumulator(base_formula, node_key, resolved, effects) do
with {:ok, base_value} <- Expression.evaluate(base_formula, resolved) do
effects
|> Enum.filter(fn %{target: target} -> target == node_key end)
|> Enum.reduce_while({:ok, base_value}, &apply_effect(&1, &2, resolved))
end
end
defp apply_effect(effect, {:ok, acc}, resolved) do
condition = Map.get(effect, :when)
item_fields = Map.get(effect, :item_fields, %{})
with {:ok, true} <- evaluate_condition(condition, resolved, item_fields),
{:ok, n} <- resolve_effect_value(effect.value, resolved, item_fields) do
{:cont, {:ok, acc + n}}
else
{:ok, false} -> {:cont, {:ok, acc}}
error -> {:halt, error}
end
end
defp evaluate_condition(nil, _resolved, _item_fields), do: {:ok, true}
defp evaluate_condition(formula, resolved, item_fields) when is_binary(formula) do
case formula |> substitute_item_fields(item_fields) |> Expression.evaluate(resolved) do
{:ok, result} -> {:ok, truthy?(result)}
error -> error
end
end
defp truthy?(true), do: true
defp truthy?(false), do: false
defp truthy?(n) when is_number(n), do: n != 0
defp truthy?(_), do: false
defp resolve_effect_value(value, _resolved, _item_fields) when is_number(value),
do: {:ok, value}
defp resolve_effect_value(formula, resolved, item_fields) when is_binary(formula) do
formula |> substitute_item_fields(item_fields) |> Expression.evaluate(resolved)
end
defp substitute_item_fields(formula, item_fields) do
Enum.reduce(item_fields, formula, fn {name, value}, acc ->
String.replace(acc, "item.#{name}", to_string(value))
end)
end
defp evaluate_mapping(input_formula, steps, resolved) do
with {:ok, input_value} <- Expression.evaluate(input_formula, resolved) do
result =
steps
|> Enum.filter(fn [threshold, _] -> input_value >= threshold end)
|> List.last()
case result do
[_, output] -> {:ok, output}
nil -> {:error, {:mapping_no_match, input_value}}
end
end
end
defp fetch_generated(resolved, node_key) do
case Map.fetch(resolved, node_key) do
{:ok, value} -> {:ok, value}
:error -> {:error, {:missing_generated_value, node_key}}
end
end
end