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-1054] Bots movement improvement #1055

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 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
8 changes: 8 additions & 0 deletions apps/arena/docs/src/bots/bots.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ Bots will enter into the attacking mode if they have charged enough energy to us
This is just a gimmick added to prevent bots from using their shooting skills randomly and without purpose.
When attacking, they will focus on the nearest player and won't consider health percentages or other factors, at least for now.

#### Tracking A Player

This state arises when the bot becomes bloodthirsty, and there are no nearby enemies that can be easily hit. The bot is more likely to start following enemies in order to catch and attack them!

Formally, for a bot to reach this state, players need to be near it but not close enough to be attacked.

![aggresive areas](bots_aggresive_areas.png)

#### Running Away

Bots will transition to this state whenever their health drops below a certain percentage. For now, this threshold is set at 40%. In this state, bots will attempt to escape from players by running in the opposite direction of the closest one. This does not necessarily mean they will run away from the player attacking them.
Expand Down
Binary file added apps/arena/docs/src/bots/bots_aggresive_areas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
191 changes: 96 additions & 95 deletions apps/bot_manager/lib/bot_state_machine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ defmodule BotManager.BotStateMachine do
@skill_1_key "1"
@skill_2_key "2"
@dash_skill_key "3"
@vision_range 1200
@min_distance_to_switch 10

def decide_action(%{bots_enabled?: false, bot_state_machine: bot_state_machine}) do
%{action: {:move, %{x: 0, y: 0}}, bot_state_machine: bot_state_machine}
Expand All @@ -27,28 +25,13 @@ defmodule BotManager.BotStateMachine do
bot_state_machine: bot_state_machine,
attack_blocked: attack_blocked
}) do
bot_state_machine =
if is_nil(bot_state_machine.previous_position) do
bot_state_machine
|> Map.put(:previous_position, bot_player.position)
|> Map.put(:current_position, bot_player.position)
else
bot_state_machine
|> Map.put(:previous_position, bot_state_machine.current_position)
|> Map.put(:current_position, bot_player.position)
end
bot_state_machine = preprocess_bot_state(bot_state_machine, bot_player)

%{distance: distance} =
get_distance_and_direction_to_positions(bot_state_machine.previous_position, bot_state_machine.current_position)

bot_state_machine =
Map.put(bot_state_machine, :progress_for_basic_skill, bot_state_machine.progress_for_basic_skill + distance)

next_state = BotStateMachineChecker.move_to_next_state(bot_player, bot_state_machine)
next_state = BotStateMachineChecker.move_to_next_state(bot_player, bot_state_machine, game_state.players)

case next_state do
:moving ->
move(bot_player, bot_state_machine)
move(bot_player, bot_state_machine, game_state.zone.radius)

:attacking ->
use_skill(%{
Expand All @@ -60,6 +43,9 @@ defmodule BotManager.BotStateMachine do

:running_away ->
run_away(bot_player, game_state, bot_state_machine)

:tracking_player ->
track_player(game_state, bot_player, bot_state_machine)
end
end

Expand All @@ -78,10 +64,11 @@ defmodule BotManager.BotStateMachine do
bot_player: bot_player,
bot_state_machine: bot_state_machine
}) do
players_with_distances = map_directions_to_players(game_state, bot_player, @vision_range)
players_with_distances =
Utils.map_directions_to_players(game_state.players, bot_player, bot_state_machine.vision_range_to_attack_player)

if Enum.empty?(players_with_distances) do
move(bot_player, bot_state_machine)
move(bot_player, bot_state_machine, game_state.zone.radius)
else
cond do
bot_state_machine.progress_for_ultimate_skill >= bot_state_machine.cap_for_ultimate_skill ->
Expand Down Expand Up @@ -112,105 +99,93 @@ defmodule BotManager.BotStateMachine do
%{action: {:use_skill, @skill_1_key, direction}, bot_state_machine: bot_state_machine}

true ->
move(bot_player, bot_state_machine)
move(bot_player, bot_state_machine, game_state.zone.radius)
end
end
end

# This function will map the directions and distance from the bot to the players.
defp map_directions_to_players(game_state, bot_player, max_distance) do
Map.delete(game_state.players, bot_player.id)
|> Map.filter(fn {player_id, player} ->
Utils.player_alive?(player) && player_within_visible_players?(bot_player, player_id) &&
not bot_belongs_to_the_same_team?(bot_player, 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)
|> Enum.filter(fn player_info -> player_info.distance <= max_distance end)
end

defp get_distance_and_direction_to_positions(base_position, base_position) do
%{
direction: %{x: 0, y: 0},
distance: 0
}
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}
# This function will determine the direction and action the bot will take.
defp determine_player_move_action(bot_player, direction) do
{:player, bot_player_info} = bot_player.aditional_info

%{
direction: direction,
distance: distance
}
if Map.has_key?(bot_player_info.cooldowns, @dash_skill_key) do
{:move, direction}
else
{:use_skill, @dash_skill_key, bot_player.direction}
end
end

defp player_within_visible_players?(bot_player, player_id) do
{:player, bot_player_info} = bot_player.aditional_info
Enum.member?(bot_player_info.visible_players, player_id)
end
defp track_player(game_state, bot_player, bot_state_machine) do
players_with_distances =
Utils.map_directions_to_players(
game_state.players,
bot_player,
bot_state_machine.max_vision_range_to_follow_player
)

defp bot_belongs_to_the_same_team?(bot_player, player) do
{:player, bot_player_info} = bot_player.aditional_info
{:player, player_info} = player.aditional_info
if Enum.empty?(players_with_distances) do
move(bot_player, bot_state_machine, game_state.zone.radius)
else
closest_player = Enum.min_by(players_with_distances, & &1.distance)

bot_player_info.team == player_info.team
%{
action: determine_player_move_action(bot_player, closest_player.direction),
bot_state_machine: bot_state_machine
}
end
end

# This function will determine the direction and action the bot will take.
defp determine_player_move_action(bot_player, bot_state_machine, direction) do
{:player, bot_player_info} = bot_player.aditional_info

direction =
if BotStateMachineChecker.should_bot_rotate_its_direction?(bot_state_machine) do
Vector.rotate_by_degrees(direction, :rand.uniform() * 360)
defp preprocess_bot_state(bot_state_machine, bot_player) do
bot_state_machine =
if is_nil(bot_state_machine.previous_position) do
bot_state_machine
|> Map.put(:previous_position, bot_player.position)
|> Map.put(:current_position, bot_player.position)
else
direction
bot_state_machine
|> Map.put(:previous_position, bot_state_machine.current_position)
|> Map.put(:current_position, bot_player.position)
end

if Map.has_key?(bot_player_info.cooldowns, @dash_skill_key) do
{:move, direction}
else
{:use_skill, @dash_skill_key, direction}
end
end
%{distance: distance} =
Utils.get_distance_and_direction_to_positions(
bot_state_machine.previous_position,
bot_state_machine.current_position
)

# This function will change the bot’s direction by checking if it has stayed in the same position between the last and current update.
# If the bot hasn’t moved, it will randomly switch its direction.
defp maybe_switch_direction(bot_player, bot_state_machine) do
x_distance = abs(bot_state_machine.current_position.x - bot_state_machine.previous_position.x)
y_distance = abs(bot_state_machine.current_position.y - bot_state_machine.previous_position.y)
bot_state_machine =
Map.put(bot_state_machine, :progress_for_basic_skill, bot_state_machine.progress_for_basic_skill + distance)

if x_distance < @min_distance_to_switch and y_distance < @min_distance_to_switch,
do: switch_direction_randomly(bot_player.direction),
else: bot_player.direction
end
cond do
Vector.distance(bot_state_machine.previous_position, bot_state_machine.current_position) < 100 &&
is_nil(bot_state_machine.start_time_stuck_in_position) ->
Map.put(bot_state_machine, :start_time_stuck_in_position, :os.system_time(:millisecond))
|> Map.put(:stuck_in_position, bot_state_machine.current_position)

not is_nil(bot_state_machine.stuck_in_position) &&
Vector.distance(bot_state_machine.stuck_in_position, bot_state_machine.current_position) > 100 ->
Map.put(bot_state_machine, :start_time_stuck_in_position, nil)
|> Map.put(:stuck_in_position, nil)

defp switch_direction_randomly(direction) do
Vector.rotate_by_degrees(direction, Enum.random([90, 180, 270]))
true ->
bot_state_machine
end
end

defp run_away(bot_player, game_state, bot_state_machine) do
players_with_distances = map_directions_to_players(game_state, bot_player, @vision_range)
players_with_distances =
Utils.map_directions_to_players(game_state, bot_player, bot_state_machine.vision_range_to_attack_player)

if Enum.empty?(players_with_distances) do
move(bot_player, bot_state_machine)
move(bot_player, bot_state_machine, game_state.zone.radius)
else
closest_player = Enum.min_by(players_with_distances, & &1.distance)

direction =
closest_player.direction |> Vector.normalize() |> Vector.rotate_by_degrees(180)

%{
action: determine_player_move_action(bot_player, bot_state_machine, direction),
action: determine_player_move_action(bot_player, direction),
bot_state_machine: bot_state_machine
}
end
Expand All @@ -225,12 +200,38 @@ defmodule BotManager.BotStateMachine do
end
end

defp move(bot_player, bot_state_machine) do
direction = maybe_switch_direction(bot_player, bot_state_machine)
defp move(bot_player, bot_state_machine, safe_zone_radius) do
bot_state_machine = determine_position_to_move_to(bot_state_machine, safe_zone_radius)

%{direction: direction} =
Utils.get_distance_and_direction_to_positions(
bot_state_machine.current_position,
bot_state_machine.position_to_move_to
)

%{
action: determine_player_move_action(bot_player, bot_state_machine, direction),
action: determine_player_move_action(bot_player, direction),
bot_state_machine: bot_state_machine
}
end

defp determine_position_to_move_to(bot_state_machine, safe_zone_radius) do
cond do
is_nil(bot_state_machine.position_to_move_to) ||
not Utils.position_within_radius(bot_state_machine.position_to_move_to, safe_zone_radius) ->
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))

Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))

BotStateMachineChecker.should_bot_move_to_another_position?(bot_state_machine) ->
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))

Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))

true ->
bot_state_machine
end
end
end
70 changes: 53 additions & 17 deletions apps/bot_manager/lib/bot_state_machine_checker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ defmodule BotManager.BotStateMachineChecker do
@moduledoc """
This module will take care of deciding what the bot will do on each deciding step
"""
alias BotManager.Utils

@low_health_percentage 40
@time_stuck_in_position 400

defstruct [
# The bot state, these are the possible states: [:idling, :moving, :attacking, :running_away]
# The bot state, these are the possible states: [:idling, :moving, :attacking, :running_away, :tracking_player, :repositioning]
:state,
# The previous position of the bot
:previous_position,
Expand All @@ -18,12 +22,20 @@ defmodule BotManager.BotStateMachineChecker do
:cap_for_basic_skill,
# This is the maximum value that the progress_for_ultimate_skill can reach
:cap_for_ultimate_skill,
# The time that the bot is going to take to change its direction in milliseconds
:time_to_change_direction,
# The last time that the bot changed its direction
:last_time_direction_changed,
# The time that the bot has been moving in the same direction
:current_time_in_direction
# The position that the bot is going to move to
:position_to_move_to,
# The time that the bot is going to take to change its position in milliseconds
:time_amount_to_change_position,
# The last time that the bot changed its position
:last_time_position_changed,
# The maximum vision range that the bot can follow a player
:max_vision_range_to_follow_player,
# The vision range that the bot has to find a player to attack
:vision_range_to_attack_player,
# Start Time in the same position
:start_time_stuck_in_position,
# The position that the bot is stuck in
:stuck_in_position
]

def new do
Expand All @@ -35,36 +47,60 @@ defmodule BotManager.BotStateMachineChecker do
cap_for_ultimate_skill: 3,
previous_position: nil,
current_position: nil,
time_to_change_direction: 1600,
last_time_direction_changed: 0,
current_time_in_direction: 0
position_to_move_to: nil,
time_amount_to_change_position: 2000,
last_time_position_changed: 0,
max_vision_range_to_follow_player: 1500,
vision_range_to_attack_player: 1200,
stuck_in_position: nil,
start_time_stuck_in_position: nil
}
end

def move_to_next_state(bot_player, bot_state_machine) do
def move_to_next_state(bot_player, bot_state_machine, players) do
cond do
bot_stuck?(bot_state_machine) -> :moving
bot_health_low?(bot_player) -> :running_away
bot_can_follow_a_player?(bot_player, bot_state_machine, players) -> :tracking_player
bot_can_turn_aggresive?(bot_state_machine) -> :attacking
true -> :moving
end
end

def bot_health_low?(bot_player) do
def should_bot_move_to_another_position?(bot_state_machine) do
current_time = :os.system_time(:millisecond)
time_since_last_position_change = current_time - bot_state_machine.last_time_position_changed

time_since_last_position_change >= bot_state_machine.time_amount_to_change_position
end

defp bot_health_low?(bot_player) do
{:player, bot_player_info} = bot_player.aditional_info
health_percentage = bot_player_info.health * 100 / bot_player_info.max_health

health_percentage <= @low_health_percentage
end

def bot_can_turn_aggresive?(bot_state_machine) do
defp bot_can_turn_aggresive?(bot_state_machine) do
bot_state_machine.progress_for_basic_skill >= bot_state_machine.cap_for_basic_skill ||
bot_state_machine.progress_for_ultimate_skill >= bot_state_machine.cap_for_ultimate_skill
end

def should_bot_rotate_its_direction?(bot_state_machine) do
current_time = :os.system_time(:millisecond)
time_since_last_direction_change = current_time - bot_state_machine.last_time_direction_changed
defp bot_can_follow_a_player?(bot_player, bot_state_machine, players) do
players_nearby_to_follow =
Utils.map_directions_to_players(players, bot_player, bot_state_machine.max_vision_range_to_follow_player)

players_nearby_to_attack =
Utils.map_directions_to_players(players, bot_player, bot_state_machine.vision_range_to_attack_player)

Enum.empty?(players_nearby_to_attack) && not Enum.empty?(players_nearby_to_follow) &&
bot_can_turn_aggresive?(bot_state_machine) && not bot_stuck?(bot_state_machine)
end

defp bot_stuck?(%{start_time_stuck_in_position: nil}), do: false

time_since_last_direction_change >= bot_state_machine.time_to_change_direction
defp bot_stuck?(bot_state_machine) do
time_stuck = :os.system_time(:millisecond) - bot_state_machine.start_time_stuck_in_position
time_stuck >= @time_stuck_in_position
end
end
Loading
Loading