Refactor Telekinesis (#7174)

* Improve AI check
This commit is contained in:
tool4ever
2025-03-17 13:06:03 +01:00
committed by GitHub
parent 8f8d6e6e30
commit a57a1f566a
13 changed files with 61 additions and 44 deletions

View File

@@ -265,7 +265,7 @@ public class AiController {
} }
} }
if (!re.requirementsCheck(game, AbilityKey.newMap())) { if (!re.requirementsCheck(game)) {
continue; continue;
} }
SpellAbility exSA = re.getOverridingAbility(); SpellAbility exSA = re.getOverridingAbility();

View File

@@ -96,7 +96,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
if (!re.zonesCheck(getGame().getZoneOf(ca))) { if (!re.zonesCheck(getGame().getZoneOf(ca))) {
continue; continue;
} }
if (!re.requirementsCheck(getGame(), AbilityKey.newMap())) { if (!re.requirementsCheck(getGame())) {
continue; continue;
} }
// Immortal Coil prevents the damage but has a similar negative effect // Immortal Coil prevents the damage but has a similar negative effect

View File

@@ -140,10 +140,6 @@ public enum AbilityKey {
Valiant("Valiant"), Valiant("Valiant"),
Won("Won"), Won("Won"),
// for AI prediction
Phase("Phase"),
PlayerTurn("PlayerTurn"),
// below used across different Replacements, don't reuse // below used across different Replacements, don't reuse
InternalTriggerTable("InternalTriggerTable"), InternalTriggerTable("InternalTriggerTable"),
SimultaneousETB("SimultaneousETB"); // for CR 614.13c SimultaneousETB("SimultaneousETB"); // for CR 614.13c

View File

@@ -1856,6 +1856,10 @@ public class AbilityUtils {
return doXMath(list.size(), expr, c, ctb); return doXMath(list.size(), expr, c, ctb);
} }
if (sq[0].equals("ActivatedThisGame")) {
return doXMath(sa.getActivationsThisGame(), expr, c, ctb);
}
if (sq[0].equals("ResolvedThisTurn")) { if (sq[0].equals("ResolvedThisTurn")) {
return doXMath(sa.getResolvedThisTurn(), expr, c, ctb); return doXMath(sa.getResolvedThisTurn(), expr, c, ctb);
} }

View File

@@ -65,8 +65,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
ctype = CounterType.getType(sa.getParam("CounterType")); ctype = CounterType.getType(sa.getParam("CounterType"));
} }
final Player pl = !sa.hasParam("DefinedPlayer") ? sa.getActivatingPlayer() : final Player pl = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
final boolean eachExisting = sa.hasParam("EachExistingCounter"); final boolean eachExisting = sa.hasParam("EachExistingCounter");
GameEntityCounterTable table = new GameEntityCounterTable(); GameEntityCounterTable table = new GameEntityCounterTable();
@@ -79,7 +78,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) { if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) {
continue; continue;
} }
if (!eachExisting && sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null, if (sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(), Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(),
CardTranslation.getTranslatedName(gameCard.getName())), null)) { CardTranslation.getTranslatedName(gameCard.getName())), null)) {
continue; continue;
@@ -114,8 +113,6 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove"); String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove");
CounterType chosenType = pc.chooseCounterType(list, sa, prompt, params); CounterType chosenType = pc.chooseCounterType(list, sa, prompt, params);
params.put("CounterType", chosenType);
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
boolean putCounter; boolean putCounter;
if (sa.hasParam("RemoveConditionSVar")) { if (sa.hasParam("RemoveConditionSVar")) {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
@@ -137,6 +134,8 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
} else if (!canReceive && canRemove) { } else if (!canReceive && canRemove) {
putCounter = false; putCounter = false;
} else { } else {
params.put("CounterType", chosenType);
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
putCounter = pc.chooseBinary(sa, prompt, BinaryChoiceType.AddOrRemove, params); putCounter = pc.chooseBinary(sa, prompt, BinaryChoiceType.AddOrRemove, params);
} }
} }

View File

@@ -65,6 +65,7 @@ import org.apache.commons.lang3.tuple.Triple;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.FutureTask;
import static java.lang.Math.max; import static java.lang.Math.max;
@@ -4912,22 +4913,23 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return true; return true;
} }
public final boolean canUntap(Player phase, boolean predict) { public final boolean canUntap(Player phase, Boolean predict) {
if (!tapped) { return false; } if (predict != null && predict) {
FutureTask<Boolean> proc = new FutureTask<>(() -> {
return canUntap(phase, null);
});
return getGame().getPhaseHandler().withContext(proc, phase, PhaseType.UNTAP);
}
if (predict != null && !tapped) { return false; }
if (phase != null && isExertedBy(phase)) { if (phase != null && isExertedBy(phase)) {
return false; return false;
} }
if (phase != null && if (phase != null &&
(hasKeyword("CARDNAME doesn't untap during your untap step.") (hasKeyword("CARDNAME doesn't untap during your untap step.")
|| hasKeyword("This card doesn't untap during your next untap step.") || hasKeyword("This card doesn't untap during your next untap step."))) {
|| hasKeyword("This card doesn't untap during your next two untap steps."))) {
return false; return false;
} }
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(this); Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(this);
if (predict) {
runParams.put(AbilityKey.PlayerTurn, phase);
runParams.put(AbilityKey.Phase, PhaseType.UNTAP);
}
return !getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Untap, runParams); return !getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Untap, runParams);
} }

View File

@@ -49,6 +49,8 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.time.StopWatch; import org.apache.commons.lang3.time.StopWatch;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/** /**
@@ -137,6 +139,23 @@ public class PhaseHandler implements java.io.Serializable {
setPriority(playerTurn); setPriority(playerTurn);
} }
public <T> T withContext(FutureTask<T> original, Player active, PhaseType pt) {
Player oldTurn = playerTurn;
PhaseType oldPhase = phase;
playerTurn = active;
phase = pt;
original.run();
try {
return original.get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
} finally {
playerTurn = oldTurn;
phase = oldPhase;
}
return null;
}
public final boolean inCombat() { return combat != null; } public final boolean inCombat() { return combat != null; }
public final Combat getCombat() { return combat; } public final Combat getCombat() { return combat; }
@@ -185,9 +204,7 @@ public class PhaseHandler implements java.io.Serializable {
playerTurn.setNumPowerSurgeLands(lands); playerTurn.setNumPowerSurgeLands(lands);
} }
// Replacement effects
final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(playerTurn); final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(playerTurn);
repRunParams.put(AbilityKey.Phase, phase);
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.BeginPhase, repRunParams); ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.BeginPhase, repRunParams);
if (repres != ReplacementResult.NotReplaced) { if (repres != ReplacementResult.NotReplaced) {
// Currently there is no effect to skip entire beginning phase // Currently there is no effect to skip entire beginning phase

View File

@@ -22,7 +22,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.game.Game; import forge.game.Game;
@@ -164,10 +163,6 @@ public class Untap extends Phase {
// TODO Replace with Static Abilities // TODO Replace with Static Abilities
for (final Card c : active.getCardsIn(ZoneType.Battlefield)) { for (final Card c : active.getCardsIn(ZoneType.Battlefield)) {
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next untap step."); c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next untap step.");
if (c.hasKeyword("This card doesn't untap during your next two untap steps.")) {
c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next two untap steps.");
c.addHiddenExtrinsicKeywords(game.getNextTimestamp(), 0, Lists.newArrayList("This card doesn't untap during your next untap step."));
}
} }
// remove exerted flags from all things in play // remove exerted flags from all things in play

View File

@@ -154,28 +154,26 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
* *
* @return a boolean. * @return a boolean.
*/ */
public boolean requirementsCheck(Game game, Map<AbilityKey, Object> runParams) { public boolean requirementsCheck(Game game) {
if (this.isSuppressed()) { if (this.isSuppressed()) {
return false; // Effect removed by effect return false; // Effect removed by effect
} }
if (hasParam("PlayerTurn")) { if (hasParam("PlayerTurn")) {
Player active = (Player) runParams.getOrDefault(AbilityKey.PlayerTurn, game.getPhaseHandler().getPlayerTurn());
if (getParam("PlayerTurn").equals("True")) { if (getParam("PlayerTurn").equals("True")) {
if (!active.equals(getHostCard().getController())) { if (!game.getPhaseHandler().isPlayerTurn(getHostCard().getController())) {
return false; return false;
} }
} else { } else {
List<Player> players = AbilityUtils.getDefinedPlayers(getHostCard(), getParam("PlayerTurn"), this); List<Player> players = AbilityUtils.getDefinedPlayers(getHostCard(), getParam("PlayerTurn"), this);
if (!players.contains(active)) { if (!players.contains(game.getPhaseHandler().getPlayerTurn())) {
return false; return false;
} }
} }
} }
if (hasParam("ActivePhases")) { if (hasParam("ActivePhases")) {
PhaseType phase = (PhaseType) runParams.getOrDefault(AbilityKey.Phase, game.getPhaseHandler().getPhase()); if (!PhaseType.parseRange(getParam("ActivePhases")).contains(game.getPhaseHandler().getPhase())) {
if (!PhaseType.parseRange(getParam("ActivePhases")).contains(phase)) {
return false; return false;
} }
} }

View File

@@ -18,6 +18,7 @@
package forge.game.replacement; package forge.game.replacement;
import java.util.*; import java.util.*;
import java.util.concurrent.FutureTask;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import forge.game.card.*; import forge.game.card.*;
@@ -127,7 +128,7 @@ public class ReplacementHandler {
&& replacementEffect.modeCheck(event, runParams) && replacementEffect.modeCheck(event, runParams)
&& !possibleReplacers.contains(replacementEffect) && !possibleReplacers.contains(replacementEffect)
&& replacementEffect.zonesCheck(cardZone) && replacementEffect.zonesCheck(cardZone)
&& replacementEffect.requirementsCheck(game, runParams) && replacementEffect.requirementsCheck(game)
&& replacementEffect.canReplace(runParams)) { && replacementEffect.canReplace(runParams)) {
possibleReplacers.add(replacementEffect); possibleReplacers.add(replacementEffect);
} }
@@ -855,15 +856,17 @@ public class ReplacementHandler {
* Helper function to check if a phase would be skipped for AI. * Helper function to check if a phase would be skipped for AI.
*/ */
public boolean wouldPhaseBeSkipped(final Player player, final PhaseType phase) { public boolean wouldPhaseBeSkipped(final Player player, final PhaseType phase) {
final Map<AbilityKey, Object> repParams = AbilityKey.newMap(); FutureTask<Boolean> proc = new FutureTask<>(() -> {
repParams.put(AbilityKey.PlayerTurn, player); final Map<AbilityKey, Object> repParams = AbilityKey.newMap();
repParams.put(AbilityKey.Phase, phase); List<ReplacementEffect> list = getReplacementList(ReplacementType.BeginPhase, repParams, ReplacementLayer.Control);
List<ReplacementEffect> list = getReplacementList(ReplacementType.BeginPhase, repParams, ReplacementLayer.Control); if (list.isEmpty()) {
if (list.isEmpty()) { return false;
return false; }
} return true;
return true; });
return player.getGame().getPhaseHandler().withContext(proc, player, phase);
} }
/** /**
* Helper function to check if an extra turn would be skipped for AI. * Helper function to check if an extra turn would be skipped for AI.
*/ */

View File

@@ -3,7 +3,7 @@ ManaCost:1 U U
Types:Creature Human Wizard Types:Creature Human Wizard
PT:1/2 PT:1/2
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Opponent | Execute$ TrigSkipPhase | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of each opponent's upkeep, that player chooses draw step, main phase, or combat phase. The player skips each instance of the chosen step or phase this turn. T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Opponent | Execute$ TrigSkipPhase | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of each opponent's upkeep, that player chooses draw step, main phase, or combat phase. The player skips each instance of the chosen step or phase this turn.
SVar:TrigSkipPhase:DB$ GenericChoice | Defined$ TriggeredPlayer | Choices$ FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat | ShowChoice$ ExceptSelf | AILogic$ Random SVar:TrigSkipPhase:DB$ GenericChoice | Defined$ TriggeredPlayer | Choices$ FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat | ShowChoice$ ExceptSelf | AILogic$ Fatespinner
SVar:FatespinnerSkipDraw:DB$ SkipPhase | Defined$ TriggeredPlayer | Step$ Draw | Duration$ EndOfTurn | SpellDescription$ Draw step SVar:FatespinnerSkipDraw:DB$ SkipPhase | Defined$ TriggeredPlayer | Step$ Draw | Duration$ EndOfTurn | SpellDescription$ Draw step
SVar:FatespinnerSkipMain:DB$ SkipPhase | Defined$ TriggeredPlayer | Phase$ Main | Duration$ EndOfTurn | SpellDescription$ Main phase SVar:FatespinnerSkipMain:DB$ SkipPhase | Defined$ TriggeredPlayer | Phase$ Main | Duration$ EndOfTurn | SpellDescription$ Main phase
SVar:FatespinnerSkipCombat:DB$ SkipPhase | Defined$ TriggeredPlayer | Phase$ BeginCombat | Duration$ EndOfTurn | SpellDescription$ Combat phase SVar:FatespinnerSkipCombat:DB$ SkipPhase | Defined$ TriggeredPlayer | Phase$ BeginCombat | Duration$ EndOfTurn | SpellDescription$ Combat phase

View File

@@ -3,6 +3,9 @@ ManaCost:U U
Types:Instant Types:Instant
A:SP$ Tap | ValidTgts$ Creature | SubAbility$ DBEffect | SpellDescription$ Tap target creature. A:SP$ Tap | ValidTgts$ Creature | SubAbility$ DBEffect | SpellDescription$ Tap target creature.
SVar:DBEffect:DB$ Effect | ReplacementEffects$ RPrevent | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | SubAbility$ DBPump | SpellDescription$ Prevent all combat damage that would be dealt by that creature this turn. SVar:DBEffect:DB$ Effect | ReplacementEffects$ RPrevent | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | SubAbility$ DBPump | SpellDescription$ Prevent all combat damage that would be dealt by that creature this turn.
SVar:DBPump:DB$ Pump | Defined$ Targeted | KW$ HIDDEN This card doesn't untap during your next two untap steps. | Duration$ Permanent | SpellDescription$ It doesn't untap during its controller's next two untap steps. | StackDescription$ SpellDescription SVar:DBPump:DB$ Effect | ReplacementEffects$ RUntap | Triggers$ ExileEff | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | Duration$ Permanent | StackDescription$ SpellDescription
SVar:RPrevent:Event$ DamageDone | Prevent$ True | IsCombat$ True | ValidSource$ Card.IsRemembered | Description$ Prevent all combat damage that would be dealt by that creature this turn. SVar:RPrevent:Event$ DamageDone | Prevent$ True | IsCombat$ True | ValidSource$ Card.IsRemembered | Description$ Prevent all combat damage that would be dealt by that creature this turn.
SVar:RUntap:Event$ Untap | ValidCard$ Creature.IsRemembered+ActivePlayerCtrl | Layer$ CantHappen | ActivePhases$ Untap | Description$ It doesn't untap during its controller's next two untap steps.
SVar:ExileEff:Mode$ Phase | Phase$ Untap | ValidPlayer$ Player.controlsCard.IsRemembered | Execute$ Exile | Static$ True
SVar:Exile:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile | ConditionCheckSVar$ Count$ActivatedThisGame
Oracle:Tap target creature. Prevent all combat damage that would be dealt by that creature this turn. It doesn't untap during its controller's next two untap steps. Oracle:Tap target creature. Prevent all combat damage that would be dealt by that creature this turn. It doesn't untap during its controller's next two untap steps.

View File

@@ -6,7 +6,7 @@ SVar:GainControl:DB$ GainControl | ValidTgts$ Creature.nonLegendary+OppCtrl | Lo
SVar:OneEach:PlayerCountOpponents$Amount SVar:OneEach:PlayerCountOpponents$Amount
SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X | SpellDescription$ Draw a card for each creature you control but don't own. SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X | SpellDescription$ Draw a card for each creature you control but don't own.
SVar:X:Count$Valid Creature.YouDontOwn+YouCtrl SVar:X:Count$Valid Creature.YouDontOwn+YouCtrl
SVar:DBChoose:DB$ ChooseCard | Defined$ Player | StartingWith$ You | Choices$ Creature | ChoiceTitle$ Choose a creature | Mandatory$ True | SpellDescription$ Starting with you, each player chooses a creature. Destroy each creature chosen this way. SVar:DBChoose:DB$ ChooseCard | Defined$ Player | StartingWith$ You | Choices$ Creature | ChoiceTitle$ Choose a creature | Mandatory$ True | SubAbility$ DBDestroy | SpellDescription$ Starting with you, each player chooses a creature. Destroy each creature chosen this way.
SVar:DBDestroy:DB$ Destroy | Defined$ ChosenCard | SubAbility$ DBCleanup SVar:DBDestroy:DB$ Destroy | Defined$ ChosenCard | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True
Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)\nI — For each opponent, gain control of up to one target nonlegendary creature that player controls for as long as The Horus Heresy remains on the battlefield.\nII — Draw a card for each creature you control but don't own.\nIII — Starting with you, each player chooses a creature. Destroy each creature chosen this way. Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)\nI — For each opponent, gain control of up to one target nonlegendary creature that player controls for as long as The Horus Heresy remains on the battlefield.\nII — Draw a card for each creature you control but don't own.\nIII — Starting with you, each player chooses a creature. Destroy each creature chosen this way.