273 lines
8.0 KiB
Elixir
273 lines
8.0 KiB
Elixir
|
|
defmodule ShootingEventPhx.Domain.Derived do
|
||
|
|
@moduledoc false
|
||
|
|
alias ShootingEventPhx.Domain.Stages
|
||
|
|
|
||
|
|
def compute(players, scores) do
|
||
|
|
player_by_id = Map.new(players, &{&1.id, &1})
|
||
|
|
|
||
|
|
pre_rows =
|
||
|
|
players
|
||
|
|
|> Enum.map(fn p ->
|
||
|
|
%{
|
||
|
|
"playerId" => p.id,
|
||
|
|
"nameAr" => p.name_ar,
|
||
|
|
"nameEn" => p.name_en,
|
||
|
|
"groupCode" => p.group_code,
|
||
|
|
"imageData" => p.image_data,
|
||
|
|
"score" => score_total(scores, p.id, Stages.preliminary_round_stages()),
|
||
|
|
"tieBreak" => get_score(scores, "prelim_tiebreak", p.id),
|
||
|
|
"rank" => 0,
|
||
|
|
"seed" => 0,
|
||
|
|
"finalGroup" => 0
|
||
|
|
}
|
||
|
|
end)
|
||
|
|
|> Enum.sort_by(fn row -> {-row["score"], row["playerId"]} end)
|
||
|
|
|> assign_dense_rank_by_score()
|
||
|
|
|
||
|
|
{pre_tie, finalists} = compute_finalists(pre_rows)
|
||
|
|
|
||
|
|
finalists =
|
||
|
|
finalists
|
||
|
|
|> Enum.with_index(1)
|
||
|
|
|> Enum.map(fn {row, idx} ->
|
||
|
|
row
|
||
|
|
|> Map.put("seed", idx)
|
||
|
|
|> Map.put("finalGroup", if(idx <= 6, do: 1, else: 2))
|
||
|
|
end)
|
||
|
|
|
||
|
|
final_group1 = Enum.filter(finalists, &(&1["finalGroup"] == 1))
|
||
|
|
final_group2 = Enum.filter(finalists, &(&1["finalGroup"] == 2))
|
||
|
|
|
||
|
|
final_rows =
|
||
|
|
finalists
|
||
|
|
|> Enum.map(fn finalist ->
|
||
|
|
p = Map.fetch!(player_by_id, finalist["playerId"])
|
||
|
|
|
||
|
|
%{
|
||
|
|
"playerId" => finalist["playerId"],
|
||
|
|
"nameAr" => p.name_ar,
|
||
|
|
"nameEn" => p.name_en,
|
||
|
|
"groupCode" => p.group_code,
|
||
|
|
"imageData" => p.image_data,
|
||
|
|
"score" => score_total(scores, finalist["playerId"], Stages.final_round_stages()),
|
||
|
|
"tieBreak" => get_score(scores, "final_tiebreak", finalist["playerId"]),
|
||
|
|
"rank" => 0,
|
||
|
|
"seed" => finalist["seed"],
|
||
|
|
"finalGroup" => finalist["finalGroup"]
|
||
|
|
}
|
||
|
|
end)
|
||
|
|
|> compute_final_ranking()
|
||
|
|
|
||
|
|
podium = final_rows |> Enum.take(3)
|
||
|
|
final_tie = final_tie_info(final_rows)
|
||
|
|
|
||
|
|
%{
|
||
|
|
"preliminaryRanking" => %{
|
||
|
|
"rows" => pre_rows,
|
||
|
|
"tieBreak" => pre_tie,
|
||
|
|
"unresolved" => pre_tie["required"] and not pre_tie["resolved"]
|
||
|
|
},
|
||
|
|
"finalists" => finalists,
|
||
|
|
"finalGroups" => %{"group1" => final_group1, "group2" => final_group2},
|
||
|
|
"finalRanking" => %{
|
||
|
|
"rows" => final_rows,
|
||
|
|
"tieBreak" => final_tie,
|
||
|
|
"unresolved" => final_tie["required"] and not final_tie["resolved"]
|
||
|
|
},
|
||
|
|
"podium" => podium
|
||
|
|
}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp compute_finalists(pre_rows) do
|
||
|
|
if length(pre_rows) <= 12 do
|
||
|
|
finalists =
|
||
|
|
pre_rows
|
||
|
|
|> Enum.with_index(1)
|
||
|
|
|> Enum.map(fn {row, seed} -> Map.put(row, "seed", seed) end)
|
||
|
|
|
||
|
|
{empty_tie(), finalists}
|
||
|
|
else
|
||
|
|
cutoff = Enum.at(pre_rows, 11)["score"]
|
||
|
|
above = Enum.filter(pre_rows, &(&1["score"] > cutoff))
|
||
|
|
at_cutoff = Enum.filter(pre_rows, &(&1["score"] == cutoff))
|
||
|
|
slots = max(12 - length(above), 0)
|
||
|
|
|
||
|
|
if length(at_cutoff) <= slots do
|
||
|
|
{empty_tie(), above ++ at_cutoff}
|
||
|
|
else
|
||
|
|
tie = %{
|
||
|
|
"required" => true,
|
||
|
|
"resolved" => true,
|
||
|
|
"slots" => slots,
|
||
|
|
"playerIds" => at_cutoff |> Enum.map(& &1["playerId"]) |> Enum.sort()
|
||
|
|
}
|
||
|
|
|
||
|
|
sorted_at_cutoff =
|
||
|
|
Enum.sort_by(at_cutoff, fn row -> {-row["tieBreak"], row["playerId"]} end)
|
||
|
|
|
||
|
|
tie =
|
||
|
|
if slots > 0 do
|
||
|
|
boundary = Enum.at(sorted_at_cutoff, slots - 1)["tieBreak"]
|
||
|
|
greater = Enum.count(sorted_at_cutoff, &(&1["tieBreak"] > boundary))
|
||
|
|
equal = Enum.count(sorted_at_cutoff, &(&1["tieBreak"] == boundary))
|
||
|
|
resolved = not (greater < slots and greater + equal > slots)
|
||
|
|
Map.put(tie, "resolved", resolved)
|
||
|
|
else
|
||
|
|
tie
|
||
|
|
end
|
||
|
|
|
||
|
|
finalists = above ++ Enum.take(sorted_at_cutoff, min(slots, length(sorted_at_cutoff)))
|
||
|
|
|
||
|
|
finalists =
|
||
|
|
finalists
|
||
|
|
|> Enum.sort_by(fn row ->
|
||
|
|
if tie["required"] do
|
||
|
|
{-row["score"], -row["tieBreak"], row["playerId"]}
|
||
|
|
else
|
||
|
|
{-row["score"], row["playerId"]}
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
|> assign_dense_rank(fn a, b ->
|
||
|
|
if tie["required"],
|
||
|
|
do: a["score"] == b["score"] and a["tieBreak"] == b["tieBreak"],
|
||
|
|
else: a["score"] == b["score"]
|
||
|
|
end)
|
||
|
|
|
||
|
|
{tie, finalists}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp compute_final_ranking(rows) do
|
||
|
|
sorted = Enum.sort_by(rows, fn row -> {-row["score"], row["seed"]} end)
|
||
|
|
tied_top = tied_top_for_podium(sorted)
|
||
|
|
|
||
|
|
rows =
|
||
|
|
if MapSet.size(tied_top) > 0 do
|
||
|
|
Enum.sort_by(sorted, fn row ->
|
||
|
|
tied = MapSet.member?(tied_top, row["playerId"])
|
||
|
|
tie_break_key = if tied, do: -row["tieBreak"], else: 0
|
||
|
|
{-row["score"], tie_break_key, row["seed"]}
|
||
|
|
end)
|
||
|
|
else
|
||
|
|
sorted
|
||
|
|
end
|
||
|
|
|
||
|
|
if MapSet.size(tied_top) > 0 do
|
||
|
|
assign_dense_rank(rows, fn a, b ->
|
||
|
|
if a["score"] != b["score"] do
|
||
|
|
false
|
||
|
|
else
|
||
|
|
tied_a = MapSet.member?(tied_top, a["playerId"])
|
||
|
|
tied_b = MapSet.member?(tied_top, b["playerId"])
|
||
|
|
if tied_a and tied_b, do: a["tieBreak"] == b["tieBreak"], else: true
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
else
|
||
|
|
assign_dense_rank_by_score(rows)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp final_tie_info(final_rows) do
|
||
|
|
tied_top = tied_top_for_podium(final_rows)
|
||
|
|
|
||
|
|
if MapSet.size(tied_top) == 0 do
|
||
|
|
empty_tie()
|
||
|
|
else
|
||
|
|
player_ids = tied_top |> MapSet.to_list() |> Enum.sort()
|
||
|
|
|
||
|
|
resolved =
|
||
|
|
if length(final_rows) >= 3 do
|
||
|
|
third = Enum.at(final_rows, 2)
|
||
|
|
|
||
|
|
{greater, equal} =
|
||
|
|
Enum.reduce(final_rows, {0, 0}, fn row, {g, e} ->
|
||
|
|
cond do
|
||
|
|
row["score"] > third["score"] ->
|
||
|
|
{g + 1, e}
|
||
|
|
|
||
|
|
row["score"] < third["score"] ->
|
||
|
|
{g, e}
|
||
|
|
|
||
|
|
true ->
|
||
|
|
row_tied = MapSet.member?(tied_top, row["playerId"])
|
||
|
|
third_tied = MapSet.member?(tied_top, third["playerId"])
|
||
|
|
|
||
|
|
cond do
|
||
|
|
row_tied and third_tied and row["tieBreak"] > third["tieBreak"] -> {g + 1, e}
|
||
|
|
row_tied and third_tied and row["tieBreak"] == third["tieBreak"] -> {g, e + 1}
|
||
|
|
not row_tied or not third_tied -> {g, e + 1}
|
||
|
|
true -> {g, e}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
|
||
|
|
not (greater < 3 and greater + equal > 3)
|
||
|
|
else
|
||
|
|
true
|
||
|
|
end
|
||
|
|
|
||
|
|
%{"required" => true, "resolved" => resolved, "slots" => 0, "playerIds" => player_ids}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp tied_top_for_podium(rows) do
|
||
|
|
rows
|
||
|
|
|> Enum.chunk_by(& &1["score"])
|
||
|
|
|> Enum.reduce({MapSet.new(), 1}, fn chunk, {set, start_pos} ->
|
||
|
|
size = length(chunk)
|
||
|
|
end_pos = start_pos + size - 1
|
||
|
|
score = List.first(chunk)["score"]
|
||
|
|
|
||
|
|
include? =
|
||
|
|
size > 1 and score > 0 and
|
||
|
|
(start_pos <= 3 or end_pos <= 3 or (start_pos < 3 and end_pos > 3))
|
||
|
|
|
||
|
|
next_set =
|
||
|
|
if include? do
|
||
|
|
Enum.reduce(chunk, set, fn row, acc -> MapSet.put(acc, row["playerId"]) end)
|
||
|
|
else
|
||
|
|
set
|
||
|
|
end
|
||
|
|
|
||
|
|
{next_set, end_pos + 1}
|
||
|
|
end)
|
||
|
|
|> elem(0)
|
||
|
|
end
|
||
|
|
|
||
|
|
defp empty_tie, do: %{"required" => false, "resolved" => true, "slots" => 0, "playerIds" => []}
|
||
|
|
|
||
|
|
defp score_total(scores, player_id, stages) do
|
||
|
|
Enum.reduce(stages, 0, fn stage, acc -> acc + get_score(scores, stage, player_id) end)
|
||
|
|
end
|
||
|
|
|
||
|
|
defp get_score(scores, stage, player_id) do
|
||
|
|
stage_map = Map.get(scores, stage, %{})
|
||
|
|
Map.get(stage_map, player_id, 0)
|
||
|
|
end
|
||
|
|
|
||
|
|
defp assign_dense_rank_by_score(rows) do
|
||
|
|
assign_dense_rank(rows, fn a, b -> a["score"] == b["score"] end)
|
||
|
|
end
|
||
|
|
|
||
|
|
defp assign_dense_rank([], _equal_fun), do: []
|
||
|
|
|
||
|
|
defp assign_dense_rank(rows, equal_fun) do
|
||
|
|
{ranked_rev, _prev, _rank, _idx} =
|
||
|
|
Enum.reduce(rows, {[], nil, 1, 0}, fn row, {acc, prev, rank, idx} ->
|
||
|
|
idx1 = idx + 1
|
||
|
|
|
||
|
|
next_rank =
|
||
|
|
if is_map(prev) and equal_fun.(row, prev) do
|
||
|
|
rank
|
||
|
|
else
|
||
|
|
idx1
|
||
|
|
end
|
||
|
|
|
||
|
|
{[%{row | "rank" => next_rank} | acc], row, next_rank, idx1}
|
||
|
|
end)
|
||
|
|
|
||
|
|
Enum.reverse(ranked_rev)
|
||
|
|
end
|
||
|
|
end
|