Improve AI play speed#11314
Conversation
| || (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS))) | ||
| && game.getStack().isEmpty() | ||
| && (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) | ||
| && ComputerUtilCost.canPayCost(sa, ai, false) |
There was a problem hiding this comment.
The ability is checked again for affordability by canPlayAndPayForFace, but only after FogAi.canPlay returns. This check is an early exit before ComputerUtil.aiLifeInDanger, which can invoke expensive multiplayer combat forecasting. It prevents an unaffordable Fog from paying that forecasting cost merely to be rejected immediately afterward. reserveManaSources also occurs after the forecast and is only a simplified mana-source check. So it is redundant for the eventual play decision, but not for avoiding the expensive work this PR targets.
The "game.getStack().isEmpty()" is similarly critical: it saves A LOT of time (games can slow down a lot if an AI has a fog in hand and this line is not present).
There was a problem hiding this comment.
reserveManaSources also occurs after the forecast and is only a simplified mana-source check
wrong
There was a problem hiding this comment.
You’re right that I described reserveManaSources too loosely: it invokes the mana-payment planner and is not merely the final source-count comparison. However, the affordability check here is an early-exit optimization, not a replacement for reservation. Java evaluates it before aiLifeInDanger; reserveManaSources is only called after that forecast. In the wide-board unaffordable-Fog benchmark, keeping the check reduced median evaluation time from 57.933 ms to 0.615 ms while preserving the decision and reservation state. The later general affordability check is also not reached during this speculative pre-combat path. So I believe the line should remain, though the earlier explanation was imprecise.
There was a problem hiding this comment.
hmn, I just hate this approach of trying to guard expensive methods with redundant logic
that fact is neither obvious this way and if applied across more places makes everything messier
+ it's not like canPayCost is a very cheap call either...
imo the right solution would be a mix of optimizing the performance inside the cause and trying to keep its AiCache around longer
| private final Map<OutcomeKey, Boolean> destroysBlocker = new HashMap<>(); | ||
| private final Map<PairKey, Integer> blockerDamage = new HashMap<>(); | ||
| private final Map<DamageToKillKey, Integer> damageToKill = new HashMap<>(); | ||
| private final Map<CombatCardKey, Integer> creatureValues = new HashMap<>(); |
There was a problem hiding this comment.
I doubt this needs such specific key
also don't want such hardcoded classes but rather more flexible approach like AiCache
or extending the usage of getCachedCreatureComparator so it can be shared between API checks
There was a problem hiding this comment.
Is your concern just CombatCardKey, or a wider architectural change?
There was a problem hiding this comment.
Made several changes that I hope address your concerns, take a look when you can.
There was a problem hiding this comment.
thanks, will look for cherry pick potential in a while
| delay = FControlGamePlayback.resolveDelay; | ||
| } | ||
| if (delay > 0) { | ||
| if (delay > 0 && !Boolean.getBoolean("forge.game.noPlaybackDelay")) { |
There was a problem hiding this comment.
this seems like your local change?
AI slow-play optimization summary
This patch grew out of reports that AI priority checks and late-game token combat could take seconds—or much longer—at every stop. I added reduced, deterministic reproductions for those paths and used the four-player token Commander profile to identify the repeated work. Changes include:
End-to-end check
The final fixed-seed game used Rhys the Redeemed, Adrix and Nev, Trostani, and Emmara and reproduced a board with roughly 80 attacking tokens plus a large trigger stack. Earlier runs of this scenario did not finish within the five-minute timeout. The final run completed all 39 turns in 102.2 seconds. Because the baseline timed out, the exact improvement cannot be calculated, but this establishes a lower bound of more than 2.9x faster / at least 66% lower wall time. I treat that as end-to-end corroboration rather than adding it to the isolated benchmark results, since whole-game timing is noisy.
The final profile recorded 279 AI evaluations, with 20.3 ms p50, 794.4 ms p95, 1.70 s p99, and 2.59 s maximum. No evaluation reached the 4.5-second timeout. The remaining dominant sampled cost is combat danger forecasting/card-property evaluation, so this patch improves the reported failure mode without claiming that the underlying forecasting algorithm is solved.
All figures above are approximate medians from opt-in wall-clock benchmarks on one machine.
Thanks to Codex for help with code and tests!