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-917] - Deathmatch mode #980

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions apps/arena/lib/arena/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule Arena.Application do
Arena.Matchmaking.GameLauncher,
Arena.Matchmaking.PairMode,
Arena.Matchmaking.QuickGameMode,
Arena.Matchmaking.DeathmatchMode,
Arena.GameBountiesFetcher,
Arena.GameTracker,
Arena.Authentication.GatewaySigner,
Expand Down
8 changes: 8 additions & 0 deletions apps/arena/lib/arena/game/player.ex
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,14 @@ defmodule Arena.Game.Player do
end)
end

def respawn_player(player, position) do
aditional_info = player.aditional_info |> Map.put(:health, player.aditional_info.base_health)

player
|> Map.put(:aditional_info, aditional_info)
|> Map.put(:position, position)
end

####################
# Internal helpers #
####################
Expand Down
11 changes: 11 additions & 0 deletions apps/arena/lib/arena/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ defmodule Arena.GameSocketHandler do
end
end

def websocket_info({:respawn_player, player_id}, state) do
state =
if state.player_id == player_id do
state |> Map.put(:enable, true) |> Map.put(:player_alive, true)
else
state
end

{:ok, state}
end

@impl true
def websocket_info({:block_actions, player_id, value}, state) do
if state.player_id == player_id do
Expand Down
87 changes: 85 additions & 2 deletions apps/arena/lib/arena/game_updater.ex
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ defmodule Arena.GameUpdater do
|> activate_trap_mechanics()
# Obstacles
|> handle_obstacles_transitions()
# Deathmatch
|> add_players_to_respawn_queue(state.game_config)
|> respawn_players(state.game_config)

{:ok, state_diff} = diff(state.last_broadcasted_game_state, game_state)

Expand Down Expand Up @@ -308,7 +311,12 @@ defmodule Arena.GameUpdater do
Process.send_after(self(), :match_timeout, state.game_config.game.match_timeout_ms)

send(self(), :natural_healing)
send(self(), {:end_game_check, Map.keys(state.game_state.players)})

if state.game_config.game.game_mode != :DEATHMATCH do
send(self(), {:end_game_check, Map.keys(state.game_state.players)})
else
Process.send_after(self(), :deathmatch_end_game_check, state.game_config.game.match_duration)
end

unless state.game_config.game.bots_enabled do
toggle_bots(self())
Expand All @@ -317,6 +325,36 @@ defmodule Arena.GameUpdater do
{:noreply, put_in(state, [:game_state, :status], :RUNNING)}
end

def handle_info(:deathmatch_end_game_check, state) do
players =
state.game_state.players
|> Enum.map(fn {player_id, player} ->
%{kills: kills} = GameTracker.get_player_result(player_id)
{player_id, player, kills}
end)
|> Enum.sort_by(fn {_player_id, _player, kills} -> kills end, :desc)

{winner_id, winner, _kills} = Enum.at(players, 0)

state =
state
|> put_in([:game_state, :status], :ENDED)
|> update_in([:game_state], fn game_state ->
players
|> Enum.reduce(game_state, fn {player_id, _player, _kills}, game_state_acc ->
put_player_position(game_state_acc, player_id)
end)
end)

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

Process.send_after(self(), :game_ended, state.game_config.game.shutdown_game_wait_ms)

{:noreply, state}
end

def handle_info({:end_game_check, last_players_ids}, state) do
case check_game_ended(state.game_state.players, last_players_ids) do
{:ongoing, players_ids} ->
Expand Down Expand Up @@ -741,6 +779,10 @@ defmodule Arena.GameUpdater do
PubSub.broadcast(Arena.PubSub, state.game_id, {:game_finished, encoded_state})
end

defp broadcast_player_respawn(game_id, player_id) do
PubSub.broadcast(Arena.PubSub, game_id, {:respawn_player, player_id})
end

defp complete_entities(nil, _), do: []

defp complete_entities(entities, category) do
Expand Down Expand Up @@ -795,7 +837,7 @@ defmodule Arena.GameUpdater do
|> Map.put(:square_wall, config.map.square_wall)
|> Map.put(:zone, %{
radius: config.map.radius - 5000,
should_start?: config.game.zone_enabled,
should_start?: if(config.game.game_mode == :DEATHMATCH, do: false, else: config.game.zone_enabled),
started: false,
enabled: false,
shrinking: false,
Expand All @@ -810,6 +852,7 @@ defmodule Arena.GameUpdater do
)
|> Map.put(:positions, %{})
|> Map.put(:traps, %{})
|> Map.put(:respawn_queue, %{})

{game, _} =
Enum.reduce(clients, {new_game, config.map.initial_positions}, fn {client_id, character_name, player_name,
Expand Down Expand Up @@ -1873,6 +1916,46 @@ defmodule Arena.GameUpdater do
end
end

defp add_players_to_respawn_queue(game_state, %{game: %{game_mode: :DEATHMATCH}} = game_config) do
now = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

respawn_queue =
Enum.reduce(game_state.players, game_state.respawn_queue, fn {player_id, player}, respawn_queue ->
if Map.has_key?(respawn_queue, player_id) || Player.alive?(player) do
respawn_queue
else
Map.put(respawn_queue, player_id, now + game_config.game.respawn_time)
end
end)

Map.put(game_state, :respawn_queue, respawn_queue)
end

defp add_players_to_respawn_queue(game_state, _game_config), do: game_state

defp respawn_players(game_state, %{game: %{game_mode: :DEATHMATCH}} = game_config) do
now = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

players_to_respawn =
game_state.respawn_queue
|> Enum.filter(fn {_player_id, time} ->
time < now
end)

{game_state, respawn_queue} =
Enum.reduce(players_to_respawn, {game_state, game_state.respawn_queue}, fn {player_id, _time},
{game_state, respawn_queue} ->
new_position = Enum.random(game_config.map.initial_positions)
player = Map.get(game_state.players, player_id) |> Player.respawn_player(new_position)
broadcast_player_respawn(game_state.game_id, player_id)
{put_in(game_state, [:players, player_id], player), Map.delete(respawn_queue, player_id)}
end)

Map.put(game_state, :respawn_queue, respawn_queue)
end

defp respawn_players(game_state, _game_config), do: game_state

##########################
# End Helpers
##########################
Expand Down
1 change: 1 addition & 0 deletions apps/arena/lib/arena/matchmaking.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ defmodule Arena.Matchmaking do
def get_queue("battle-royale"), do: Arena.Matchmaking.GameLauncher
def get_queue("pair"), do: Arena.Matchmaking.PairMode
def get_queue("quick-game"), do: Arena.Matchmaking.QuickGameMode
def get_queue("deathmatch"), do: Arena.Matchmaking.DeathmatchMode
def get_queue(:undefined), do: Arena.Matchmaking.GameLauncher
end
139 changes: 139 additions & 0 deletions apps/arena/lib/arena/matchmaking/deathmatch_mode.ex
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have a matchmaking file for each game mode

Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
defmodule Arena.Matchmaking.DeathmatchMode do
@moduledoc false
alias Arena.Utils
alias Ecto.UUID

use GenServer

# 3 Mins
# TODO: add this to the configurator https://github.com/lambdaclass/mirra_backend/issues/985
@match_duration 180_000
@respawn_time 5000

# Time to wait to start game with any amount of clients
@start_timeout_ms 4_000

# API
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end

def join(client_id, character_name, player_name) do
GenServer.call(__MODULE__, {:join, client_id, character_name, player_name})
end

def leave(client_id) do
GenServer.call(__MODULE__, {:leave, client_id})
end

# Callbacks
@impl true
def init(_) do
Process.send_after(self(), :launch_game?, 300)
{:ok, %{clients: [], batch_start_at: 0}}
end

@impl true
def handle_call({:join, client_id, character_name, player_name}, {from_pid, _}, %{clients: clients} = state) do
batch_start_at = maybe_make_batch_start_at(state.clients, state.batch_start_at)

{:reply, :ok,
%{
state
| batch_start_at: batch_start_at,
clients: clients ++ [{client_id, character_name, player_name, from_pid}]
}}
end

def handle_call({:leave, client_id}, _, state) do
clients = Enum.reject(state.clients, fn {id, _, _, _} -> id == client_id end)
{:reply, :ok, %{state | clients: clients}}
end

@impl true
def handle_info(:launch_game?, %{clients: clients} = state) do
Process.send_after(self(), :launch_game?, 300)
diff = System.monotonic_time(:millisecond) - state.batch_start_at

if length(clients) >= Application.get_env(:arena, :players_needed_in_match) or
(diff >= @start_timeout_ms and length(clients) > 0) do
send(self(), :start_game)
end

{:noreply, state}
end

def handle_info(:start_game, state) do
{game_clients, remaining_clients} = Enum.split(state.clients, Application.get_env(:arena, :players_needed_in_match))
create_game_for_clients(game_clients)

{:noreply, %{state | clients: remaining_clients}}
end

def handle_info({:spawn_bot_for_player, bot_client, game_id}, state) do
spawn(fn ->
Finch.build(:get, Utils.get_bot_connection_url(game_id, bot_client))
|> Finch.request(Arena.Finch)
end)

{:noreply, state}
end

defp maybe_make_batch_start_at([], _) do
System.monotonic_time(:millisecond)
end

defp maybe_make_batch_start_at([_ | _], batch_start_at) do
batch_start_at
end

defp spawn_bot_for_player(bot_clients, game_id) do
Enum.each(bot_clients, fn {bot_client, _, _, _} ->
send(self(), {:spawn_bot_for_player, bot_client, game_id})
end)
end

defp get_bot_clients(missing_clients) do
characters =
Arena.Configuration.get_game_config()
|> Map.get(:characters)
|> Enum.filter(fn character -> character.active end)

Enum.map(1..missing_clients//1, fn i ->
client_id = UUID.generate()

{client_id, Enum.random(characters).name, Enum.at(Arena.Utils.bot_names(), i), nil}
end)
end

# Receives a list of clients.
# Fills the given list with bots clients, creates a game and tells every client to join that game.
defp create_game_for_clients(clients, game_params \\ %{}) do
# We spawn bots only if there is one player
bot_clients =
case Enum.count(clients) do
1 -> get_bot_clients(Application.get_env(:arena, :players_needed_in_match) - Enum.count(clients))
_ -> []
end

{:ok, game_pid} =
GenServer.start(Arena.GameUpdater, %{
clients: clients,
bot_clients: bot_clients,
game_params:
game_params
|> Map.put(:game_mode, :DEATHMATCH)
|> Map.put(:match_duration, @match_duration)
|> Map.put(:respawn_time, @respawn_time)
})

game_id = game_pid |> :erlang.term_to_binary() |> Base58.encode()

spawn_bot_for_player(bot_clients, game_id)

Enum.each(clients, fn {_client_id, _character_name, _player_name, from_pid} ->
Process.send(from_pid, {:join_game, game_id}, [])
Process.send(from_pid, :leave_waiting_game, [])
end)
end
end
23 changes: 2 additions & 21 deletions apps/arena/lib/arena/matchmaking/game_launcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,6 @@ defmodule Arena.Matchmaking.GameLauncher do
# Time to wait to start game with any amount of clients
@start_timeout_ms 4_000

@bot_names [
"TheBlackSwordman",
"SlashJava",
"SteelBallRun",
"Jeff",
"Messi",
"Stone Ocean",
"Jeepers Creepers",
"Bob",
"El javo",
"Alberso",
"Thomas",
"Timmy",
"Pablito",
"Nicolino",
"Cangrejo",
"Mansito"
]

# API
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
Expand Down Expand Up @@ -116,7 +97,7 @@ defmodule Arena.Matchmaking.GameLauncher do
Enum.map(1..missing_clients//1, fn i ->
client_id = UUID.generate()

{client_id, Enum.random(characters).name, Enum.at(@bot_names, i), nil}
{client_id, Enum.random(characters).name, Enum.at(Arena.Utils.bot_names(), i), nil}
end)
end

Expand All @@ -134,7 +115,7 @@ defmodule Arena.Matchmaking.GameLauncher do
GenServer.start(Arena.GameUpdater, %{
clients: clients,
bot_clients: bot_clients,
game_params: game_params
game_params: game_params |> Map.put(:game_mode, :BATTLE)
})

game_id = game_pid |> :erlang.term_to_binary() |> Base58.encode()
Expand Down
Loading
Loading