defmodule TradeMachine.GMGradeV2.Pillars.TimelineAlignment.CoreAgeFit do @moduledoc "Scores each core player's whole-year age against the selected mandate." alias TradeMachine.GMGradeV2.MandateProfile alias TradeMachine.GMGradeV2.Result.MetricResult @doc "Returns AVC's core-weighted Core Age Fit metric." @spec score([map()], MandateProfile.t()) :: MetricResult.t() def score(core, %MandateProfile{} = profile) when is_list(core) do players = Enum.map(core, fn player -> player_score = player_score(player.age, profile) %{ player_id: player.player_id, age: player.age, core_weight: player.core_weight, score: player_score } end) score = Enum.reduce(players, 0.0, &(&1.score * &1.core_weight + &2)) %MetricResult{ key: :core_age_fit, version: profile.version, score: score, inputs: %{mandate: profile.key}, supporting: %{ players: players, ideal_core_age: profile.ideal_core_age, penalty_below: profile.age_penalty_below, penalty_above: profile.age_penalty_above }, weights: Map.new(players, &{&1.player_id, &1.core_weight}), explanation: "Core players are scored against #{profile.label}'s ideal age range." } end @doc "Scores one whole-year age against a mandate's asymmetric range." @spec player_score(number(), MandateProfile.t()) :: float() def player_score(age, %MandateProfile{} = profile) when is_number(age) do penalty = cond do age < profile.ideal_core_age.min -> (profile.ideal_core_age.min - age) * profile.age_penalty_below age > profile.ideal_core_age.max -> (age - profile.ideal_core_age.max) * profile.age_penalty_above true -> 0 end max(0.0, 100.0 - penalty) end end