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(%{value: v}, {:ok, acc}, resolved) do
case resolve_effect_value(v, resolved) do
{:ok, n} -> {:cont, {:ok, acc + n}}
error -> {:halt, error}
end
end
defp resolve_effect_value(value, _resolved) when is_number(value), do: {:ok, value}
defp resolve_effect_value(formula, resolved) when is_binary(formula),
do: Expression.evaluate(formula, resolved)
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