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;
}
SpellAbility exSA = re.getOverridingAbility();

View File

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

View File

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

View File

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

View File

@@ -65,8 +65,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
ctype = CounterType.getType(sa.getParam("CounterType"));
}
final Player pl = !sa.hasParam("DefinedPlayer") ? sa.getActivatingPlayer() :
AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
final Player pl = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst();
final boolean eachExisting = sa.hasParam("EachExistingCounter");
GameEntityCounterTable table = new GameEntityCounterTable();
@@ -79,7 +78,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) {
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(),
CardTranslation.getTranslatedName(gameCard.getName())), null)) {
continue;
@@ -114,8 +113,6 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove");
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;
if (sa.hasParam("RemoveConditionSVar")) {
final Card host = sa.getHostCard();
@@ -137,6 +134,8 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
} else if (!canReceive && canRemove) {
putCounter = false;
} else {
params.put("CounterType", chosenType);
prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " ";
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.Map.Entry;
import java.util.concurrent.FutureTask;
import static java.lang.Math.max;
@@ -4912,22 +4913,23 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return true;
}
public final boolean canUntap(Player phase, boolean predict) {
if (!tapped) { return false; }
public final boolean canUntap(Player phase, Boolean predict) {
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)) {
return false;
}
if (phase != null &&
(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 two untap steps."))) {
|| hasKeyword("This card doesn't untap during your next untap step."))) {
return false;
}
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);
}

View File

@@ -49,6 +49,8 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.time.StopWatch;
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);
}
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 Combat getCombat() { return combat; }
@@ -185,9 +204,7 @@ public class PhaseHandler implements java.io.Serializable {
playerTurn.setNumPowerSurgeLands(lands);
}
// Replacement effects
final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(playerTurn);
repRunParams.put(AbilityKey.Phase, phase);
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.BeginPhase, repRunParams);
if (repres != ReplacementResult.NotReplaced) {
// 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.Entry;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.game.Game;
@@ -164,10 +163,6 @@ public class Untap extends Phase {
// TODO Replace with Static Abilities
for (final Card c : active.getCardsIn(ZoneType.Battlefield)) {
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

View File

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

View File

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

View File

@@ -3,6 +3,9 @@ ManaCost:U U
Types:Instant
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: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: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.

View File

@@ -6,7 +6,7 @@ SVar:GainControl:DB$ GainControl | ValidTgts$ Creature.nonLegendary+OppCtrl | Lo
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: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: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.