Skip to content

1123 use pathfinding in actual bot behavior #1129

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

Merged
merged 32 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
26ddf89
refactor: split path selection from path following
mFragaBA Mar 18, 2025
552c5e5
wip: fix old check that avoided tracking player
mFragaBA Mar 19, 2025
483389f
fix: check empty path in :move
mFragaBA Mar 25, 2025
f5a432b
refactor: change 2 case cond for if
mFragaBA Mar 25, 2025
bcc26a1
refactor: simplify `determine_position_to_move_to`
mFragaBA Mar 25, 2025
0ee0e90
refactor: simplify `maybe_set_tracking_path`
mFragaBA Mar 25, 2025
7db5b12
run formatter
mFragaBA Mar 25, 2025
2ebcee9
refactor: remove duplicate function impl
mFragaBA Mar 25, 2025
4856309
comment out unused variable
mFragaBA Mar 25, 2025
0cbbc69
refactor: remove unnecessary function
mFragaBA Mar 25, 2025
a3fb30f
restore old distance implementation
mFragaBA Mar 25, 2025
1a0e130
refactor: remove unused param
mFragaBA Mar 25, 2025
d4818f3
fix: stop considering stamina in tracking decision
mFragaBA Apr 1, 2025
ca45eef
feat: add timeout and cooldown to tracking
mFragaBA Apr 1, 2025
00ff63d
run formatter
mFragaBA Apr 1, 2025
093d72e
tune tracking cooldown and timeout parameters
mFragaBA Apr 1, 2025
0bb63b8
fix: make bot pick new position if zone closed and wandering position…
mFragaBA Apr 1, 2025
87a81fa
fix formatting and credo
mFragaBA Apr 1, 2025
b9535da
feat: do not do anything if bot is dead
mFragaBA Apr 1, 2025
a7850ce
remove PATHFINDING_TEST conditional checks
mFragaBA Apr 3, 2025
912b0de
increase cap for basic skill
mFragaBA Apr 7, 2025
d558bcd
fix: do not send action if action already executing and is blocking
mFragaBA Apr 7, 2025
0c142b4
feat: add min delay between attacks
mFragaBA Apr 7, 2025
e81195a
fix: cap progress for basic attack
mFragaBA Apr 7, 2025
e8fbf43
fix: set last_time_attacking_exited when attacking state exits
mFragaBA Apr 7, 2025
41b5169
Merge branch 'main' into 1123-use-pathfinding-in-actual-bot-behavior
mFragaBA Apr 7, 2025
79034b1
run format
mFragaBA Apr 7, 2025
b4f9641
remove commented constant
mFragaBA Apr 9, 2025
0435e9c
add comments explaining constants used in state machine
mFragaBA Apr 9, 2025
0ebf71c
Merge branch 'main' into 1123-use-pathfinding-in-actual-bot-behavior
mFragaBA Apr 9, 2025
74f99c2
Merge branch 'main' into 1123-use-pathfinding-in-actual-bot-behavior
mFragaBA Apr 11, 2025
168552d
Merge branch 'main' into 1123-use-pathfinding-in-actual-bot-behavior
Nico-Sanchez Apr 15, 2025
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
24 changes: 17 additions & 7 deletions apps/arena/lib/arena/bots/bot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ defmodule Arena.Bots.Bot do
end

defp maybe_update_state_params(state, game_state, config) do
if System.get_env("PATHFINDING_TEST") == "true" and is_nil(state.bot_state_machine.collision_grid) do
if is_nil(state.bot_state_machine.collision_grid) do
PathfindingGrid.get_map_collision_grid(config.map.name, self())
end

Expand Down Expand Up @@ -119,19 +119,29 @@ defmodule Arena.Bots.Bot do
end

defp update_block_attack_state(%{current_action: %{action: {:use_skill, _, _}, sent: false}} = state) do
Process.send_after(self(), :unblock_attack, 100)
Process.send_after(self(), :unblock_attack, 50)
%{state | attack_blocked: true, current_action: %{state.current_action | sent: true}}
end

defp update_block_attack_state(state), do: state

defp send_current_action(%{current_action: %{action: {:move, direction}, sent: false}} = state) do
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
Arena.GameUpdater.move(state.game_pid, state.bot_player.id, direction, timestamp)
defp send_current_action(
%{current_action: %{action: {:move, direction}, sent: false}, bot_player: bot_player} = state
) do
{:player, aditional_info} = bot_player.aditional_info

if Enum.all?(aditional_info.current_actions, fn current_action ->
(current_action.action != :EXECUTING_SKILL_1 or not state.bot_skills.basic.block_movement) and
(current_action.action != :EXECUTING_SKILL_2 or not state.bot_skills.ultimate.block_movement)
end) do
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
Arena.GameUpdater.move(state.game_pid, state.bot_player.id, direction, timestamp)
end
end

defp send_current_action(%{current_action: %{action: {:use_skill, skill_key, direction}, sent: false}} = state) do
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
Arena.GameUpdater.move(state.game_pid, state.bot_player.id, %{x: 0, y: 0}, timestamp)
Arena.GameUpdater.attack(state.game_pid, state.bot_player.id, skill_key, %{target: direction}, timestamp)
end

Expand All @@ -141,6 +151,6 @@ defmodule Arena.Bots.Bot do
Logger.error("Bot #{state.bot_id} terminating: #{inspect(reason)}")
end

defp min_decision_delay_ms(), do: if(System.get_env("PATHFINDING_TEST") == "true", do: 100, else: 750)
defp max_decision_delay_ms(), do: if(System.get_env("PATHFINDING_TEST") == "true", do: 150, else: 1250)
defp min_decision_delay_ms(), do: 40
defp max_decision_delay_ms(), do: 60
end
4 changes: 1 addition & 3 deletions apps/arena/lib/arena/bots/pathfinding_grid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ defmodule Arena.Bots.PathfindingGrid do
end

def init(_opts) do
if System.get_env("PATHFINDING_TEST") == "true" do
Process.send_after(self(), :update_config, 1_000)
end
Process.send_after(self(), :update_config, 1_000)

{:ok, %{}}
end
Expand Down
225 changes: 119 additions & 106 deletions apps/bot_manager/lib/bot_state_machine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ defmodule BotManager.BotStateMachine do

@skill_1_key "1"
@skill_2_key "2"
@dash_skill_key "3"

def decide_action(%{enabled?: false, bot_state_machine: bot_state_machine}) do
# The minimum distance a tracked player has to move for the tracking path to
# get recalculated
@path_recalculation_min_diff 300

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}
end

def decide_action(%{bot_player: %{aditional_info: {:player, %{health: health}}}, bot_state_machine: bot_state_machine})
when health <= 0 do
%{action: {:move, %{x: 0, y: 0}}, bot_state_machine: bot_state_machine}
end

Expand All @@ -29,31 +37,57 @@ defmodule BotManager.BotStateMachine do
bot_state_machine = preprocess_bot_state(bot_state_machine, bot_player)
next_state = BotStateMachineChecker.move_to_next_state(bot_player, bot_state_machine, game_state.players)

if System.get_env("PATHFINDING_TEST") == "true" do
move(bot_player, bot_state_machine, game_state.zone.radius)
else
case next_state do
:moving ->
move(bot_player, bot_state_machine, game_state.zone.radius)

:attacking ->
use_skill(%{
bot_player: bot_player,
bot_state_machine: bot_state_machine,
game_state: game_state,
attack_blocked: attack_blocked,
bot_skills: skills
})

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

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

:attacking ->
use_skill(%{
bot_player: bot_player,
bot_state_machine: bot_state_machine,
game_state: game_state,
attack_blocked: attack_blocked,
bot_skills: skills
})

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

def decide_action(%{bot_state_machine: bot_state_machine}),
do: %{action: :idling, bot_state_machine: bot_state_machine}

# This function will handle state switching logic to leave the bot state machine in a proper state
defp maybe_exit_state(%{state: state} = bot_state_machine, state) do
bot_state_machine
end

defp maybe_exit_state(%{state: state} = bot_state_machine, new_state) do
bot_state_machine
|> Map.put(:state, new_state)
|> Map.put(:last_time_state_changed, :os.system_time(:millisecond))
|> exit_state(state)
end

# updates necessary data to exit each specific state
defp exit_state(bot_state_machine, :tracking_player) do
bot_state_machine
|> Map.put(:last_time_tracking_exited, :os.system_time(:millisecond))
end

defp exit_state(bot_state_machine, :attacking) do
bot_state_machine
|> Map.put(:last_time_attacking_exited, :os.system_time(:millisecond))
end

defp exit_state(bot_state_machine, _exited_state) do
bot_state_machine
end

@doc """
This function will be in charge of using the bot's skill.
Depending on the bot's state, it will use the basic skill, the ultimate skill or move.
Expand Down Expand Up @@ -81,7 +115,7 @@ defmodule BotManager.BotStateMachine do
)

if Enum.empty?(players_with_distances) do
move(bot_player, bot_state_machine, game_state.zone.radius)
move(bot_state_machine, game_state.zone.radius)
else
bot_state_machine =
Map.put(
Expand All @@ -108,7 +142,7 @@ defmodule BotManager.BotStateMachine do
)

if Enum.empty?(players_with_distances) do
move(bot_player, bot_state_machine, game_state.zone.radius)
move(bot_state_machine, game_state.zone.radius)
else
bot_state_machine =
Map.put(
Expand All @@ -124,44 +158,49 @@ defmodule BotManager.BotStateMachine do
end

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

# 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

if System.get_env("PATHFINDING_TEST") == "true" do
{:move, direction}
else
if Map.has_key?(bot_player_info.cooldowns, @dash_skill_key) do
{:move, direction}
else
{:use_skill, @dash_skill_key, bot_player.direction}
end
move(bot_state_machine, game_state.zone.radius)
end
end

defp track_player(game_state, bot_player, bot_state_machine) do
defp maybe_set_tracking_path(game_state, bot_player, bot_state_machine) do
players_with_distances =
Utils.map_directions_to_players(
game_state.players,
bot_player,
Utils.get_action_distance_based_on_action_type(
Utils.get_action_distance_by_type(
bot_state_machine.is_melee,
bot_state_machine.melee_tracking_range,
bot_state_machine.ranged_tracking_range
)
)

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

cond do
is_nil(bot_state_machine.path_towards_position) or Enum.empty?(bot_state_machine.path_towards_position) or
Vector.distance(bot_state_machine.position_to_move_to, closest_player.position) > @path_recalculation_min_diff ->
try_pathing_towards(bot_state_machine, closest_player.position)

BotStateMachineChecker.current_waypoint_reached?(bot_state_machine) ->
Map.put(bot_state_machine, :path_towards_position, tl(bot_state_machine.path_towards_position))

true ->
bot_state_machine
end
end

defp track_player(game_state, bot_player, bot_state_machine) do
if is_nil(bot_state_machine.path_towards_position) || Enum.empty?(bot_state_machine.path_towards_position) do
move(bot_state_machine, game_state.zone.radius)
else
closest_player = Enum.min_by(players_with_distances, & &1.distance)
current_waypoint = hd(bot_state_machine.path_towards_position)

direction =
Vector.sub(current_waypoint, bot_player.position)
|> Vector.normalize()

%{
action: determine_player_move_action(bot_player, closest_player.direction),
action: {:move, direction},
bot_state_machine: bot_state_machine
}
end
Expand All @@ -185,8 +224,10 @@ defmodule BotManager.BotStateMachine do
bot_state_machine.current_position
)

new_progress = min(bot_state_machine.cap_for_basic_skill * 3, bot_state_machine.progress_for_basic_skill + distance)

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

cond do
Vector.distance(bot_state_machine.previous_position, bot_state_machine.current_position) < 100 &&
Expand All @@ -213,9 +254,9 @@ defmodule BotManager.BotStateMachine do
end
end

defp move(bot_player, bot_state_machine, safe_zone_radius) do
defp move(bot_state_machine, safe_zone_radius) do
bot_state_machine =
determine_position_to_move_to(bot_state_machine, safe_zone_radius, System.get_env("PATHFINDING_TEST") == "true")
determine_position_to_move_to(bot_state_machine, safe_zone_radius)

# TODO instead of using `get_distance_and_direction_to_positions, use the pathfinding module`
cond do
Expand All @@ -227,7 +268,7 @@ defmodule BotManager.BotStateMachine do
)

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

Expand All @@ -239,7 +280,7 @@ defmodule BotManager.BotStateMachine do
)

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

Expand All @@ -251,53 +292,19 @@ defmodule BotManager.BotStateMachine do
end
end

defp determine_position_to_move_to(bot_state_machine, safe_zone_radius, true = _pathfinding_on) do
cond do
is_nil(bot_state_machine.collision_grid) ->
bot_state_machine

is_nil(bot_state_machine.path_towards_position) ->
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))

from = %{x: bot_state_machine.current_position.x, y: bot_state_machine.current_position.y}
to = %{x: position_to_move_to.x, y: position_to_move_to.y}

shortest_path = AStarNative.a_star_shortest_path(from, to, bot_state_machine.collision_grid)
defp determine_position_to_move_to(%{collision_grid: nil} = bot_state_machine, _safe_zone_radius) do
bot_state_machine
end

# If we don't have a path, retry finding new position in map
if Enum.empty?(shortest_path) do
Map.put(bot_state_machine, :path_towards_position, nil)
|> Map.put(:position_to_move_to, nil)
else
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
|> Map.put(
:path_towards_position,
shortest_path
)
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
end
defp determine_position_to_move_to(bot_state_machine, safe_zone_radius) do
cond do
is_nil(bot_state_machine.path_towards_position) || Enum.empty?(bot_state_machine.path_towards_position) ||
Vector.distance(%{x: 0, y: 0}, bot_state_machine.position_to_move_to) > safe_zone_radius ->
try_pick_random_position_to_move_to(bot_state_machine, safe_zone_radius)

BotStateMachineChecker.current_waypoint_reached?(bot_state_machine) and
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))

from = %{x: bot_state_machine.current_position.x, y: bot_state_machine.current_position.y}
to = %{x: position_to_move_to.x, y: position_to_move_to.y}

shortest_path = AStarNative.a_star_shortest_path(from, to, bot_state_machine.collision_grid)

# If we don't have a path, retry finding new position in map
if Enum.empty?(shortest_path) do
Map.put(bot_state_machine, :path_towards_position, nil)
|> Map.put(:position_to_move_to, nil)
else
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
|> Map.put(
:path_towards_position,
shortest_path
)
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
end
try_pick_random_position_to_move_to(bot_state_machine, safe_zone_radius)

BotStateMachineChecker.current_waypoint_reached?(bot_state_machine) ->
Map.put(bot_state_machine, :path_towards_position, tl(bot_state_machine.path_towards_position))
Expand All @@ -307,23 +314,29 @@ defmodule BotManager.BotStateMachine do
end
end

defp determine_position_to_move_to(bot_state_machine, safe_zone_radius, false = _pathfinding_on) 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))
defp try_pick_random_position_to_move_to(bot_state_machine, safe_zone_radius) do
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))
try_pathing_towards(bot_state_machine, position_to_move_to)
end

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))
defp try_pathing_towards(bot_state_machine, position_to_move_to) do
from = %{x: bot_state_machine.current_position.x, y: bot_state_machine.current_position.y}
to = %{x: position_to_move_to.x, y: position_to_move_to.y}

Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
shortest_path = AStarNative.a_star_shortest_path(from, to, bot_state_machine.collision_grid)

true ->
bot_state_machine
# If we don't have a path, retry finding new position in map
if Enum.empty?(shortest_path) do
Map.put(bot_state_machine, :path_towards_position, nil)
|> Map.put(:position_to_move_to, nil)
else
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
|> Map.put(
:path_towards_position,
shortest_path
)
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
end
end
end
Loading
Loading