Skip to content

Commit e6bb209

Browse files
1123 use pathfinding in actual bot behavior (#1129)
* refactor: split path selection from path following * wip: fix old check that avoided tracking player it was bugged and never entered the tracking state. Fixing it brought more errors Imma debug * fix: check empty path in :move * refactor: change 2 case cond for if * refactor: simplify `determine_position_to_move_to` * refactor: simplify `maybe_set_tracking_path` * run formatter * refactor: remove duplicate function impl * comment out unused variable * refactor: remove unnecessary function * restore old distance implementation * refactor: remove unused param * fix: stop considering stamina in tracking decision * feat: add timeout and cooldown to tracking this is to make bots a bit less aggresive * run formatter * tune tracking cooldown and timeout parameters * fix: make bot pick new position if zone closed and wandering position is out of it * fix formatting and credo * feat: do not do anything if bot is dead * remove PATHFINDING_TEST conditional checks * increase cap for basic skill * fix: do not send action if action already executing and is blocking * feat: add min delay between attacks * fix: cap progress for basic attack * fix: set last_time_attacking_exited when attacking state exits * run format * remove commented constant * add comments explaining constants used in state machine --------- Co-authored-by: Nicolás Sanchez <[email protected]>
1 parent 99c3f33 commit e6bb209

File tree

4 files changed

+200
-143
lines changed

4 files changed

+200
-143
lines changed

apps/arena/lib/arena/bots/bot.ex

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ defmodule Arena.Bots.Bot do
7373
end
7474

7575
defp maybe_update_state_params(state, game_state, config) do
76-
if System.get_env("PATHFINDING_TEST") == "true" and is_nil(state.bot_state_machine.collision_grid) do
76+
if is_nil(state.bot_state_machine.collision_grid) do
7777
PathfindingGrid.get_map_collision_grid(config.map.name, self())
7878
end
7979

@@ -119,19 +119,29 @@ defmodule Arena.Bots.Bot do
119119
end
120120

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

126126
defp update_block_attack_state(state), do: state
127127

128-
defp send_current_action(%{current_action: %{action: {:move, direction}, sent: false}} = state) do
129-
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
130-
Arena.GameUpdater.move(state.game_pid, state.bot_player.id, direction, timestamp)
128+
defp send_current_action(
129+
%{current_action: %{action: {:move, direction}, sent: false}, bot_player: bot_player} = state
130+
) do
131+
{:player, aditional_info} = bot_player.aditional_info
132+
133+
if Enum.all?(aditional_info.current_actions, fn current_action ->
134+
(current_action.action != :EXECUTING_SKILL_1 or not state.bot_skills.basic.block_movement) and
135+
(current_action.action != :EXECUTING_SKILL_2 or not state.bot_skills.ultimate.block_movement)
136+
end) do
137+
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
138+
Arena.GameUpdater.move(state.game_pid, state.bot_player.id, direction, timestamp)
139+
end
131140
end
132141

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

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

144-
defp min_decision_delay_ms(), do: if(System.get_env("PATHFINDING_TEST") == "true", do: 100, else: 750)
145-
defp max_decision_delay_ms(), do: if(System.get_env("PATHFINDING_TEST") == "true", do: 150, else: 1250)
154+
defp min_decision_delay_ms(), do: 40
155+
defp max_decision_delay_ms(), do: 60
146156
end

apps/arena/lib/arena/bots/pathfinding_grid.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ defmodule Arena.Bots.PathfindingGrid do
1212
end
1313

1414
def init(_opts) do
15-
if System.get_env("PATHFINDING_TEST") == "true" do
16-
Process.send_after(self(), :update_config, 1_000)
17-
end
15+
Process.send_after(self(), :update_config, 1_000)
1816

1917
{:ok, %{}}
2018
end

apps/bot_manager/lib/bot_state_machine.ex

Lines changed: 119 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,17 @@ defmodule BotManager.BotStateMachine do
1010

1111
@skill_1_key "1"
1212
@skill_2_key "2"
13-
@dash_skill_key "3"
1413

15-
def decide_action(%{enabled?: false, bot_state_machine: bot_state_machine}) do
14+
# The minimum distance a tracked player has to move for the tracking path to
15+
# get recalculated
16+
@path_recalculation_min_diff 300
17+
18+
def decide_action(%{bots_enabled?: false, bot_state_machine: bot_state_machine}) do
19+
%{action: {:move, %{x: 0, y: 0}}, bot_state_machine: bot_state_machine}
20+
end
21+
22+
def decide_action(%{bot_player: %{aditional_info: {:player, %{health: health}}}, bot_state_machine: bot_state_machine})
23+
when health <= 0 do
1624
%{action: {:move, %{x: 0, y: 0}}, bot_state_machine: bot_state_machine}
1725
end
1826

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

32-
if System.get_env("PATHFINDING_TEST") == "true" do
33-
move(bot_player, bot_state_machine, game_state.zone.radius)
34-
else
35-
case next_state do
36-
:moving ->
37-
move(bot_player, bot_state_machine, game_state.zone.radius)
38-
39-
:attacking ->
40-
use_skill(%{
41-
bot_player: bot_player,
42-
bot_state_machine: bot_state_machine,
43-
game_state: game_state,
44-
attack_blocked: attack_blocked,
45-
bot_skills: skills
46-
})
47-
48-
:tracking_player ->
49-
track_player(game_state, bot_player, bot_state_machine)
50-
end
40+
bot_state_machine = maybe_exit_state(bot_state_machine, next_state)
41+
42+
case next_state do
43+
:moving ->
44+
move(bot_state_machine, game_state.zone.radius)
45+
46+
:attacking ->
47+
use_skill(%{
48+
bot_player: bot_player,
49+
bot_state_machine: bot_state_machine,
50+
game_state: game_state,
51+
attack_blocked: attack_blocked,
52+
bot_skills: skills
53+
})
54+
55+
:tracking_player ->
56+
bot_state_machine = maybe_set_tracking_path(game_state, bot_player, bot_state_machine)
57+
track_player(game_state, bot_player, bot_state_machine)
5158
end
5259
end
5360

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

64+
# This function will handle state switching logic to leave the bot state machine in a proper state
65+
defp maybe_exit_state(%{state: state} = bot_state_machine, state) do
66+
bot_state_machine
67+
end
68+
69+
defp maybe_exit_state(%{state: state} = bot_state_machine, new_state) do
70+
bot_state_machine
71+
|> Map.put(:state, new_state)
72+
|> Map.put(:last_time_state_changed, :os.system_time(:millisecond))
73+
|> exit_state(state)
74+
end
75+
76+
# updates necessary data to exit each specific state
77+
defp exit_state(bot_state_machine, :tracking_player) do
78+
bot_state_machine
79+
|> Map.put(:last_time_tracking_exited, :os.system_time(:millisecond))
80+
end
81+
82+
defp exit_state(bot_state_machine, :attacking) do
83+
bot_state_machine
84+
|> Map.put(:last_time_attacking_exited, :os.system_time(:millisecond))
85+
end
86+
87+
defp exit_state(bot_state_machine, _exited_state) do
88+
bot_state_machine
89+
end
90+
5791
@doc """
5892
This function will be in charge of using the bot's skill.
5993
Depending on the bot's state, it will use the basic skill, the ultimate skill or move.
@@ -81,7 +115,7 @@ defmodule BotManager.BotStateMachine do
81115
)
82116

83117
if Enum.empty?(players_with_distances) do
84-
move(bot_player, bot_state_machine, game_state.zone.radius)
118+
move(bot_state_machine, game_state.zone.radius)
85119
else
86120
bot_state_machine =
87121
Map.put(
@@ -108,7 +142,7 @@ defmodule BotManager.BotStateMachine do
108142
)
109143

110144
if Enum.empty?(players_with_distances) do
111-
move(bot_player, bot_state_machine, game_state.zone.radius)
145+
move(bot_state_machine, game_state.zone.radius)
112146
else
113147
bot_state_machine =
114148
Map.put(
@@ -124,44 +158,49 @@ defmodule BotManager.BotStateMachine do
124158
end
125159

126160
true ->
127-
move(bot_player, bot_state_machine, game_state.zone.radius)
128-
end
129-
end
130-
131-
# This function will determine the direction and action the bot will take.
132-
defp determine_player_move_action(bot_player, direction) do
133-
{:player, bot_player_info} = bot_player.aditional_info
134-
135-
if System.get_env("PATHFINDING_TEST") == "true" do
136-
{:move, direction}
137-
else
138-
if Map.has_key?(bot_player_info.cooldowns, @dash_skill_key) do
139-
{:move, direction}
140-
else
141-
{:use_skill, @dash_skill_key, bot_player.direction}
142-
end
161+
move(bot_state_machine, game_state.zone.radius)
143162
end
144163
end
145164

146-
defp track_player(game_state, bot_player, bot_state_machine) do
165+
defp maybe_set_tracking_path(game_state, bot_player, bot_state_machine) do
147166
players_with_distances =
148167
Utils.map_directions_to_players(
149168
game_state.players,
150169
bot_player,
151-
Utils.get_action_distance_based_on_action_type(
170+
Utils.get_action_distance_by_type(
152171
bot_state_machine.is_melee,
153172
bot_state_machine.melee_tracking_range,
154173
bot_state_machine.ranged_tracking_range
155174
)
156175
)
157176

158-
if Enum.empty?(players_with_distances) do
159-
move(bot_player, bot_state_machine, game_state.zone.radius)
177+
closest_player = Enum.min_by(players_with_distances, & &1.distance)
178+
179+
cond do
180+
is_nil(bot_state_machine.path_towards_position) or Enum.empty?(bot_state_machine.path_towards_position) or
181+
Vector.distance(bot_state_machine.position_to_move_to, closest_player.position) > @path_recalculation_min_diff ->
182+
try_pathing_towards(bot_state_machine, closest_player.position)
183+
184+
BotStateMachineChecker.current_waypoint_reached?(bot_state_machine) ->
185+
Map.put(bot_state_machine, :path_towards_position, tl(bot_state_machine.path_towards_position))
186+
187+
true ->
188+
bot_state_machine
189+
end
190+
end
191+
192+
defp track_player(game_state, bot_player, bot_state_machine) do
193+
if is_nil(bot_state_machine.path_towards_position) || Enum.empty?(bot_state_machine.path_towards_position) do
194+
move(bot_state_machine, game_state.zone.radius)
160195
else
161-
closest_player = Enum.min_by(players_with_distances, & &1.distance)
196+
current_waypoint = hd(bot_state_machine.path_towards_position)
197+
198+
direction =
199+
Vector.sub(current_waypoint, bot_player.position)
200+
|> Vector.normalize()
162201

163202
%{
164-
action: determine_player_move_action(bot_player, closest_player.direction),
203+
action: {:move, direction},
165204
bot_state_machine: bot_state_machine
166205
}
167206
end
@@ -185,8 +224,10 @@ defmodule BotManager.BotStateMachine do
185224
bot_state_machine.current_position
186225
)
187226

227+
new_progress = min(bot_state_machine.cap_for_basic_skill * 3, bot_state_machine.progress_for_basic_skill + distance)
228+
188229
bot_state_machine =
189-
Map.put(bot_state_machine, :progress_for_basic_skill, bot_state_machine.progress_for_basic_skill + distance)
230+
Map.put(bot_state_machine, :progress_for_basic_skill, new_progress)
190231

191232
cond do
192233
Vector.distance(bot_state_machine.previous_position, bot_state_machine.current_position) < 100 &&
@@ -213,9 +254,9 @@ defmodule BotManager.BotStateMachine do
213254
end
214255
end
215256

216-
defp move(bot_player, bot_state_machine, safe_zone_radius) do
257+
defp move(bot_state_machine, safe_zone_radius) do
217258
bot_state_machine =
218-
determine_position_to_move_to(bot_state_machine, safe_zone_radius, System.get_env("PATHFINDING_TEST") == "true")
259+
determine_position_to_move_to(bot_state_machine, safe_zone_radius)
219260

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

229270
%{
230-
action: determine_player_move_action(bot_player, direction),
271+
action: {:move, direction},
231272
bot_state_machine: bot_state_machine
232273
}
233274

@@ -239,7 +280,7 @@ defmodule BotManager.BotStateMachine do
239280
)
240281

241282
%{
242-
action: determine_player_move_action(bot_player, direction),
283+
action: {:move, direction},
243284
bot_state_machine: bot_state_machine
244285
}
245286

@@ -251,53 +292,19 @@ defmodule BotManager.BotStateMachine do
251292
end
252293
end
253294

254-
defp determine_position_to_move_to(bot_state_machine, safe_zone_radius, true = _pathfinding_on) do
255-
cond do
256-
is_nil(bot_state_machine.collision_grid) ->
257-
bot_state_machine
258-
259-
is_nil(bot_state_machine.path_towards_position) ->
260-
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))
261-
262-
from = %{x: bot_state_machine.current_position.x, y: bot_state_machine.current_position.y}
263-
to = %{x: position_to_move_to.x, y: position_to_move_to.y}
264-
265-
shortest_path = AStarNative.a_star_shortest_path(from, to, bot_state_machine.collision_grid)
295+
defp determine_position_to_move_to(%{collision_grid: nil} = bot_state_machine, _safe_zone_radius) do
296+
bot_state_machine
297+
end
266298

267-
# If we don't have a path, retry finding new position in map
268-
if Enum.empty?(shortest_path) do
269-
Map.put(bot_state_machine, :path_towards_position, nil)
270-
|> Map.put(:position_to_move_to, nil)
271-
else
272-
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
273-
|> Map.put(
274-
:path_towards_position,
275-
shortest_path
276-
)
277-
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
278-
end
299+
defp determine_position_to_move_to(bot_state_machine, safe_zone_radius) do
300+
cond do
301+
is_nil(bot_state_machine.path_towards_position) || Enum.empty?(bot_state_machine.path_towards_position) ||
302+
Vector.distance(%{x: 0, y: 0}, bot_state_machine.position_to_move_to) > safe_zone_radius ->
303+
try_pick_random_position_to_move_to(bot_state_machine, safe_zone_radius)
279304

280305
BotStateMachineChecker.current_waypoint_reached?(bot_state_machine) and
281306
BotStateMachineChecker.should_bot_move_to_another_position?(bot_state_machine) ->
282-
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))
283-
284-
from = %{x: bot_state_machine.current_position.x, y: bot_state_machine.current_position.y}
285-
to = %{x: position_to_move_to.x, y: position_to_move_to.y}
286-
287-
shortest_path = AStarNative.a_star_shortest_path(from, to, bot_state_machine.collision_grid)
288-
289-
# If we don't have a path, retry finding new position in map
290-
if Enum.empty?(shortest_path) do
291-
Map.put(bot_state_machine, :path_towards_position, nil)
292-
|> Map.put(:position_to_move_to, nil)
293-
else
294-
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
295-
|> Map.put(
296-
:path_towards_position,
297-
shortest_path
298-
)
299-
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
300-
end
307+
try_pick_random_position_to_move_to(bot_state_machine, safe_zone_radius)
301308

302309
BotStateMachineChecker.current_waypoint_reached?(bot_state_machine) ->
303310
Map.put(bot_state_machine, :path_towards_position, tl(bot_state_machine.path_towards_position))
@@ -307,23 +314,29 @@ defmodule BotManager.BotStateMachine do
307314
end
308315
end
309316

310-
defp determine_position_to_move_to(bot_state_machine, safe_zone_radius, false = _pathfinding_on) do
311-
cond do
312-
is_nil(bot_state_machine.position_to_move_to) ||
313-
not Utils.position_within_radius(bot_state_machine.position_to_move_to, safe_zone_radius) ->
314-
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))
317+
defp try_pick_random_position_to_move_to(bot_state_machine, safe_zone_radius) do
318+
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))
315319

316-
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
317-
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
320+
try_pathing_towards(bot_state_machine, position_to_move_to)
321+
end
318322

319-
BotStateMachineChecker.should_bot_move_to_another_position?(bot_state_machine) ->
320-
position_to_move_to = BotManager.Utils.random_position_within_safe_zone_radius(floor(safe_zone_radius))
323+
defp try_pathing_towards(bot_state_machine, position_to_move_to) do
324+
from = %{x: bot_state_machine.current_position.x, y: bot_state_machine.current_position.y}
325+
to = %{x: position_to_move_to.x, y: position_to_move_to.y}
321326

322-
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
323-
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
327+
shortest_path = AStarNative.a_star_shortest_path(from, to, bot_state_machine.collision_grid)
324328

325-
true ->
326-
bot_state_machine
329+
# If we don't have a path, retry finding new position in map
330+
if Enum.empty?(shortest_path) do
331+
Map.put(bot_state_machine, :path_towards_position, nil)
332+
|> Map.put(:position_to_move_to, nil)
333+
else
334+
Map.put(bot_state_machine, :position_to_move_to, position_to_move_to)
335+
|> Map.put(
336+
:path_towards_position,
337+
shortest_path
338+
)
339+
|> Map.put(:last_time_position_changed, :os.system_time(:millisecond))
327340
end
328341
end
329342
end

0 commit comments

Comments
 (0)