I’m trying to hack together an application that runs tournaments: players can join tournaments, 1-on-1 matches are played between the tournament participants and they get rated based on the results.
Since this application is my first usage of Ash, there are many things that are not yet clear to me. At the moment, I’ve gotten to the point where the most important resources have gotten either skeleton or basic implementations, but I’ve hit a point where I can’t quite figure out how I would write some functionality in an “idiomatic Ash way”.
Code
The code that I haven’t figured out how to write is located in the function create_ratings_for_match_result_participants/1
. The rest of the Rating
module is rather basic Ash code.
defmodule Cassis.Tournaments.Rating do
use Ash.Resource,
domain: Cassis.Tournaments,
data_layer: AshPostgres.DataLayer
require Ash.Resource.Attribute.Helpers
postgres do
table "ratings"
repo Cassis.Repo
end
actions do
defaults [:read]
create :create do
accept [:type, :value]
# accept [:type]
argument :participant, :map do
allow_nil? false
end
argument :result, :map do
allow_nil? false
end
change manage_relationship(:participant, type: :append_and_remove)
change manage_relationship(:result, type: :append_and_remove)
end
end
attributes do
uuid_primary_key :id
timestamps()
attribute :value, :float, allow_nil?: false
attribute :type, :atom do
allow_nil? false
constraints one_of: [:elo]
default :elo
end
end
relationships do
belongs_to :participant, Cassis.Tournaments.Participant
belongs_to :result, Cassis.Tournaments.Result
end
def create_ratings_for_match_result_participants(%Result{} = result) do
result = Ash.load!(result, match: [:participants])
[match_participant_a, match_participant_b] = result.match.participants
[
participant_a_rating_value,
participant_b_rating_value
] =
Enum.map(
[
match_participant_a,
match_participant_b
],
&latest_rating_value_or_generate_default_value/1
)
a_vs_b_outcome =
cond do
result.draw? -> :draw
result.winner.id == match_participant_a.id -> :win
result.loser.id == match_participant_a.id -> :loss
end
{
new_participant_a_rating_value,
new_participant_b_rating_value
} =
Elo.rate(
participant_a_rating_value,
participant_b_rating_value,
a_vs_b_outcome
)
__MODULE__
|> Ash.Changeset.for_create(:create, %{
type: :elo,
value: new_participant_a_rating_value,
result: result,
participant: match_participant_a
})
|> Ash.create!()
__MODULE__
|> Ash.Changeset.for_create(:create, %{
type: :elo,
value: new_participant_b_rating_value,
result: result,
participant: match_participant_b
})
|> Ash.create!()
:ok
end
defp latest_rating_value_or_generate_default_value(match_participant) do
Rating
|> Ash.Query.filter(participant_id == ^match_participant.id)
|> Ash.Query.sort(inserted_at: :desc)
|> Ash.Query.limit(1)
|> Ash.read!()
|> case do
[latest_rating] -> latest_rating.value
[] -> default_elo_rating()
end
end
defp default_elo_rating, do: 1200
end
Comments
- The current version of the
create :create
action isaccept
ing a:value
, but the intention is that thevalue
should only be set through the procedure described increate_ratings_for_match_result_participants
. In other words:accept [:type, :value]
should beaccept [:type]
, but I’m not there yet, and I needvalue
for testing (I guess I could have a separate action that allows this). constraints one_of: [:elo]
is there because at some point I’m adding another rating algorithm.
Questions
- What would be the best way to transform the logic of
create_ratings_for_match_result_participants
to an idiomatic Ash action? - Considering the resources that are being called in
create_ratings_for_match_result_participants
, is it best to place the resulting action inRating
, or maybe in one of the other resources, e.g.Result
? - Any obvious problems with the code that maybe also need to be addressed?