Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-581] Implement daily quest feature #590

Merged
merged 43 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b77f9e9
Add quests tables
agustinesco May 10, 2024
4089830
Implement quest schemas
agustinesco May 10, 2024
17c42bc
Add quest descriptions import
agustinesco May 10, 2024
d9661ed
Create quests module
agustinesco May 10, 2024
99321c4
Add quest creation for users method
agustinesco May 10, 2024
66134c6
Merge branch 'main' into gh-581-implement-daily-quest-feature
agustinesco May 10, 2024
80a0ebd
Rename tables
agustinesco May 10, 2024
55e634c
Rename missing schema
agustinesco May 10, 2024
ca522ee
Start implementing quest progress
agustinesco May 10, 2024
89f1963
Rename quest objective
agustinesco May 10, 2024
7ef676b
Remove unused method
agustinesco May 10, 2024
8789541
Separate conditions from objectives
agustinesco May 10, 2024
1d8e2e8
Merge branch 'main' into gh-581-implement-daily-quest-feature
agustinesco May 12, 2024
d69c1aa
Add user getter for quests
agustinesco May 13, 2024
de61842
Implement quest filterer and upser
agustinesco May 13, 2024
b978c12
Fix arena match result google_user name
agustinesco May 13, 2024
2e8c6c7
Add missing quest fields
agustinesco May 13, 2024
03c611e
Add missing arena match result relation
agustinesco May 13, 2024
ab2c652
Add missing daily quest relation to user
agustinesco May 13, 2024
91f92ae
Change type for field
agustinesco May 13, 2024
fd2ead7
Move macro to the top
agustinesco May 13, 2024
3781881
Add module doc to config
agustinesco May 13, 2024
a2a3a6c
Add completed to user_daily
agustinesco May 13, 2024
98b9921
Give reward when quest complete
agustinesco May 13, 2024
872ac61
Add quest documentation
agustinesco May 13, 2024
29f791d
Add daily quest docu
agustinesco May 13, 2024
656d568
Fix credo complain
agustinesco May 13, 2024
e815671
Refactor matches query
agustinesco May 14, 2024
9fc7adc
Remove completion check
agustinesco May 14, 2024
8160451
Rename column from arena match result in proper migration
agustinesco May 14, 2024
ce9db3b
Remove unnecessary default on quest description
agustinesco May 14, 2024
d4b8d3b
Add missing preload
agustinesco May 14, 2024
d6a3be4
Merge branch 'main' into gh-581-implement-daily-quest-feature
agustinesco May 14, 2024
5c460fb
Merge branch 'main' into gh-581-implement-daily-quest-feature
agustinesco May 15, 2024
ff9fda3
Add null false to quest table
agustinesco May 15, 2024
703677e
Fix match put
agustinesco May 15, 2024
69bab8b
Only add quests if valid
agustinesco May 15, 2024
74e3cd5
Remove completed boolean from daily quest
agustinesco May 15, 2024
370ea37
Fix typos
agustinesco May 15, 2024
889aa6b
Rename field on quest descriptions
agustinesco May 15, 2024
fcbeec2
fFix typo
agustinesco May 15, 2024
79b08f1
Run md through spell checker
agustinesco May 15, 2024
8003548
Merge branch 'main' into gh-581-implement-daily-quest-feature
agustinesco May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/game_backend/docs/configuration/quests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Quests

Quests are a set of conditions that players need to meet by playing matches in order to get rewards (for the time being, currencies). You can specify an objective that's the final goal.

## Configuration

- `config_id`: Unique ID for the quests in the config file.
- `description`: A description of the quest.
- `type`: The duration of the quest. Possible values can be ["daily"].
- `objective`: What the player needs to achieve in order to complete the quest (can be left empty).
- `match_tracking_field`: The match_tracking_field of the arena match result table that will be used as a target.
- `value`: The amount of progress that the player needs to meet to complete the quest.
- `comparison`: Comparator to use against the progress to check quest completion.
- `scope`: Will determine how the progress will be processed to meet quest completion. `match` will only add 1 for each valid match, `day` will sum the numeric value of the field. *Do not use with string fields*.
- `conditions`: A list of conditions to filter if a match is valid in order to add to the progress of quest completion.
- `value`: Value to compare with arena results to check validity.
- `match_tracking_field`: match_tracking_field from the arena result to take to compare.
- `comparison`: Comparator to use against the arena results to check validity.
- `reward`: Reward to give when the user completes.
- `currency`: Currency to give on quest completion.
- `amount`: Amount of currency to give on quest completion.
16 changes: 16 additions & 0 deletions apps/game_backend/lib/game_backend/curse_of_mirra/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule GameBackend.CurseOfMirra.Config do
@moduledoc """
Module to import config to the db related to Curse Of Mirra from json files
"""

alias GameBackend.CurseOfMirra.Quests

def import_quest_descriptions_config() do
{:ok, skills_json} =
Application.app_dir(:game_backend, "priv/curse_of_mirra/quests_descriptions.json")
|> File.read()

Jason.decode!(skills_json, [{:keys, :atoms}])
|> Quests.upsert_quests()
end
end
112 changes: 112 additions & 0 deletions apps/game_backend/lib/game_backend/curse_of_mirra/quests.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
defmodule GameBackend.CurseOfMirra.Quests do
@moduledoc """
Module to work with quest logic
"""
alias GameBackend.Quests.DailyQuest
alias GameBackend.Repo
alias GameBackend.Quests.Quest
alias Ecto.Multi
import Ecto.Query

@doc """
Get a %Quest{} by the config_id field

## Examples

iex>get_quest_by_config_id(4)
%Quest{config_id: 4}

"""
def get_quest_by_config_id(quest_config_id) do
Repo.get_by(Quest, config_id: quest_config_id)
end

@doc """
Get all %Quest{} by the type field that doesn't have a valid daily quest

a valid daily quest means:
- the inserted at is inside the current day period
- It hasn't been completed

## Examples

iex>get_user_missing_quests_by_type(user_id, "daily")
[%Quest{type: "daily"}]
"""
def get_user_missing_quests_by_type(user_id, type) do
naive_today = NaiveDateTime.utc_now()
start_of_date = NaiveDateTime.beginning_of_day(naive_today)
end_of_date = NaiveDateTime.end_of_day(naive_today)

q =
from(q in Quest,
left_join: dq in DailyQuest,
on: q.id == dq.quest_id and dq.user_id == ^user_id,
where:
(is_nil(dq) or dq.inserted_at < ^start_of_date or dq.inserted_at > ^end_of_date or not is_nil(dq.completed_at)) and
Nico-Sanchez marked this conversation as resolved.
Show resolved Hide resolved
q.type == ^type,
distinct: q.id
)

Repo.all(q)
end

@doc """
Run the quest changeset with the given attrs
Return %Changeset{}

## Examples

iex>change_quest(quest, attrs)
%Changeset{}

"""
def change_quest(quest, attrs) do
Quest.changeset(quest, attrs)
end

@doc """
Insert or update config quests present in the "quests_descriptions.json" file
"""
def upsert_quests(quests_params) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you feel curious you can try the actual Ecto Upsert rather than implement your own, but full disclosure I have never used it so no idea if it will work, but would be a nice improvement here if it does

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm if we use the Ecto upsert we should update every other upsert already implemented in the repo, maybe we could do this change in a proper pr

Enum.reduce(quests_params, Multi.new(), fn quest_param, multi ->
case get_quest_by_config_id(quest_param.config_id) do
nil ->
changeset = change_quest(%Quest{}, quest_param)
Multi.insert(multi, quest_param.config_id, changeset)

quest ->
changeset = change_quest(quest, quest_param)
Multi.update(multi, quest_param.config_id, changeset)
end
end)
|> Repo.transaction()
end

def add_quest_to_user_id(user_id, amount, type) do
available_quests =
get_user_missing_quests_by_type(user_id, type)
|> Enum.shuffle()

if amount > Enum.count(available_quests) do
{:error, :not_enough_quests_in_config}
else
{multi, _quests} =
Enum.reduce(1..amount, {Multi.new(), available_quests}, fn
_index, {multi, [quest | next_quests]} ->
attrs = %{
user_id: user_id,
quest_id: quest.id
}

changeset = DailyQuest.changeset(%DailyQuest{}, attrs)

multi = Multi.insert(multi, {:insert_user_quest, user_id, quest.id}, changeset)

{multi, next_quests}
end)

Repo.transaction(multi)
end
end
end
65 changes: 53 additions & 12 deletions apps/game_backend/lib/game_backend/matches.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,58 @@ defmodule GameBackend.Matches do
alias GameBackend.Repo

def create_arena_match_results(match_id, results) do
currency_config = Application.get_env(:game_backend, :currencies_config)
Multi.new()
|> create_arena_match_results(match_id, results)
|> add_google_users_to_multi(results)
|> give_trophies(results)
|> Repo.transaction()
end

Enum.reduce(results, Multi.new(), fn result, transaction_acc ->
result = Map.put(result, "match_id", match_id)
changeset = ArenaMatchResult.changeset(%ArenaMatchResult{}, result)
{:ok, google_user} = Users.get_google_user(result["user_id"])
####################
# Multi operations #
####################

amount_of_trophies = Currencies.get_amount_of_currency_by_name(google_user.user.id, "Trophies")
amount = get_amount_of_trophies_to_modify(amount_of_trophies, result["position"], currency_config)
defp create_arena_match_results(multi, match_id, results) do
Enum.reduce(results, multi, fn result, multi ->
attrs =
Map.put(result, "google_user_id", result["user_id"])
|> Map.put("match_id", match_id)

Multi.insert(transaction_acc, {:insert, result["user_id"]}, changeset)
|> Multi.run(
changeset = ArenaMatchResult.changeset(%ArenaMatchResult{}, attrs)
Multi.insert(multi, {:insert, result["user_id"]}, changeset)
end)
end

defp add_google_users_to_multi(multi, results) do
Multi.run(multi, :get_google_users, fn repo, _changes_so_far ->
google_users =
Enum.map(results, fn result -> result["user_id"] end)
|> Users.get_google_users_with_todays_daily_quests(repo)

{:ok, google_users}
end)
end

defp give_trophies(multi, results) do
currency_config = Application.get_env(:game_backend, :currencies_config)

Enum.reduce(results, multi, fn result, multi ->
Multi.run(
multi,
{:add_trophies_to, result["user_id"]},
fn _, _ ->
fn _, %{get_google_users: google_users} ->
google_user = Enum.find(google_users, fn google_user -> google_user.id == result["user_id"] end)

amount_of_trophies =
Enum.find(google_user.user.currencies, fn user_currency -> user_currency.currency.name == "Trophies" end)
|> case do
nil -> 0
currency -> currency.amount
end

amount =
get_amount_of_trophies_to_modify(amount_of_trophies, result["position"], currency_config)

Currencies.add_currency_by_name_and_game!(
google_user.user.id,
"Trophies",
Expand All @@ -33,10 +71,13 @@ defmodule GameBackend.Matches do
end
)
end)
|> Repo.transaction()
end

## TODO: Properly pre-process `currencies_config` so the keys are integers and we don't need convertion
####################
# Helpers #
####################

## TODO: Properly pre-process `currencies_config` so the keys are integers and we don't need conversion
## https://github.com/lambdaclass/mirra_backend/issues/601
def get_amount_of_trophies_to_modify(current_trophies, position, currencies_config) when is_integer(position) do
get_amount_of_trophies_to_modify(current_trophies, to_string(position), currencies_config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule GameBackend.Matches.ArenaMatchResult do
field(:duration_ms, :integer)
timestamps()

belongs_to(:user, GameBackend.Users.GoogleUser)
belongs_to(:google_user, GameBackend.Users.GoogleUser)
end

@required [
Expand All @@ -30,7 +30,7 @@ defmodule GameBackend.Matches.ArenaMatchResult do
:deaths,
:character,
:match_id,
:user_id,
:google_user_id,
:position,
:damage_done,
:damage_taken,
Expand Down
32 changes: 32 additions & 0 deletions apps/game_backend/lib/game_backend/quests/daily_quest.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule GameBackend.Quests.DailyQuest do
@moduledoc """
Relation between users and quests, will determine if a quest is completed or no
"""

use GameBackend.Schema
import Ecto.Changeset

alias GameBackend.Quests.Quest
alias GameBackend.Users.User

schema "daily_quest" do
field(:completed_at, :utc_datetime)
belongs_to(:quest, Quest)
belongs_to(:user, User)

timestamps()
end

@required [
:quest_id,
:user_id
]

@permitted [:completed_at] ++ @required

def changeset(changeset, attrs) do
changeset
|> cast(attrs, @permitted)
|> validate_required(@required)
end
end
39 changes: 39 additions & 0 deletions apps/game_backend/lib/game_backend/quests/quest.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule GameBackend.Quests.Quest do
@moduledoc """
Quest define objective that users need to accomplish by finish matches that
creates a %GameBackend.Matches.ArenaMatchResult{} used to check if the user
have completed requirements in the :objective field
"""

use GameBackend.Schema
import Ecto.Changeset

schema "quests" do
field(:description, :string)
field(:type, :string)
field(:objective, :map)
field(:reward, :map)
field(:config_id, :integer)
field(:conditions, {:array, :map})

timestamps()
end

@required [
:description,
:objective,
:reward,
:conditions,
:type,
:config_id
]

@permitted [] ++ @required

def changeset(changeset, attrs) do
changeset
|> cast(attrs, @permitted)
|> validate_required(@required)
|> validate_inclusion(:type, ["daily"])
end
end
42 changes: 42 additions & 0 deletions apps/game_backend/lib/game_backend/users.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ defmodule GameBackend.Users do

import Ecto.Query, warn: false

alias GameBackend.Quests.DailyQuest
alias GameBackend.Matches.ArenaMatchResult
alias GameBackend.Users.DungeonSettlementLevel
alias GameBackend.Users.KalineTreeLevel
alias Ecto.Multi
Expand Down Expand Up @@ -56,6 +58,46 @@ defmodule GameBackend.Users do
if user, do: {:ok, user}, else: {:error, :not_found}
end

@doc """
Get a list of GoogleUser based on their id with the necessary preloads
to process daily quests.

## Examples

iex> get_google_users_with_todays_daily_quests(["51646f3a-d9e9-4ce6-8341-c90b8cad3bdf"])
[%GoogleUser{}]
"""
def get_google_users_with_todays_daily_quests(ids, repo \\ Repo) do
naive_today = NaiveDateTime.utc_now()
start_of_date = NaiveDateTime.beginning_of_day(naive_today)
end_of_date = NaiveDateTime.end_of_day(naive_today)

arena_match_result_subquery =
from(amr in ArenaMatchResult,
where: amr.inserted_at > ^start_of_date and amr.inserted_at < ^end_of_date
)

daily_quest_subquery =
from(dq in DailyQuest,
where: dq.inserted_at > ^start_of_date and dq.inserted_at < ^end_of_date and is_nil(dq.completed_at),
preload: [:quest]
)

q =
from(u in GoogleUser,
where: u.id in ^ids,
preload: [
arena_match_results: ^arena_match_result_subquery,
user: [
currencies: :currency,
daily_quests: ^daily_quest_subquery
]
]
)

repo.all(q)
end

@doc """
Gets a single GoogleUser.
Returns {:ok, GoogleUser}.
Expand Down
3 changes: 3 additions & 0 deletions apps/game_backend/lib/game_backend/users/google_user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ defmodule GameBackend.Users.GoogleUser do
use GameBackend.Schema
import Ecto.Changeset

alias GameBackend.Matches.ArenaMatchResult

schema "google_users" do
field(:email, :string)
has_one(:user, GameBackend.Users.User)
has_many(:arena_match_results, ArenaMatchResult)
timestamps()
end

Expand Down
Loading
Loading