Skip to content

Commit

Permalink
Apply time limit and cutoff when entering results via API
Browse files Browse the repository at this point in the history
  • Loading branch information
jonatanklosko committed Nov 25, 2023
1 parent 5413b4f commit 39198ad
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 13 deletions.
3 changes: 2 additions & 1 deletion lib/wca_live/scoretaking.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ defmodule WcaLive.Scoretaking do

format = Format.get_by_id!(round.format_id)
event_id = round.competition_event.event_id
time_limit = round.time_limit
cutoff = round.cutoff

results_with_attrs =
Expand All @@ -134,7 +135,7 @@ defmodule WcaLive.Scoretaking do
Enum.reduce(results_with_attrs, Multi.new(), fn {result, attempts, entered_at}, multi ->
Multi.update(multi, {:updated_result, result.id}, fn _changes ->
result
|> Result.changeset(%{attempts: attempts}, event_id, format, cutoff)
|> Result.changeset(%{attempts: attempts}, event_id, format, time_limit, cutoff)
|> Changeset.put_change(:entered_by_id, user.id)
|> Changeset.put_change(:entered_at, DateTime.truncate(entered_at, :second))
end)
Expand Down
100 changes: 88 additions & 12 deletions lib/wca_live/scoretaking/result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,95 @@ defmodule WcaLive.Scoretaking.Result do
timestamps()
end

def changeset(result, attrs, event_id, format, cutoff) do
result
|> cast(attrs, @required_fields ++ @optional_fields)
|> cast_embed(:attempts)
|> compute_best_and_average(event_id, format)
|> validate_required(@required_fields)
|> validate_length(:attempts, max: format.number_of_attempts)
|> validate_no_trailing_skipped()
|> validate_not_all_dns(format, cutoff)
def changeset(result, attrs, event_id, format, time_limit, cutoff) do
changeset =
result
|> cast(attrs, @required_fields ++ @optional_fields)
|> cast_embed(:attempts)
|> validate_required(@required_fields)
|> validate_length(:attempts, max: format.number_of_attempts)
|> validate_no_trailing_skipped()
|> validate_not_all_dns(format, cutoff)

# Run further computations only if attempts are valid
if changeset.valid? do
changeset
|> apply_time_limit_and_cutoff(time_limit, cutoff)
|> compute_best_and_average(event_id, format)
else
changeset
end
end

# Note that we already have the same logic on the client, but we
# apply it on the server to make sure the data is consistent when
# entering results with direct API calls
defp apply_time_limit_and_cutoff(changeset, time_limit, cutoff) do
attempts = get_field(changeset, :attempts)

attempts =
attempts
|> apply_time_limit(time_limit)
|> apply_cutoff(cutoff)

put_embed(changeset, :attempts, attempts)
end

defp apply_time_limit(attempts, nil), do: attempts

defp apply_time_limit(attempts, %{cumulative_round_wcif_ids: []} = time_limit) do
Enum.map(attempts, fn attempt ->
if attempt.result >= time_limit.centiseconds do
put_in(attempt.result, AttemptResult.dnf())
else
attempt
end
end)
end

defp apply_time_limit(attempts, time_limit) do
# Note: for now cross-round cumulative time limits are handled
# as single-round cumulative time limits for each of the rounds

{attempts, _sum} =
Enum.map_reduce(attempts, 0, fn attempt, sum ->
sum =
if attempt.result > 0 do
sum + attempt.result
else
sum
end

attempt =
if attempt.result > 0 and sum >= time_limit.centiseconds do
put_in(attempt.result, AttemptResult.dnf())
else
attempt
end

{attempt, sum}
end)

attempts
end

defp apply_cutoff(attempts, nil), do: attempts

defp apply_cutoff(attempts, cutoff) do
meets_cutoff? =
attempts
|> Enum.take(cutoff.number_of_attempts)
|> Enum.any?(&AttemptResult.better?(&1.result, cutoff.attempt_result))

if meets_cutoff? do
attempts
else
Enum.take(attempts, cutoff.number_of_attempts)
end
end

defp compute_best_and_average(changeset, event_id, format) do
%{attempts: attempts} = apply_changes(changeset)
attempts = get_field(changeset, :attempts)

attempt_results =
attempts
Expand All @@ -68,7 +144,7 @@ defmodule WcaLive.Scoretaking.Result do
end

defp validate_no_trailing_skipped(changeset) do
%{attempts: attempts} = apply_changes(changeset)
attempts = get_field(changeset, :attempts)
last_attempt = List.last(attempts)

if last_attempt && AttemptResult.skipped?(last_attempt.result) do
Expand All @@ -79,7 +155,7 @@ defmodule WcaLive.Scoretaking.Result do
end

defp validate_not_all_dns(changeset, format, cutoff) do
%{attempts: attempts} = apply_changes(changeset)
attempts = get_field(changeset, :attempts)
attempt_results = Enum.map(attempts, & &1.result)

max_incomplete_attempts =
Expand Down
87 changes: 87 additions & 0 deletions test/wca_live/scoretaking_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,93 @@ defmodule WcaLive.ScoretakingTest do
assert {:ok, _round} = Scoretaking.enter_results(round, attrs, user)
end

test "enter_results/3 applies non-cumulative time limit" do
user = insert(:user)

round =
insert(:round,
time_limit: build(:time_limit, centiseconds: 1250, cumulative_round_wcif_ids: [])
)

result = insert(:result, round: round, attempts: [])

attrs = [
%{
id: result.id,
attempts: [
%{result: 1000},
%{result: 1250},
%{result: 1100},
%{result: 1300}
],
entered_at: DateTime.utc_now()
}
]

assert {:ok, _round} = Scoretaking.enter_results(round, attrs, user)
result = Repo.reload(result)
assert [1000, -1, 1100, -1] == Enum.map(result.attempts, & &1.result)
end

test "enter_results/3 applies single-round cumulative time limit" do
user = insert(:user)

competition = insert(:competition)
ce333bf = insert(:competition_event, competition: competition, event_id: "333bf")

round =
insert(:round,
competition_event: ce333bf,
time_limit: build(:time_limit, centiseconds: 20000, cumulative_round_wcif_ids: ["333bf"])
)

result = insert(:result, round: round, attempts: [])

attrs = [
%{
id: result.id,
attempts: [
%{result: 3000},
%{result: 12000},
%{result: 5000}
],
entered_at: DateTime.utc_now()
}
]

assert {:ok, _round} = Scoretaking.enter_results(round, attrs, user)
result = Repo.reload(result)
assert [3000, 12000, -1] == Enum.map(result.attempts, & &1.result)
end

test "enter_results/3 applies cutoff" do
user = insert(:user)

round =
insert(:round,
cutoff: build(:cutoff, number_of_attempts: 2, attempt_result: 800),
format_id: "a"
)

result = insert(:result, round: round, attempts: [])

attrs = [
%{
id: result.id,
attempts: [
%{result: 1000},
%{result: 800},
%{result: 1200}
],
entered_at: DateTime.utc_now()
}
]

assert {:ok, _round} = Scoretaking.enter_results(round, attrs, user)
result = Repo.reload(result)
assert [1000, 800] == Enum.map(result.attempts, & &1.result)
end

test "enter_result_attempt/5 adds skipped attempts when necessary" do
user = insert(:user)
round = insert(:round, cutoff: nil, format_id: "a")
Expand Down

0 comments on commit 39198ad

Please sign in to comment.