Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 8, 2026

  • Initial Refactor
    • Move AI sources from game/modes/ to game/modes/ai/
    • Extract static functions to separate .hpp headers in ai/tasks/
    • Simplify function parameters (pass only necessary fields)
  • Pathfinding improvements
    • Added navmesh_cell_id struct combining island_id + cell for single comparison
    • Refactored check_path_deviation:
      • O(1) check first: are we on current or previous cell?
      • Only then search nearby cells in ±DEVIATION_CHECK_RANGE_V
      • Same logic applies to both rerouting and main paths
    • Rerouting now only targets unoccupied cells OR the target portal cell
    • Rerouting path is trimmed if it coincides with unoccupied cells on main path
    • Fixed advance_path_if_cell_reached: when node_index == 0, checks if in half facing towards next cell
    • Restored get_pathfinding_movement_direction:
      • Returns normalized direction for movement
      • Outputs un-normalized crosshair offset via target_crosshair_offset reference parameter
      • Includes rerouting-to-main path easing for crosshair continuity
    • Added stuck detection:
      • Tracks time spent on same cell via stuck_cell and stuck_time in ai_pathfinding_state
      • When stuck for 2+ seconds (STUCK_ROTATION_INTERVAL_SECS), rotates crosshair offset by 90 degrees
      • Rotation increases by 90 degrees every 2 seconds (90°, 180°, 270°, etc.)
      • Resets stuck timer when the cell changes
  • Debug drawing
    • Added DEBUG_LOGIC_STEP_RECTS for drawing filled quads (4 vec2 corners)
    • debug_draw_pathfinding now draws cell AABBs with smaller alpha (40/255)
    • Renderer updated to handle debug_rects as 2 triangles per rect
    • Added debug_rect constructor taking (pos, size, rotation) using augs::make_rect_points
  • Better random walk
    • Add reusable "random unoccupied cell within n steps" function
    • Handle occupied starting cells with BFS
    • Add world coordinates overload find_random_unoccupied_position_within_steps
  • start_pathfinding for bots
    • Use find_path_across_islands_many (navigate only to next portal)
    • Clear pathfinding state when receiving teleportation message
    • Don't initiate pathfinding while standing on portal cell
  • Pathfinding to a bomb
    • Find closest unoccupied cell the bomb touches
    • Implement fallback for bombs on unwalkable tiles
  • New bot capabilities
    • Post sound_cue_message when footstep fires
    • Make bots hear footsteps and chase using pathfinding
  • Editor testing
    • Spawn character at DEBUG_PATHFINDING_START during playtesting
    • Pathfind to DEBUG_PATHFINDING_END or bomb
    • Added numpad speed control during playtesting (NUMPAD0-7 for 0.01x-10.0x speeds)
Original prompt

Improving bots AI with actual pathfinding

arena_mode_ai.h
arena_mode_ai.cpp
arena_mode_ai_structs.h

These handle the AI logic that is called every step. The bots are currently very simple, they just walk in random directions, shoot player on sight, and go to his last seen position.

I will need more complex bots.

Initial refactor

  • First move these ai sources to game/modes/ai/

  • Now update_arena_mode_ai/post_solve_arena_mode_ai has only high level flow, i want you to move all static functions like static vec2 handle_random_movement to their own .hpp headers and include then in arena_mode_ai.cpp e.g. ai/tasks/handle_random_movement.hpp, so that we don't get lost in code. So all modular "AI tasks" have their own header

  • You're also free to rearrange/refactor any existing AI-related code during this operation.

  • I'm thinking these arguments
    const arena_mode::input& in,
    arena_mode_player& player,
    are used only for one or two fields, and it's an overkill to have to pass them. Just pass what is necessary from them like is_ffa, money amount and cosmos.

New function: fat_line_of_sight

A fat line of sight (FLoS) exists when querying the physics world cache with:
for_each_intersection_with_polygon with a rectangle facing the target has no entities. So imagine a long rotated rectangle whose one short end (its center exactly) touches/covers the source character (can just touch its center pos), and the other short end touches the destination. How wide ('fat') is that rectangle is defined by a parameter and it can be 100 px for now. This is to ensure nothing will actually stand in characer's way.

This will be used instead of normal ray cast whenever testing navigability/LoS for a character in AI - except for when we want a shooting LoS as that does not need to be "fat".

FLoS should use predefined_queries::pathfinding()

Better random walk

Have a reusable "random walkable cell within n steps" function in pathfinding code. This will be used for "random walking" with a sensible distance.

  • if source cell occupied, bfs starting from it to find closest unoccupied first - reuse the function that pathfinding will use to resolve a "starting-from-occupied" situation, details later.
  • once unoccupied cell found, perform "bfs" with n steps, each time going in a random direction (except the one we came from, so always choose 3 dirs and 4 only at the start) - use the stable_rng. Disallow portals.

Most complex task: start_pathfinding

A bot needs to be able to navigate to any point on the map (given its unoccupied). And use it for when chasing the player.

  • First check if there is a direct FLoS to the destination - you can do this once every step during the pathfinding as well.
  • If there is no FLoS, initiate the actual pathfinding algorithm with find_path_across_islands_many.
  • If there is FLoS, navigate directly to target without pathfinding.

Navigating the path

Bot must gradually navigate the pathfinding_path returned from find_path_across_islands_many.

  • arena_mode_ai_state needs to store the pathfinding_path as well as the current cell index on the path, have a separate struct ai_pathfinding_state.
  • index starts with 0 and goes up to the length of std::vector<pathfinding_node> nodes.
  • Every frame movement.flags.set_from_closest_direction must be set from the current character center to the center of the next cell
  • If a bot's pos is within the epsilon of the next cell's center, it advances to the next.
    • If the bot's is pos outside epsilon but
  • Bot's crosshair should always be facing the next cell's center but with easing - e.g. when there are cells 0,1,2, with 1 above 0 and 2 to the left of 1, it should be facing fully up left when in the center of 0, and fully left in the center of 1, but like -45 degrees when halfway between 0 and 1. So when travelling from 0 to 1 we need to take 2's center into consideration when smoothing. What is being smoothed here is the point on the imaginary connected edges between centers, progress being determined from bot's distance to the centers, and always look at that smothed point directly.
  • A bot must somehow detect if it fell off the path. If it is no longer on the cell nodes[index]:
    • check nodes[index - 5 .. index + 5] and find the closest one to the character (length_sq, euclid dist)
    • if the bot is found within any of these cell bounds, actually set the index to that cell, we're still on a path
    • otherwise begin auxiiliary "rerouting" pathfinding to the closest cell. Have a separate std::optional for rerouting pathfinding to not have to recalculate the main path once we're back on track
    • any time we fall from the rerouting pathfinding, recalculate rerouting pathfinding anew as if we just fell from the original path. dont reroute to the rerouting path.
  • At any moment the bot has FLoS to the target position, clear pathfinding state and navigate directly to that target. If we lose FLoS we pathfind again from the current b...

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 5 commits January 8, 2026 20:09
…ons to task headers

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
…ble cell functions

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
…tep listening

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
…I pathfinding

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor AI code structure for improved pathfinding Improve bot AI with actual pathfinding Jan 8, 2026
Copilot AI requested a review from geneotech January 8, 2026 20:26
@geneotech
Copy link
Member

@copilot Fixes:

  • have some global inline functions to check ::is_cell_portal, ::is_cell_walkable, ::is_cell_occupied (in headers defining navmesh likely) and use them instead of
    start_type >= 2, apply this wherever you see comparing to numbers
    also use this nomenclature everywhere:
    0 - cell is walkable and unoccupied
    1 - cell is unwalkable, and occupied

=2 - cell is walkable, but occupied
look through all navmesh/pathfinding logic to fix this, specifically replace all uses of "free" cell to know what is being meant

  • find_random_walkable_cell_within_steps should then be named "unoccupied" cell

  • use vector arithmetic for stuff like this
    const auto dir = directions[chosen_dir];
    current = vec2u(
    static_cast<uint32_t>(static_cast(current.x) + dir.x),
    static_cast<uint32_t>(static_cast(current.y) + dir.y)
    );

  • add a rule to break returns like this
    return
    repro::fabs(bot_pos.x - cell_world.x) <= half_size &&
    repro::fabs(bot_pos.y - cell_world.y) <= half_size
    ;

  • find_closest_unoccupied_cell implements a bfs. please move bfs logic to a separate function in augs/algorithm/bfs.hpp. Add a rule for this in AGENTS.md to separate algorithmic logic there.

  • use is_like_plantable_bomb in editor_setup.cpp to check if its a bomb and save that entity as target not just debug_pathfinding_to_bomb = true. then you dont have to
    cosm.for_each_havinginvariants::hand_fuse(
    [&](const auto& typed_handle) {
    again in test_mode.cpp

forgot to complete this point:

  • If a bot's pos is within the epsilon of the next cell's center, it advances to the next.
    • If the bot is in the next cell already, but outside epsilon, but is in that half of the cell which is facing "away" from the previous cell, advance to the next.

Also: Only do the FLoS checking for pathfinding when there is no portal on the way already. Since there might be actually a line of sight but a portal shortens the distance.

  • Please actually move the relevant ai sources to game/modes/ai/, delete original files and adjust the include paths everywhere.

…ic, is_like_plantable_bomb

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot Fixes:

  • have some global inline functions to check ::is_cell_portal, ::is_cell_walkable, ::is_cell_occupied (in headers defining navmesh likely) and use them instead of
    start_type >= 2, app...

Addressed all feedback in commit 1ad24b4:

  • Added global helper functions is_cell_walkable, is_cell_unoccupied, is_cell_occupied, is_cell_portal, is_cell_unwalkable in cosmos_navmesh.h
  • Updated nomenclature throughout: 0=walkable+unoccupied, 1=unwalkable+occupied, >=2=walkable+occupied (portals)
  • Renamed function to find_random_unoccupied_cell_within_steps
  • Using vector arithmetic: vec2u(vec2i(current) + dir)
  • Fixed return formatting with semicolon on own line per AGENTS.md style
  • Moved BFS to reusable augs::bfs_find_all_matching in augs/algorithm/bfs.hpp
  • Added AGENTS.md rule about separating algorithmic logic
  • Using is_like_plantable_bomb and storing entity_id debug_pathfinding_bomb_target instead of bool

@geneotech
Copy link
Member

@copilot - anyways, scrap fat_line_of_sight completely. always use pathfinding to a destination. make this a member of the physics world cache just in case for the future.

  • i forgot to mention important detail in pathfinding regarding multiple portals.
    • we shall use find_path_across_islands_many instead of find_path_across_islands_many_full
    • this means we only ever navigate up to the next portal. when that cell is reached, we terminate and clear pathfinding state.
    • if the character gets teleported at any time through messages::teleportation (post solve should intercept this), terminate and clear pathfinding state.
    • At any point if i am standing on any portal cell (my center is within a portal cell), do not initialize a pathfinding session whenever there is no active pathfinding and there is a chasing request. Existing pathfinding sessions/reroutings can of course be continued. This is because: portals can sometimes take up to even 500 ms to teleport, or i can be flying inside a portal for a while when i am exiting on the other side which will make the pathfinding results walk through that exit portal first.
    • the next step, AI still knows that it is supposed to chase a specific target. but it sees no active pathfinding session - therefore it initiates one and calculates the path again. now since this will happen only post-teleportation, it will return the path to the next portal or directly to destination.
  • this seems silly
    const auto target_portal_value = target_portal_index.has_value()
    ? static_cast<uint8_t>(2 + target_portal_index.value())
    : static_cast<uint8_t>(0);
    you can just value == target_portal_index because if target_portal_index is nullopt the == will always return false, simplify them

find_closest_unoccupied_cell should actually be named find_closest_walkable_cell since we're filtering for portals too
(we're looking for an actually walkable one) and it seems to me having two separate lambdas is_walkable/is_unoccupied inside is superfluous there.

  • the whole get_parent/set_visited/get_visited logic, as well as even for_each_neighbor
    (with
    const vec2i directions[4] = {
    { 0, -1 },
    { 0, 1 },
    { -1, 0 },
    { 1, 0 }
    };
    )
    seems awfully repeated for me in all pathfinding funcs.
    Implement a pathfinding_graph_view which accepts a context&, pathfinding_context&, possibly island&, and have that structure temporarily created within all pathfinding functions, and have these reusable members implemented there. It should also have wrappers for calling the pathfinding functions with lambdas of its own methods. Then reuse that in all pathfinding functions.

  • bfs_find_all_matching should abort after finding 4 matching walkables, right now it literally traverses entire graph. so it should support for_each_neighbor returning ABORT. and name it aprpopriately bfs_full and dont do
    std::vector results;
    there, this should be on the caller's side.

  • for find_random_unoccupied_cell_within_steps, have an overload find_random_unoccupied_cell_within_steps that accepts a complete navmesh + pos in world coordinates rather than island index + cell index.

Copilot AI and others added 2 commits January 8, 2026 22:16
… find_path_across_islands_many

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot - anyways, scrap fat_line_of_sight completely. always use pathfinding to a destination. make this a member of the physics world cache just in case for the future.

  • i forgot to mention impo...

Addressed in commits 70bc519 and 187c199:

  • Removed fat_line_of_sight completely - always use pathfinding
  • Now using find_path_across_islands_many instead of _full (navigates only to next portal)
  • When reaching a portal cell, pathfinding state is cleared to allow teleportation
  • is_on_portal_cell() helper added - won't initiate new pathfinding while on a portal cell
  • Simplified portal value comparison using value_or(NO_TARGET_PORTAL) constant
  • Renamed find_closest_unoccupied_cellfind_closest_walkable_cell with merged lambdas
  • BFS refactored to bfs_for_each_matching with callback-based abort (stops after MAX_WALKABLE_CANDIDATES = 4)
  • Added find_random_unoccupied_position_within_steps overload for world coordinates
  • Added global constants: CELL_DIRECTIONS, MAX_WALKABLE_CANDIDATES, NO_TARGET_PORTAL

Note: pathfinding_graph_view refactor to consolidate repeated get_parent/set_visited/for_each_neighbor logic is a larger refactor that I can do in a follow-up if needed.

@geneotech
Copy link
Member

@copilot Yes, do the followup refactor.
Also:
Brother, by the Standard, something like this
std::optional() == 2
is always false. you dont have to use value_or(NO_TARGET_PORTAL). Just compare with the std::optional right away.

  • bfs_for_each_matching -> bfs_full since this potentially iterates whole graph

  • pathfinding.rerouting_paths.reset();
    pathfinding.rerouting_path_index = 0;
    pathfinding.rerouting_node_index = 0;
    have this in one struct ("pathfinding_progress" ? ) so just one .reset() is needed to clear the rerouting state.
    this way try_advance can also just take one argument for which path to advance

  • and also why do we have vectors here anyway?
    std::vector<pathfinding_path> paths;
    std::optional<std::vector<pathfinding_path>> rerouting_paths;
    we were supposed to only navigate up to the next portal, so just one segment.
    similarly the rerouting path should only ever navigate to the next segment.

  • advance_path_if_reached -> advance_path_if_cell_reached, more specific

  • break long ifs like this
    if (main_path.island_index >= navmesh.islands.size() ||
    pathfinding.current_node_index >= main_path.nodes.size()
    ) { this at the last line

  • remove src/game/modes/arena_mode_ai.h, replace includes with the new src/game/modes/ai/arena_mode_ai.h

Copilot AI and others added 2 commits January 8, 2026 22:49
…al comparison

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot Yes, do the followup refactor.
Also:
Brother, by the Standard, something like this...

Addressed in commits 9f1b003 and e85b87b:

  • Added pathfinding_progress struct that wraps std::optional<pathfinding_path> + node_index - now pathfinding.rerouting.clear() resets rerouting state with one call
  • Changed ai_pathfinding_state to use main and rerouting as pathfinding_progress instead of vectors - since we only navigate to next portal, single path suffices
  • Renamed bfs_for_each_matchingbfs_full
  • Added is_cell_target_portal(value, optional) helper - no more value_or(255) pattern
  • Created pathfinding_graph_view struct to consolidate repeated lambdas (get_visited/set_visited/set_parent/get_parent/for_each_neighbor/reconstruct_path)
  • Refactored find_path_within_island and find_closest_walkable_cell to use the graph view

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copilot AI and others added 2 commits January 10, 2026 19:42
…r, return un-normalized, add rerouting-to-main easing

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
…inding with smaller alpha

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
@geneotech
Copy link
Member

@copilot You completely broke the get_pathfinding_crosshair function. revert to the prev definition
inline std::optional get_pathfinding_movement_direction(
const ai_pathfinding_state& pathfinding,
const vec2 bot_pos,
const cosmos_navmesh& navmesh,
vec2& target_crosshair_offset
) {
const auto current_target_opt = ::get_current_path_target(pathfinding, navmesh);

if (!current_target_opt.has_value()) {
	/*
		No valid current target, navigate directly to final target.
	*/
	const auto dir = pathfinding.target_position - bot_pos;
	target_crosshair_offset = dir;
	return vec2(dir).normalize();
}

const auto current_target = *current_target_opt;
const auto dir = current_target - bot_pos;

/*
	Calculate smoothed crosshair target by looking ahead on the path.
	When travelling from cell 0 to 1, with cell 2 ahead, the crosshair interpolates
	between looking at cell 1's center (when at cell 0) and cell 2's center (when at cell 1).
	
	The interpolated point lies on the imaginary line from current cell center to next cell center,
	with progress determined by bot's distance to the current cell center.
*/
vec2 look_ahead_target = current_target;

const pathfinding_progress* active_progress_ptr = nullptr;

if (pathfinding.rerouting.has_value()) {
	active_progress_ptr = &*pathfinding.rerouting;
}
else {
	active_progress_ptr = &pathfinding.main;
}

const auto& active_progress = *active_progress_ptr;
const auto& path = active_progress.path;

if (path.island_index < navmesh.islands.size()) {
	const auto& island = navmesh.islands[path.island_index];
	const auto cell_size = static_cast<float>(island.cell_size);

	if (cell_size > 0.0f && active_progress.node_index + 1 < path.nodes.size()) {
		const auto next_target = ::cell_to_world(island, path.nodes[active_progress.node_index + 1].cell_xy);

		/*
			Calculate progress as how close we are to the current cell center.
			At center of current cell (progress = 1.0): look fully at next cell.
			Far from current cell (progress = 0.0): look at current cell.
		*/
		const auto dist_to_current = (bot_pos - current_target).length();
		const auto t = std::clamp(1.0f - dist_to_current / cell_size, 0.0f, 1.0f);

		look_ahead_target = current_target + (next_target - current_target) * t;
	}
}

target_crosshair_offset = look_ahead_target - bot_pos;

return vec2(dir).normalize();

}

the ONLY changes:

  • remove normalization at returns
  • change how this is retrieved
    if (cell_size > 0.0f && active_progress.node_index + 1 < path.nodes.size()) {
    const auto next_target = ::cell_to_world(island, path.nodes[active_progress.node_index + 1].cell_xy);
    if active_progress.node_index == path nodes.size() - 1 then change the next_target. everything else should remain the same, including interpolation

… output param, keep rerouting easing

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 10, 2026

@copilot You completely broke the get_pathfinding_crosshair function. revert to the prev definition
inline std::optional get_pathfinding_movement_direction(
const ai_pathfinding_state& pathfind...

Reverted to get_pathfinding_movement_direction with the original signature (returns normalized direction, outputs crosshair offset via reference parameter) in commit 55f1a53. The rerouting-to-main path easing is preserved for crosshair continuity.

@geneotech
Copy link
Member

@copilot

  • Use augs::make_rect_points for this, better yet have a debug_rect constructor that takes a point, size, rotation and calculates these on its own.
  •                   const auto tl = cell_center + vec2(-half_cell_size, -half_cell_size);
    
  •                   const auto tr = cell_center + vec2(half_cell_size, -half_cell_size);
    
  •                   const auto br = cell_center + vec2(half_cell_size, half_cell_size);
    
  •                   const auto bl = cell_center + vec2(-half_cell_size, half_cell_size);
    
  •                   DEBUG_LOGIC_STEP_RECTS.emplace_back(cell_color, std::array<vec2, 4>{ tl, tr, br, bl });
    
  • the drawn AABBs do not follow the camera they are static on screen. check if you're properly projecting them

…AABB drawing

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 10, 2026

@copilot

  • Use augs::make_rect_points for this, better yet have a debug_rect constructor that takes a point, size, rotation and calculates these on its own.
    ...

Added debug_rect constructor taking (rgba, vec2 pos, vec2 size, real32 rotation_degrees) using augs::make_rect_points. Updated the cell AABB drawing in debug_draw_pathfinding to use this simpler constructor. Commit 94f7296.

@geneotech
Copy link
Member

@copilot In get_pathfinding_movement_direction, if we detect that we've been on the same cell for the past 2 seconds, I'd like the offset to be rotated 90 degrees so that the character is facing with his side to the destination. Increase the angle by 90 degrees every 2 seconds. Reset this offset as soon as we leave the cell.

… stuck on same cell

Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 10, 2026

@copilot In get_pathfinding_movement_direction, if we detect that we've been on the same cell for the past 2 seconds, I'd like the offset to be rotated 90 degrees so that the character is facing with ...

Added stuck detection in commit 8d58210:

  • Tracks time spent on same cell via stuck_cell and stuck_time in ai_pathfinding_state
  • When stuck for 2+ seconds (STUCK_ROTATION_INTERVAL_SECS), rotates crosshair offset by 90 degrees
  • Rotation increases by 90 degrees every 2 seconds (90°, 180°, 270°, etc.)
  • Resets stuck timer when the cell changes

@geneotech geneotech changed the title Improve bot AI with actual pathfinding Implement navigation along the paths for characters Jan 11, 2026
@geneotech geneotech changed the title Implement navigation along the paths for characters Pathfinding: Implement navigation along paths for characters Jan 11, 2026
@geneotech geneotech marked this pull request as ready for review January 11, 2026 19:38
@geneotech geneotech merged commit 2356192 into master Jan 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants