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-370] Start implement bot behavior #446

Merged
merged 12 commits into from
Apr 17, 2024
2 changes: 1 addition & 1 deletion apps/arena/lib/arena/serialization/messages.pb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ end
defmodule Arena.Serialization.GameState.ObstaclesEntry do
@moduledoc false

use Protobuf, map: true, protoc_gen_elixir_version: "0.12.0", syntax: :proto3
use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"
lotuuu marked this conversation as resolved.
Show resolved Hide resolved

field(:key, 1, type: :uint64)
field(:value, 2, type: Arena.Serialization.Entity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ defmodule ArenaLoadTest.Serialization.GameState.ItemsEntry do
field(:value, 2, type: ArenaLoadTest.Serialization.Entity)
end

defmodule ArenaLoadTest.Serialization.GameState.ObstaclesEntry do
@moduledoc false

use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

field(:key, 1, type: :uint64)
field(:value, 2, type: ArenaLoadTest.Serialization.Entity)
end

defmodule ArenaLoadTest.Serialization.GameState.PoolsEntry do
@moduledoc false

Expand Down Expand Up @@ -310,7 +319,13 @@ defmodule ArenaLoadTest.Serialization.GameState do
map: true
)

field(:pools, 14,
field(:obstacles, 14,
lotuuu marked this conversation as resolved.
Show resolved Hide resolved
repeated: true,
type: ArenaLoadTest.Serialization.GameState.ObstaclesEntry,
map: true
)

field(:pools, 15,
repeated: true,
type: ArenaLoadTest.Serialization.GameState.PoolsEntry,
map: true
Expand Down
61 changes: 61 additions & 0 deletions apps/bot_manager/lib/bot_state_machine.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
defmodule BotManager.BotStateMachine do
@moduledoc """
This module will take care of deciding what the bot will do on each deciding step
"""

alias BotManager.Utils
alias BotManager.Math.Vector

def decide_action(%{game_state: game_state, bot_player: bot_player}) do
closest_player =
map_directions_to_players(game_state, bot_player)
|> Enum.min_by(fn player_info -> player_info.distance end)

cond do
closest_player.distance > 300 ->
{:move, closest_player.direction}

closest_player.distance < 50 ->
{:move, Vector.mult(closest_player.direction, -1)}

closest_player.distance <= 300 ->
{:attack, closest_player.direction}

true ->
{:move, create_random_direction()}
end
end

def decide_action(_), do: :stand
lotuuu marked this conversation as resolved.
Show resolved Hide resolved

defp create_random_direction() do
Enum.random([
%{x: 1, y: 0},
%{x: 0, y: -1},
%{x: -1, y: 0},
%{x: 0, y: 1}
])
end

defp map_directions_to_players(game_state, bot_player) do
Map.delete(game_state.players, bot_player.id)
|> Map.filter(fn {_player_id, player} -> Utils.player_alive?(player) end)
|> Enum.map(fn {_player_id, player} ->
player_info =
get_distance_and_direction_to_positions(bot_player.position, player.position)

Map.merge(player, player_info)
end)
end

defp get_distance_and_direction_to_positions(base_position, end_position) do
%{x: x, y: y} = Vector.sub(end_position, base_position)
distance = :math.sqrt(:math.pow(x, 2) + :math.pow(y, 2))
direction = %{x: x / distance, y: y / distance}

%{
direction: direction,
distance: distance
}
end
end
91 changes: 58 additions & 33 deletions apps/bot_manager/lib/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ defmodule BotManager.GameSocketHandler do
It handles the communication with the server.
"""

alias BotManager.BotStateMachine

use WebSockex, restart: :transient
require Logger

@message_delay_ms 300
@decision_delay_ms 200
@action_delay_ms 30

def start_link(%{"bot_client" => bot_client, "game_id" => game_id} = params) do
ws_url = ws_url(params)
Expand All @@ -18,43 +21,80 @@ defmodule BotManager.GameSocketHandler do
})
end

#######################
# handlers #
#######################

def handle_connect(_conn, state) do
send(self(), :move)
send(self(), :attack)
send(self(), :decide_action)
send(self(), :perform_action)
{:ok, state}
end

def handle_frame(_frame, state) do
def handle_frame({:binary, frame}, state) do
case BotManager.Protobuf.GameEvent.decode(frame) do
%{event: {:update, game_state}} ->
bot_player = Map.get(game_state.players, state.player_id)

update = %{
bot_player: bot_player,
game_state: game_state
}

{:ok, Map.merge(state, update)}

%{event: {:joined, joined}} ->
{:ok, Map.merge(state, joined)}

%{event: {:finished, _}} ->
{:stop, state}

_ ->
{:ok, state}
end
end

def handle_info(:decide_action, state) do
Process.send_after(self(), :decide_action, @decision_delay_ms)

action = BotStateMachine.decide_action(state)

{:ok, Map.put(state, :current_action, action)}
end

def handle_info(:perform_action, state) do
Process.send_after(self(), :perform_action, @action_delay_ms)

send_current_action(state)

{:ok, state}
end

def handle_info(:move, state) do
def handle_cast({:send, {_type, _msg} = frame}, state) do
{:reply, frame, state}
end

defp send_current_action(%{current_action: {:move, direction}}) do
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
{x, y} = create_random_direction()

game_action =
BotManager.Protobuf.GameAction.encode(%BotManager.Protobuf.GameAction{
action_type:
{:move,
%BotManager.Protobuf.Move{
direction: %BotManager.Protobuf.Direction{
x: x,
y: y
x: direction.x,
y: direction.y
}
}},
timestamp: timestamp
})

WebSockex.cast(self(), {:send, {:binary, game_action}})

Process.send_after(self(), :move, @message_delay_ms)

{:ok, state}
end

def handle_info(:attack, state) do
defp send_current_action(%{current_action: {:attack, direction}}) do
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
{x, y} = create_random_direction()

game_action =
BotManager.Protobuf.GameAction.encode(%BotManager.Protobuf.GameAction{
Expand All @@ -64,27 +104,21 @@ defmodule BotManager.GameSocketHandler do
skill: "1",
parameters: %BotManager.Protobuf.AttackParameters{
target: %BotManager.Protobuf.Direction{
x: x,
y: y
x: direction.x,
y: direction.y
}
}
}},
timestamp: timestamp
})

WebSockex.cast(self(), {:send, {:binary, game_action}})

Process.send_after(self(), :attack, @message_delay_ms)
{:ok, state}
end

def handle_cast({:send, {_type, _msg} = frame}, state) do
{:reply, frame, state}
end
defp send_current_action(_), do: nil

def terminate(_, _, _) do
Logger.info("Websocket terminated")
:ok
exit(:normal)
end

defp ws_url(%{
Expand All @@ -100,13 +134,4 @@ defmodule BotManager.GameSocketHandler do
"wss://#{arena_host}/play/#{game_id}/#{bot_client}"
end
end

defp create_random_direction() do
Enum.random([
{1, 0},
{0, -1},
{-1, 0},
{0, 1}
])
end
end
33 changes: 33 additions & 0 deletions apps/bot_manager/lib/math/vector.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule BotManager.Math.Vector do
@moduledoc """
Module to handle math operations with vectors
"""

def sub(vector, value) when is_integer(value) or is_float(value) do
%{
x: vector.x - value,
y: vector.y - value
}
end

def sub(first_vector, second_vector) do
%{
x: first_vector.x - second_vector.x,
y: first_vector.y - second_vector.y
}
end

def mult(vector, value) when is_integer(value) or is_float(value) do
%{
x: vector.x * value,
y: vector.y * value
}
end

def mult(first_vector, second_vector) do
%{
x: first_vector.x * second_vector.x,
y: first_vector.y * second_vector.y
}
end
end
29 changes: 28 additions & 1 deletion apps/bot_manager/lib/protobuf/messages.pb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ defmodule BotManager.Protobuf.GameState.ItemsEntry do
field(:value, 2, type: BotManager.Protobuf.Entity)
end

defmodule BotManager.Protobuf.GameState.ObstaclesEntry do
@moduledoc false

use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

field(:key, 1, type: :uint64)
field(:value, 2, type: BotManager.Protobuf.Entity)
end

defmodule BotManager.Protobuf.GameState.PoolsEntry do
@moduledoc false

Expand Down Expand Up @@ -299,7 +308,14 @@ defmodule BotManager.Protobuf.GameState do
field(:status, 11, type: BotManager.Protobuf.GameStatus, enum: true)
field(:start_game_timestamp, 12, type: :int64, json_name: "startGameTimestamp")
field(:items, 13, repeated: true, type: BotManager.Protobuf.GameState.ItemsEntry, map: true)
field(:pools, 14, repeated: true, type: BotManager.Protobuf.GameState.PoolsEntry, map: true)

field(:obstacles, 14,
repeated: true,
type: BotManager.Protobuf.GameState.ObstaclesEntry,
map: true
)

field(:pools, 15, repeated: true, type: BotManager.Protobuf.GameState.PoolsEntry, map: true)
end

defmodule BotManager.Protobuf.Entity do
Expand Down Expand Up @@ -337,6 +353,15 @@ defmodule BotManager.Protobuf.Player.EffectsEntry do
field(:value, 2, type: BotManager.Protobuf.Effect)
end

defmodule BotManager.Protobuf.Player.CooldownsEntry do
@moduledoc false

use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

field(:key, 1, type: :string)
field(:value, 2, type: :uint64)
end

defmodule BotManager.Protobuf.Player do
@moduledoc false

Expand All @@ -359,6 +384,7 @@ defmodule BotManager.Protobuf.Player do
field(:power_ups, 9, type: :uint64, json_name: "powerUps")
field(:effects, 10, repeated: true, type: BotManager.Protobuf.Player.EffectsEntry, map: true)
field(:inventory, 11, type: BotManager.Protobuf.Item)
field(:cooldowns, 12, repeated: true, type: BotManager.Protobuf.Player.CooldownsEntry, map: true)
end

defmodule BotManager.Protobuf.Effect do
Expand Down Expand Up @@ -421,6 +447,7 @@ defmodule BotManager.Protobuf.PlayerAction do

field(:action, 1, type: BotManager.Protobuf.PlayerActionType, enum: true)
field(:duration, 2, type: :uint64)
field(:destination, 3, type: BotManager.Protobuf.Position)
end

defmodule BotManager.Protobuf.Move do
Expand Down
9 changes: 9 additions & 0 deletions apps/bot_manager/lib/utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule BotManager.Utils do
@moduledoc """
utils to work with nested game state operations
"""

def player_alive?(%{aditional_info: {:player, %{health: health}}}), do: health > 0

def player_alive?(_), do: :not_a_player
end
2 changes: 1 addition & 1 deletion apps/game_client/lib/game_client/protobuf/messages.pb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ end
defmodule GameClient.Protobuf.GameState.ObstaclesEntry do
@moduledoc false

use Protobuf, map: true, protoc_gen_elixir_version: "0.12.0", syntax: :proto3
use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

field(:key, 1, type: :uint64)
field(:value, 2, type: GameClient.Protobuf.Entity)
Expand Down
Loading