Skip to content

AI perf follow-ups: fog check caching, tap/untap scan zones, chump block re-evaluation#11160

Open
Blackvipe99 wants to merge 3 commits into
Card-Forge:masterfrom
Blackvipe99:perf-ai-scans
Open

AI perf follow-ups: fog check caching, tap/untap scan zones, chump block re-evaluation#11160
Blackvipe99 wants to merge 3 commits into
Card-Forge:masterfrom
Blackvipe99:perf-ai-scans

Conversation

@Blackvipe99

Copy link
Copy Markdown
Contributor

Follow-up to #11157, continuing the AI slowdown work from #3410. Three changes, all targeting full-game card scans and redundant recomputation in the AI's decision hot paths.

1. Cache isPreventCombatDamageThisTurn() per game timestamp (ReplacementHandler)

Even zone-restricted (#11157), the fog check still runs once per attacker per hypothetical combat per candidate spell ability. The set of active fog-type effects only changes together with events that advance the game timestamp (zone changes — fog effect cards entering/leaving the command zone — and ability-granting effects), so the result is now cached and recomputed only when game.getTimestamp() changes. Fields are volatile; a benign race recomputes the same value. Reviewer note: this assumes any event that adds/removes a matching replacement effect also advances the game timestamp — true for the Effect-card lifecycle that fog effects use, but please sanity-check against edge cases I may not know about.

2. Skip hidden zones when scanning for Tap/Untap replacement effects (ReplacementHandler.getReplacementList)

Profiling AI timeouts (captured eval-thread stacks at the moment of timeout) showed the dominant remaining stall was not combat prediction but Card.canTap/canUntapcantHappenCheckgetReplacementListforEachCardInGame, reached thousands of times per AI decision via ComputerUtilMana.groupSourcesByManaColor (per mana source per cost check) and CreatureEvaluator (per creature evaluation). Sample stack:

at forge.game.card.Card.updateReplacementEffects(Card.java:7044)
at forge.game.replacement.ReplacementHandler.lambda$getReplacementList$0(ReplacementHandler.java:119)
at forge.game.Game.forEachCardInGame(Game.java:751)
at forge.game.replacement.ReplacementHandler.getReplacementList(ReplacementHandler.java:101)
at forge.game.replacement.ReplacementHandler.cantHappenCheck(ReplacementHandler.java:156)
at forge.game.card.Card.canTap(Card.java:4679)
at forge.game.cost.CostTap.canPay(CostTap.java:69)
...
at forge.ai.ComputerUtilMana.groupSourcesByManaColor(ComputerUtilMana.java:1573)

For ReplacementType.Tap/Untap events the scan now skips cards in zones outside STATIC_ABILITIES_SOURCE_ZONES — no tap/untap-replacing effect functions from a library or hand, and the existing zonesCheck already rejected them after visiting (and after paying getReplacementEffects()'s rebuild cost per card). In multiplayer Commander this cuts each of those scans by the ~350+ cards sitting in libraries.

3. Avoid redundant lifeInDanger re-evaluation in makeChumpBlocks (AiBlockController)

makeChumpBlocks re-ran lifeInDanger (a full combat damage prediction over all attackers) at every recursion level — once per attacker — making block assignment O(attackers² × board). The recursion now only re-evaluates when a blocker was actually assigned since the last check; skipping an attacker leaves the combat unchanged, so the previous result still holds. Decisions are identical by construction.

Testing

  • Full build + headless sims: constructed 1v1 and 4-player FFA with token-swarm decks and a fog stall deck — games complete normally, fog effects verified applying to combat damage, no exceptions.
  • Benchmark: 4-player Commander mirror of a Krenko, Mob Boss goblin deck (worst-case wide boards), MATCH_AI_TIMEOUT=3: before these changes the AI hit the timeout during the match; after, zero timeouts across repeated full games.
  • Real 5-player Commander play with these patches: no AI decision exceeded the 3s timeout over a full game session.

…d redundant lifeInDanger re-evaluation

Follow-up to Card-Forge#11157 / Card-Forge#3410. Caches isPreventCombatDamageThisTurn per
game timestamp, restricts Tap/Untap replacement scans to zones such
effects can be active from, and makes makeChumpBlocks re-run the full
combat damage prediction only when a blocker was assigned since the
last check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java Outdated
Review feedback: keep the engine free of caching/AI concerns. The
engine-side timestamp cache is reverted; staticDamagePrevention gains
an overload taking an optional precomputed answer, and the AI's
predictDamageTo supplies it from AiCache (cleared per AI decision).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
// tap/untap events can only be replaced by effects active from open zones;
// don't scan libraries and hands for them - this is a major hot path, as
// canTap/canUntap run a cantHappenCheck per mana source per AI cost check
if ((event == ReplacementType.Tap || event == ReplacementType.Untap)

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.

let's just guard it by Spell.performanceMode on the off chance some custom card needs it to work:
then you can also make the constraints even tighter since only Ood Sphere should care for it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 0c9c6da — the skip now only applies when performance mode is on, and the allowed zones are tightened to Battlefield + Command (grepped cardsfolder: every Event$ Tap/Untap RE is ActiveZones$ Battlefield except Ood Sphere's goad effect and the Edge of Malacol / Imprison This Insolent Wretch planechase/archenemy ones, which all sit in Command).

…ield + command

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants