defmodule TradeMachine.GMGradeV2.Pillars.RosterConstruction.RosterCompleteness do @moduledoc """ Scores AVC's Roster Completeness metric over the final scenario roster. Standard and two-way roster limits come from `TradeMachine.CBA.Rules`; this module does not duplicate CBA constants. Standard contracts receive AVC's piecewise base score, and occupied two-way slots add at most five points. """ alias TradeMachine.CBA.Rules alias TradeMachine.GMGradeV2.Benchmark alias TradeMachine.GMGradeV2.ChallengeSnapshot alias TradeMachine.GMGradeV2.Result.MetricResult @two_way_bonus_cap 5.0 @doc "Returns a full-precision Roster Completeness metric result." @spec score(map(), ChallengeSnapshot.t()) :: MetricResult.t() def score( %{roster: roster}, %ChallengeSnapshot{benchmark: %Benchmark{} = benchmark} ) when is_list(roster) do {two_way_players, standard_players} = Enum.split_with(roster, &two_way?/1) standard_count = length(standard_players) two_way_count = length(two_way_players) minimum = Rules.min_standard_roster() maximum = Rules.max_standard_roster() maximum_two_way = Rules.max_two_way_contracts() standard_score = standard_roster_score(standard_count, minimum, maximum) bonus = two_way_bonus(two_way_count, maximum_two_way) %MetricResult{ key: :roster_completeness, version: benchmark.version, score: min(100.0, standard_score + bonus), inputs: %{ standard_count: standard_count, two_way_count: two_way_count, minimum_standard_roster: minimum, maximum_standard_roster: maximum, maximum_two_way_contracts: maximum_two_way }, supporting: %{ standard_roster_score: standard_score, two_way_bonus: bonus }, explanation: "Roster Completeness scores #{standard_count} standard contracts and " <> "#{two_way_count} of #{maximum_two_way} available two-way slots." } end @doc """ Applies AVC's season-aware piecewise curve to a standard-roster count. When the legal range spans more than two players, values between the legal minimum (80) and one below the maximum (90) are linearly interpolated. """ @spec standard_roster_score(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: float() # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity def standard_roster_score(count, minimum, maximum) when is_integer(count) and count >= 0 and is_integer(minimum) and minimum >= 0 and is_integer(maximum) and maximum >= minimum do cond do count >= maximum -> 100.0 count == maximum - 1 -> 90.0 count >= minimum and maximum - minimum > 2 -> 80.0 + 10.0 * (count - minimum) / (maximum - minimum - 1) count >= minimum -> 80.0 count == minimum - 1 -> 60.0 count == minimum - 2 -> 40.0 count == minimum - 3 -> 20.0 true -> 0.0 end end @doc "Calculates the capped five-point two-way roster bonus." @spec two_way_bonus(non_neg_integer(), non_neg_integer()) :: float() def two_way_bonus(_count, 0), do: 0.0 def two_way_bonus(count, maximum) when is_integer(count) and count >= 0 and is_integer(maximum) and maximum > 0 do min(@two_way_bonus_cap, @two_way_bonus_cap * count / maximum) end defp two_way?(row) when is_map(row) do Map.get(row, :is_two_way, Map.get(row, "is_two_way")) == true end end