Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into gh-426-implement-dung…
Browse files Browse the repository at this point in the history
…eon-mode
  • Loading branch information
lotuuu committed May 14, 2024
2 parents 3f7c496 + fb0e523 commit 9f32289
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 42 deletions.
1 change: 1 addition & 0 deletions apps/arena/lib/arena/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Arena.Application do
{Finch, name: Arena.Finch},
# Start game launcher genserver
Arena.GameLauncher,
Arena.GameTracker,
# Start a worker by calling: Arena.Worker.start_link(arg)
# {Arena.Worker, arg},
# Start to serve requests, typically the last entry
Expand Down
118 changes: 118 additions & 0 deletions apps/arena/lib/arena/game_tracker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
defmodule Arena.GameTracker do
@moduledoc """
This module is in charge of tracking the different game changes that we consider
datapoints and use for quests, analytics, etc.
`GameTracker` will behave similar to a metrics collector, but with a push model rather than pull.
Games will push the data to it and at the end GameTracker will push the data to Gateway for storing
"""
use GenServer

def start_link(_args) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end

def start_tracking(match_id, client_to_player_map, players, human_clients) do
GenServer.call(__MODULE__, {:start_tracking, match_id, client_to_player_map, players, human_clients})
end

def finish_tracking(match_pid, winner_id) do
GenServer.cast(__MODULE__, {:finish_tracking, match_pid, winner_id})
end

## TODO: define events structs or pattern
## https://github.com/lambdaclass/mirra_backend/issues/601
def push_event(match_pid, event) do
GenServer.cast(__MODULE__, {:push_event, match_pid, event})
end

##########################
# Callbacks
##########################
@impl true
def init(_) do
{:ok, %{matches: %{}}}
end

@impl true
def handle_call({:start_tracking, match_id, client_to_player_map, players, human_clients}, {match_pid, _}, state) do
player_to_client =
Map.new(client_to_player_map, fn {client_id, player_id} -> {player_id, client_id} end)

players =
Map.new(players, fn {_, player} ->
player_data = %{
id: player.id,
controller: if(Enum.member?(human_clients, player_to_client[player.id]), do: :human, else: :bot),
character: player.aditional_info.character_name,
kills: [],
death: nil
}

{player.id, player_data}
end)

match_state = %{
match_id: match_id,
players: players,
player_to_client: player_to_client,
position_on_death: map_size(players)
}

state = put_in(state, [:matches, match_pid], match_state)
{:reply, :ok, state}
end

@impl true
def handle_cast({:finish_tracking, match_pid, winner_id}, state) do
match_data = get_in(state, [:matches, match_pid])
results = generate_results(match_data, winner_id)
payload = Jason.encode!(%{results: results})
## TODO: Handle errors and retry sending
## https://github.com/lambdaclass/mirra_backend/issues/601
send_request("/arena/match/#{match_data.match_id}", payload)

matches = Map.delete(state.matches, match_pid)
{:noreply, %{state | matches: matches}}
end

def handle_cast({:push_event, match_pid, event}, state) do
state = update_in(state, [:matches, match_pid], fn data -> update_data(data, event) end)
{:noreply, state}
end

defp update_data(data, {:kill, killer, victim}) do
data
|> update_in([:players, killer.id, :kills], fn kills -> kills ++ [victim.character_name] end)
|> put_in([:players, victim.id, :death], killer.character_name)
|> put_in([:players, victim.id, :position], data.position_on_death)
|> put_in([:position_on_death], data.position_on_death - 1)
end

defp generate_results(match_data, winner_id) do
Enum.filter(match_data.players, fn {_player_id, player_data} -> player_data.controller == :human end)
|> Enum.map(fn {_player_id, player_data} ->
%{
user_id: get_in(match_data, [:player_to_client, player_data.id]),
## TODO: way to track `abandon`, currently a bot taking over will endup with a result
## GameUpdater should send an event when the abandon happens to mark the player
## https://github.com/lambdaclass/mirra_backend/issues/601
result: if(player_data.id == winner_id, do: "win", else: "loss"),
kills: length(player_data.kills),
## TODO: this only works because you can only die once
## https://github.com/lambdaclass/mirra_backend/issues/601
deaths: if(player_data.death == nil, do: 0, else: 1),
character: player_data.character,
position: player_data.position
}
end)
end

defp send_request(path, payload) do
gateway_url = Application.get_env(:arena, :gateway_url)

Finch.build(:post, "#{gateway_url}#{path}", [{"content-type", "application/json"}], payload)
## We might want to change this to `Finch.async_request/2`, but let's measure the impact first
|> Finch.request(Arena.Finch)
end
end
55 changes: 21 additions & 34 deletions apps/arena/lib/arena/game_updater.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Arena.GameUpdater do
"""

use GenServer
alias Arena.GameTracker
alias Arena.Game.Crate
alias Arena.Game.Effect
alias Arena.{Configuration, Entities}
Expand Down Expand Up @@ -40,13 +41,24 @@ defmodule Arena.GameUpdater do
game_id = self() |> :erlang.term_to_binary() |> Base58.encode()
game_config = Configuration.get_game_config()
game_state = new_game(game_id, clients ++ bot_clients, game_config)
match_id = Ecto.UUID.generate()

send(self(), :update_game)
Process.send_after(self(), :game_start, game_config.game.start_game_time_ms)

clients_ids = Enum.map(clients, fn {client_id, _, _, _} -> client_id end)
bot_clients_ids = Enum.map(bot_clients, fn {client_id, _, _, _} -> client_id end)
{:ok, %{clients: clients_ids, bot_clients: bot_clients_ids, game_config: game_config, game_state: game_state}}

:ok = GameTracker.start_tracking(match_id, game_state.client_to_player_map, game_state.players, clients_ids)

{:ok,
%{
match_id: match_id,
clients: clients_ids,
bot_clients: bot_clients_ids,
game_config: game_config,
game_state: game_state
}}
end

##########################
Expand Down Expand Up @@ -171,7 +183,7 @@ defmodule Arena.GameUpdater do

PubSub.broadcast(Arena.PubSub, state.game_state.game_id, :end_game_state)
broadcast_game_ended(winner, state.game_state)
report_game_results(state, winner.id)
GameTracker.finish_tracking(self(), winner.id)

## The idea of having this waiting period is in case websocket processes keep
## sending messages, this way we give some time before making them crash
Expand Down Expand Up @@ -328,6 +340,7 @@ defmodule Arena.GameUpdater do
) do
entry = %{killer_id: killer_id, victim_id: victim_id}
victim = Map.get(game_state.players, victim_id)
killer = Map.get(game_state.players, killer_id)

amount_of_power_ups = get_amount_of_power_ups(victim, game_config.power_ups.power_ups_per_kill)

Expand All @@ -342,6 +355,12 @@ defmodule Arena.GameUpdater do

broadcast_player_dead(state.game_state.game_id, victim_id)

GameTracker.push_event(
self(),
{:kill, %{id: killer.id, character_name: killer.aditional_info.character_name},
%{id: victim.id, character_name: victim.aditional_info.character_name}}
)

{:noreply, %{state | game_state: game_state}}
end

Expand Down Expand Up @@ -494,38 +513,6 @@ defmodule Arena.GameUpdater do
|> Map.put(:aditional_info, entity |> Entities.maybe_add_custom_info())
end

defp report_game_results(state, winner_id) do
results =
Map.take(state.game_state.client_to_player_map, state.clients)
|> Enum.map(fn {client_id, player_id} ->
player = Map.get(state.game_state.players, player_id)

%{
user_id: client_id,
## TODO: way to track `abandon`, currently a bot taking over will endup with a result
result: if(player.id == winner_id, do: "win", else: "loss"),
kills: player.aditional_info.kill_count,
## TODO: this only works because you can only die once
deaths: if(Player.alive?(player), do: 0, else: 1),
character: player.aditional_info.character_name,
match_id: Ecto.UUID.generate(),
position: Map.get(state.game_state.positions, client_id)
}
end)

payload = Jason.encode!(%{results: results})

## TODO: we should be doing this in a better way, both the url and the actual request
## maybe a separate GenServer that gets the results and tries to send them to the server?
## This way if it fails we can retry or something
spawn(fn ->
gateway_url = Application.get_env(:arena, :gateway_url)

Finch.build(:post, "#{gateway_url}/arena/match", [{"content-type", "application/json"}], payload)
|> Finch.request(Arena.Finch)
end)
end

##########################
# End broadcast
##########################
Expand Down
15 changes: 10 additions & 5 deletions apps/game_backend/lib/game_backend/matches.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@ defmodule GameBackend.Matches do
alias GameBackend.Users
alias GameBackend.Users.Currencies
alias Ecto.Multi
alias GameBackend.Repo
alias GameBackend.Matches.ArenaMatchResult
alias GameBackend.Repo

def create_arena_match_results(results) do
def create_arena_match_results(match_id, results) do
currency_config = Application.get_env(:game_backend, :currencies_config)

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"])

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)
amount = get_amount_of_trophies_to_modify(amount_of_trophies, result["position"], currency_config)

Multi.insert(transaction_acc, {:insert, result["user_id"]}, changeset)
|> Multi.run(
Expand All @@ -37,6 +36,12 @@ defmodule GameBackend.Matches do
|> Repo.transaction()
end

## TODO: Properly pre-process `currencies_config` so the keys are integers and we don't need convertion
## 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)
end

def get_amount_of_trophies_to_modify(current_trophies, position, currencies_config) do
Enum.sort_by(
get_in(currencies_config, ["ranking_system", "ranks"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ defmodule GameBackend.Matches.ArenaMatchResult do
|> validate_number(:kills, greater_than_or_equal_to: 0)
|> validate_number(:deaths, greater_than_or_equal_to: 0)
|> validate_inclusion(:result, ["win", "loss", "abandon"])
## TODO: This enums should actually be read from config
## https://github.com/lambdaclass/mirra_backend/issues/601
|> validate_inclusion(:character, ["h4ck", "muflus", "uma"])
|> foreign_key_constraint(:user_id)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ defmodule Gateway.Controllers.Arena.MatchResultsController do
use Gateway, :controller
alias GameBackend.Matches

def create(conn, %{"results" => results}) do
case Matches.create_arena_match_results(results) do
def create(conn, %{"match_id" => match_id, "results" => results}) do
case Matches.create_arena_match_results(match_id, results) do
{:ok, _match_result} ->
send_resp(conn, 201, "")

Expand Down
2 changes: 1 addition & 1 deletion apps/gateway/lib/gateway/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ defmodule Gateway.Router do
scope "/arena", Gateway.Controllers.Arena do
pipe_through :api

post "/match", MatchResultsController, :create
post "/match/:match_id", MatchResultsController, :create
end

scope "/", Gateway do
Expand Down

0 comments on commit 9f32289

Please sign in to comment.