Skip to content

Commit

Permalink
[GH-917] - Deathmatch mode (#980)
Browse files Browse the repository at this point in the history
* feat: enable deathmatch matchmaking

* feat: add end game check for deathmatch

* feat: add deathmatch logic into the game updater

* feat: show deathmatch mode in the browser

* fix: input when respawning was not working

* fix: win condition

* fix: battle royale was broken

* fix: add mising parameter in pair mode matchmaking

* feat: increase deathmatch game time

* feat: send the game mode to the client

* fix: players were not respawning

* chore: requested changes

* fix: typo in deathmatch

* feat: centralize bot names

* fix: another wrong check and fix player positions

* chore: mix format

* chore: increase arena version

* chore: update arena version again
  • Loading branch information
tvillegas98 authored Nov 28, 2024
1 parent cd99c1b commit 413e90b
Show file tree
Hide file tree
Showing 19 changed files with 719 additions and 407 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 @@ -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
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

0 comments on commit 413e90b

Please sign in to comment.