Skip to content

Keep bot death areas out of the stuck area penalty - #2215

Merged
sunzenshen merged 3 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-death-penalty-split
Oct 12, 2026
Merged

sunzenshen merged 3 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-death-penalty-split

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

Before, an area avoid penalty that was added when a bot dies at a location was invalidating potential cover areas when bots were looking for cover during retreats and attacks. This PR separates those two penalty types to allow area penalty checks to distinguish between them.


Since #1739, a bot's death adds 10 to the same per-area penalty that a bot getting stuck adds 1000 to. Path cost reads the sum as a soft cost. The cover and retreat searches read it differently: RetreatToCover, the grenade retreat and the hazard retreat's safe-area search skip any area whose penalty reaches one stuck, and Attack's leapfrog cover search ranks candidates by the penalty before their cover. Stuck areas are already skipped, so in Attack the ranking key is the death count alone: an area where one bot died loses to every area with no deaths that gives even partial cover, and the search does not stop at hard cover that has a death.

On ntre_apparatus_dm (30 bots), 9 % of Attack's cover picks pass over a death area for a cleaner one, and 1.7 % pass over hard cover nearer the goal for it. Deaths alone never reach the skip threshold (at most 17 deaths in one area a match, 170 of the 1000 needed). The skip that does act is the stuck one: 8-22 % of Attack, RetreatToCover and grenade cover picks skip an area where a bot got stuck, and that area stays skipped until the map changes.

What this PR does

Deaths get their own penalty (neo_bot_path_reservation.{h,cpp}, neo_bot_behavior.cpp, neo_bot_path_cost.cpp). OnKilled adds neo_bot_path_reservation_killed_penalty to a separate death penalty, which path cost adds exactly as it added the shared one. The cover, retreat and safe-area searches read only where bots got stuck. Attack's cover ranking then compares stucks only, which the skip leaves at zero, so it falls through to the friendly-reservation and visibility tie-breaks it already had.

Stuck areas are forgotten each round and skipped only on a repeat (second commit; neo_bot_path_reservation.{h,cpp}, the four searches). The stuck penalty is cleared with the other round state in ClearRound (hazards and reservations already reset there), so a stuck in round 1 no longer shapes round 9: without it, by round 5 of a CTG match most RetreatToCover picks skip a stuck area (see Results). The four searches skip an area only once bots got stuck in it twice in the round (NEO_BOT_STUCK_PRONE_AREA_COUNT); below that the stucks are a cost: Attack's cover search ranks by them as before, RetreatToCover counts the area as exposed to one more threat (NEO_BOT_STUCK_AREA_EXPOSURE), grenade cover adds the penalty to its candidate cost as path cost does, and the hazard retreat uses such an area only when no area free of stucks is found. A round clear was chosen over a time decay: it is one moved line beside state that already resets per round, needs no new timestamp or tuning constant, and the stuck areas measured are repeat spots (up to 5-10 stucks in one area a match) that a decay would forget while bots still get stuck there.

An area's penalties live in one struct (third commit; neo_bot_path_reservation.{h,cpp}, path cost and the four searches). AreaPenalty_t holds the stuck and death penalties of an area in one map, so path cost and each search look an area up once and read the sum or the one type they need, and a new penalty type is one more field. A new round zeroes the stuck field in place and map load empties the map, as in the first two commits. The hazard marks stay apart: they are per team and expire by time.

How to test

  1. map ntre_apparatus_dm with ntre_apparatus_dm: first navmesh neoAssets#157's navmesh, sv_cheats 1, neo_bot_quota 12, sv_neo_bot_attack_debug_cover 1, and join spectator.
  2. Watch Attack bots near a spot where several bots have died. Without this PR, the yellow cover area sometimes skips hard cover where a bot died for partial cover nearby; with it, deaths there no longer decide the pick.

Cost

One CUtlMap lookup per area in path cost and in each cover or safe-area search, as upstream has for the shared penalty.

Results

Every match below: Linux dedicated servers built from upstream master at #2175 with #2198, #2176 and #2212, without this PR, with the first commit, and with both; the same test-only logging (kills, stuck events, move failures, round and match end, and a cover-pick log of what each cover and retreat search picked and skipped) in every arm; that logging is a harness patch and not part of this PR. Release builds for the rates. The Debug build for the assert check is upstream master with this PR alone, plus the same logging. Default weapon damage (1.485), 15 s pre-round freeze; the arms compared in a table ran interleaved in the same batches of 8 parallel servers. Every match reached match end with 0 asserts and 0 crashes. "Stuck" is OnStuck plus FAIL_STUCK, without the round-start spawn crowd, SeekWeapon and dead bots; an episode is one bot's stuck events less than 2 s apart. Intervals are 95 % bootstrap intervals (4000 draws) of the paired ratio.

ntre_apparatus_dm, #157's navmesh head, one-round 5-minute DM; 48 pairs at 30 bots; at 12 bots 24 pairs for the first commit and 48 for both (a repeat batch of 24 pairs of the two-commit build, pooled; the "without" means, the cover rows and the deaths row are from the first 24):

a match 30 bots without first commit both 12 bots without first commit both (48 pairs)
Attack picks passing over a death area 9.2 % 0 0.4 % 5.0 % 0 0.3 %
... passing over hard cover for it 1.7 % 0 0 0.4 % 0 0
RetreatToCover picks skipping a stuck area 22.4 % 24.5 % 19.4 % 21.1 % 23.0 % 19.4 %
cover reached, Attack / RetreatToCover 245 / 185 239 / 188 246 / 191 67 / 43 72 / 45 74 / 43
stuck episodes 8.4 x0.99 [0.86, 1.15] x1.03 [0.89, 1.19] 8.2 x1.09 [0.95, 1.26] x0.98 [0.84, 1.12]
fall deaths 5.0 x0.83 [0.69, 1.01] x0.90 [0.78, 1.03] 2.6 x1.24 [0.97, 1.62] x1.19 [0.93, 1.52]
FAIL_NO_PATH 26.4 x0.95 [0.81, 1.11] x0.93 [0.81, 1.08] 5.4 x0.95 [0.72, 1.26] x0.94 [0.75, 1.17]
player kills 245.8 x1.01 [1.00, 1.03] x1.00 [0.99, 1.01] 65.8 x1.01 [0.97, 1.05] x1.00 [0.97, 1.03]
deaths 250.7 x1.01 [1.00, 1.03] x1.00 [0.99, 1.01] 68.3 x1.02 [0.98, 1.06] x1.01 [0.98, 1.04] (first 24)

CTG (both commits; ntre_dusk_ctg, ntre_isolation_ctg, ghost spawn 0, 3-minute round, 1 round, 30 bots, 8 pairs each): stuck episodes x1.01 [0.72, 1.43] and x1.09 [0.68, 1.51], kills x1.06 [0.97, 1.16] and x1.03 [0.94, 1.13], no fall deaths; captures 1 -> 0 on each map.

Debug build (upstream master with this PR): 8 ntre_apparatus_dm matches (#157's navmesh head), ntre_dusk_ctg and ntre_isolation_ctg (ghost spawn 0) and ntre_sentinel_jgr (juggernaut spawn 0), 30 bots: 11 of 11 at match end, 0 asserts, 0 crashes.

Five-round CTG (both commits; ntre_dusk_ctg, ntre_isolation_ctg, ghost spawn 0, 4 pairs each): without this PR, the share of RetreatToCover picks that skip an area where a bot got stuck grows each round, from 16 % to 87 % by round 5 on ntre_dusk_ctg and from 11 % to 72 % on ntre_isolation_ctg; with it, it stays between 1 % and 9 %. Stuck events a round 11.6 -> 8.8 and 8.5 -> 6.8 (stuck events after match end left out), kills a round 24.3 -> 23.2 and 23.1 -> 24.8.

Third commit (storage only): the build with all three commits against the build with the first two, 30 bots, 8 pairs on ntre_apparatus_dm: stuck episodes x0.98 [0.65, 1.41], kills x1.01 [0.97, 1.06], FAIL_NO_PATH x1.09 [0.76, 1.69], cover-pick rates within a few points. Debug build of all three commits: 4 ntre_apparatus_dm, ntre_dusk_ctg (ghost spawns 0 and 2) and ntre_sentinel_jgr (juggernaut spawns 0 and 4), 30 bots, 0 asserts, 0 crashes.

Not run: TDM and JGR maps in Release, the other ntre_ maps, and a Windows build.

Toolchain

  • Linux GCC Distro Native (Linux Mint 22.3, GCC 13.3.0)

Linked Issues

A bot's death added to the same per-area penalty as a bot getting
stuck. Cover and retreat searches skip an area whose penalty reaches
one stuck, and Attack's cover search ranks candidates by it before
their cover, so an area where bots died lost to any area with fewer
deaths even when it gave better cover.

Keep deaths in their own penalty, read only as the path cost it was
before; cover and retreat searches read only where bots got stuck.
A bot getting stuck once made its area unusable as cover, retreat goal
or safe area for every bot until the map changed: 9-21 % of cover and
retreat picks on ntre_apparatus_dm skipped such an area. One stuck can
be a crowd or a pushed prop.

Clear stuck areas with the other round state, and skip an area only
after bots got stuck there twice in the round. Below that, the stucks
are a cost: Attack already ranks cover by them, RetreatToCover counts
the area as exposed to one more threat, grenade cover adds them as path
cost does, and a hazard retreat uses such areas only as a fallback.
The stuck and death penalties lived in two maps, so path cost and the
cover and retreat searches looked an area up once per penalty type,
two or three times per area.

Keep both in one map of per-area structs: each reader looks the area
up once and reads the sum or the type it needs, and a new penalty type
is one more field. A new round clears the stuck field in place and map
load clears the map, as before. The hazard marks stay apart: they are
per team and expire by time, not by round.
@sunzenshen
sunzenshen requested a review from sunmachine October 10, 2026 03:31
@sunzenshen
sunzenshen marked this pull request as ready for review October 10, 2026 03:31
@sunzenshen sunzenshen added the Bots Related to bot players label Oct 10, 2026

@sunmachine sunmachine left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to work as intended! Code looks good, no issues.

@sunmachine
sunmachine requested a review from a team October 11, 2026 19:39
@sunzenshen
sunzenshen merged commit 9a8b63d into NeotokyoRebuild:master Oct 12, 2026
16 of 17 checks passed
@sunzenshen
sunzenshen removed the request for review from a team October 12, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bots Related to bot players

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants