Skip to content

Commit 211cb85

Browse files
Blackvipe99claude
andauthored
AI perf follow-ups: fog check caching, tap/untap scan zones, chump block re-evaluation (#11160)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c57a325 commit 211cb85

5 files changed

Lines changed: 54 additions & 7 deletions

File tree

forge-ai/src/main/java/forge/ai/AiBlockController.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,14 @@ private void makeChumpBlocks(final Combat combat) {
649649
}
650650

651651
private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
652-
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
652+
makeChumpBlocks(combat, attackers, true);
653+
}
654+
655+
// recheckDanger: lifeInDanger runs a full combat damage prediction, so only
656+
// re-evaluate it when a blocker was assigned since the last check - skipping
657+
// an attacker leaves the combat unchanged and the previous result still holds
658+
private void makeChumpBlocks(final Combat combat, List<Card> attackers, boolean recheckDanger) {
659+
if (recheckDanger && !ComputerUtilCombat.lifeInDanger(ai, combat)) {
653660
lifeInDanger = false;
654661
return;
655662
}
@@ -663,10 +670,11 @@ private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
663670
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)
664671
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
665672
attackers.remove(0);
666-
makeChumpBlocks(combat, attackers);
673+
makeChumpBlocks(combat, attackers, false);
667674
return;
668675
}
669676

677+
boolean blocked = false;
670678
List<Card> chumpBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
671679
if (!chumpBlockers.isEmpty()) {
672680
final Card blocker = ComputerUtilCard.getWorstCreatureAI(chumpBlockers);
@@ -688,7 +696,7 @@ private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
688696
attackersLeft.remove(other);
689697
blockedButUnkilled.add(other);
690698
attackers.remove(other);
691-
makeChumpBlocks(combat, attackers);
699+
makeChumpBlocks(combat, attackers, true);
692700
return;
693701
}
694702
}
@@ -698,9 +706,10 @@ private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
698706
combat.addBlocker(attacker, blocker);
699707
attackersLeft.remove(attacker);
700708
blockedButUnkilled.add(attacker);
709+
blocked = true;
701710
}
702711
attackers.remove(0);
703-
makeChumpBlocks(combat, attackers);
712+
makeChumpBlocks(combat, attackers, blocked);
704713
}
705714

706715
// Block creatures with "can't be blocked except by two or more creatures"

forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2228,11 +2228,20 @@ public final static int predictDamageTo(final GameEntity target, final int damag
22282228
int restDamage = damage;
22292229

22302230
restDamage = target.staticReplaceDamage(restDamage, source, isCombat);
2231-
restDamage = target.staticDamagePrevention(restDamage, possiblePrevention, source, isCombat);
2231+
restDamage = target.staticDamagePrevention(restDamage, possiblePrevention, source, isCombat,
2232+
isCombat ? isCombatDamagePreventedThisTurnCached(target.getGame()) : null);
22322233

22332234
return restDamage;
22342235
}
22352236

2237+
// cached per AI decision (AiCache is cleared in chooseSpellAbilityToPlay);
2238+
// predictions ask this once per attacker otherwise
2239+
private static Boolean isCombatDamagePreventedThisTurnCached(final Game game) {
2240+
return AiCache.getCached("isPreventCombatDamageThisTurn",
2241+
() -> game.getReplacementHandler().isPreventCombatDamageThisTurn(),
2242+
List.of(AiCache::identity), game);
2243+
}
2244+
22362245
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
22372246
if (combatant.hasFirstStrike() || combatant.hasDoubleStrike()) {
22382247
return true;

forge-game/src/main/java/forge/game/GameEntity.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,27 @@ public void setName(final String s) {
7979
// This should be also usable by the AI to forecast an effect (so it must
8080
// not change the game state)
8181
public int staticDamagePrevention(int damage, final int possiblePrevention, final Card source, final boolean isCombat) {
82+
return staticDamagePrevention(damage, possiblePrevention, source, isCombat, null);
83+
}
84+
85+
// combatDamagePreventedThisTurn: optional precomputed result of
86+
// ReplacementHandler.isPreventCombatDamageThisTurn, so the AI can cache it
87+
// instead of re-running the check for every attacker of a predicted combat
88+
public int staticDamagePrevention(int damage, final int possiblePrevention, final Card source, final boolean isCombat, final Boolean combatDamagePreventedThisTurn) {
8289
if (damage <= 0) {
8390
return 0;
8491
}
8592
if (!source.canDamagePrevented(isCombat)) {
8693
return damage;
8794
}
8895

89-
if (isCombat && getGame().getReplacementHandler().isPreventCombatDamageThisTurn()) {
90-
return 0;
96+
if (isCombat) {
97+
final boolean prevented = combatDamagePreventedThisTurn != null
98+
? combatDamagePreventedThisTurn.booleanValue()
99+
: getGame().getReplacementHandler().isPreventCombatDamageThisTurn();
100+
if (prevented) {
101+
return 0;
102+
}
91103
}
92104

93105
for (final Card ca : getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {

forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import forge.game.player.Player;
4343
import forge.game.player.PlayerCollection;
4444
import forge.game.spellability.AbilitySub;
45+
import forge.game.spellability.Spell;
4546
import forge.game.spellability.SpellAbility;
4647
import forge.game.zone.Zone;
4748
import forge.game.zone.ZoneType;
@@ -102,6 +103,18 @@ public List<ReplacementEffect> getReplacementList(final ReplacementType event, f
102103
Card c = preList.get(crd);
103104
Zone cardZone = game.getZoneOf(c);
104105

106+
// all tap/untap replacements are active from the battlefield or the command zone
107+
// (e.g. Ood Sphere); skip other zones - this is a major hot path, as canTap/canUntap
108+
// run a cantHappenCheck per mana source per AI cost check
109+
// (performance mode only, in case a custom card wants one active from elsewhere)
110+
if (Spell.isPerformanceMode()
111+
&& (event == ReplacementType.Tap || event == ReplacementType.Untap)
112+
&& cardZone != null
113+
&& cardZone.getZoneType() != ZoneType.Battlefield
114+
&& cardZone.getZoneType() != ZoneType.Command) {
115+
return true;
116+
}
117+
105118
// only when not prelist
106119
boolean noLKIstate = c != crd || event != ReplacementType.Moved || c.isImmutable() || runParams.get(AbilityKey.LastStateBattlefield) == null;
107120
if (!noLKIstate) {

forge-game/src/main/java/forge/game/spellability/Spell.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ public static void setPerformanceMode(boolean performanceMode){
5454
Spell.performanceMode=performanceMode;
5555
}
5656

57+
public static boolean isPerformanceMode() {
58+
return performanceMode;
59+
}
60+
5761
private boolean castFaceDown = false;
5862

5963
public Spell(final Card sourceCard, final Cost abCost) {

0 commit comments

Comments
 (0)