refactor: improved search in stack

This commit is contained in:
Oleg Agafonov 2025-08-10 02:07:15 +04:00
parent 26adccdfd5
commit 384ce67cc3
20 changed files with 101 additions and 119 deletions

View file

@ -398,7 +398,10 @@ public class ComputerPlayer6 extends ComputerPlayer {
}
protected void resolve(SimulationNode2 node, int depth, Game game) {
StackObject stackObject = game.getStack().getFirst();
StackObject stackObject = game.getStack().getFirstOrNull();
if (stackObject == null) {
throw new IllegalStateException("Catch empty stack on resolve (something wrong with sim code)");
}
if (stackObject instanceof StackAbility) {
// AI hint for search effects (calc all possible cards for best score)
SearchEffect effect = getSearchEffect((StackAbility) stackObject);

View file

@ -638,8 +638,8 @@ public class HumanPlayer extends PlayerImpl {
// Check check if the spell being paid for cares about the color of mana being paid
// See: https://github.com/magefree/mage/issues/9070
boolean caresAboutManaColor = false;
if (!game.getStack().isEmpty() && game.getStack().getFirst() instanceof Spell) {
Spell spellBeingCast = (Spell) game.getStack().getFirst();
if (game.getStack().getFirstOrNull() instanceof Spell) {
Spell spellBeingCast = (Spell) game.getStack().getFirstOrNull();
if (!spellBeingCast.isResolving() && spellBeingCast.getControllerId().equals(this.getId())) {
CardImpl card = (CardImpl) game.getCard(spellBeingCast.getSourceId());
caresAboutManaColor = card.caresAboutManaColor(game);

View file

@ -68,10 +68,7 @@ class AstralDriftTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.getState().getStack().isEmpty()) {
return false;
}
StackObject item = game.getState().getStack().getFirst();
StackObject item = game.getState().getStack().getFirstOrNull();
if (!(item instanceof StackAbility
&& item.getStackAbility() instanceof CyclingAbility)) {
return false;

View file

@ -78,11 +78,9 @@ class CobraTrapWatcher extends Watcher {
if (event.getType() == GameEvent.EventType.DESTROYED_PERMANENT) {
Permanent perm = game.getPermanentOrLKIBattlefield(event.getTargetId()); // can regenerate or be indestructible
if (perm != null && !perm.isCreature(game)) {
if (!game.getStack().isEmpty()) {
StackObject spell = game.getStack().getStackObject(event.getSourceId());
if (spell != null && game.getOpponents(perm.getControllerId()).contains(spell.getControllerId())) {
players.add(perm.getControllerId());
}
StackObject spell = game.getStack().getStackObject(event.getSourceId());
if (spell != null && game.getOpponents(perm.getControllerId()).contains(spell.getControllerId())) {
players.add(perm.getControllerId());
}
}
}

View file

@ -1,7 +1,5 @@
package mage.cards.r;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ReplacementEffectImpl;
@ -10,20 +8,20 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.stack.StackObject;
import mage.players.Player;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public final class RainOfGore extends CardImpl {
public RainOfGore(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{B}{R}");
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}{R}");
// If a spell or ability would cause its controller to gain life, that player loses that much life instead.
@ -74,10 +72,7 @@ class RainOfGoreEffect extends ReplacementEffectImpl {
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
if (!game.getStack().isEmpty()) {
StackObject stackObject = game.getStack().getFirst();
return stackObject.isControlledBy(event.getPlayerId());
}
return false;
StackObject stackObject = game.getStack().getFirstOrNull();
return stackObject != null && stackObject.isControlledBy(event.getPlayerId());
}
}

View file

@ -120,13 +120,8 @@ enum SoulSculptorCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
if (!game.getStack().isEmpty()) {
StackObject stackObject = game.getStack().getFirst();
if (stackObject != null) {
return !stackObject.getCardType(game).contains(CardType.CREATURE);
}
}
return true;
StackObject stackObject = game.getStack().getFirstOrNull();
return stackObject != null && !stackObject.getCardType(game).contains(CardType.CREATURE);
}
@Override

View file

@ -21,6 +21,7 @@ import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player;
import java.util.UUID;
@ -94,18 +95,22 @@ class SoulfireGrandMasterCastFromHandReplacementEffect extends ReplacementEffect
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Spell spell = (Spell) game.getStack().getFirst();
if (!spell.isCopy() && !spell.isCountered()) {
Card sourceCard = game.getCard(spellId);
if (sourceCard != null && Zone.STACK.equals(game.getState().getZone(spellId))) {
Player player = game.getPlayer(sourceCard.getOwnerId());
if (player != null) {
player.moveCards(sourceCard, Zone.HAND, source, game);
discard();
return true;
StackObject stackObject = game.getStack().getFirstOrNull();
if (stackObject instanceof Spell) {
Spell spell = (Spell) stackObject;
if (!spell.isCopy() && !spell.isCountered()) {
Card sourceCard = game.getCard(spellId);
if (sourceCard != null && Zone.STACK.equals(game.getState().getZone(spellId))) {
Player player = game.getPlayer(sourceCard.getOwnerId());
if (player != null) {
player.moveCards(sourceCard, Zone.HAND, source, game);
discard();
return true;
}
}
}
}
return false;
}
@ -134,8 +139,8 @@ class SoulfireGrandMasterCastFromHandReplacementEffect extends ReplacementEffect
if (zEvent.getFromZone() == Zone.STACK
&& zEvent.getToZone() == Zone.GRAVEYARD
&& event.getTargetId().equals(spellId)) {
if (game.getStack().getFirst() instanceof Spell) {
Card cardOfSpell = ((Spell) game.getStack().getFirst()).getCard();
if (game.getStack().getFirstOrNull() instanceof Spell) {
Card cardOfSpell = ((Spell) game.getStack().getFirstOrNull()).getCard();
return cardOfSpell.getMainCard().getId().equals(spellId);
}
}

View file

@ -99,12 +99,10 @@ enum FirstSpellCastFromNotHandEachTurnCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
if (game.getStack().isEmpty()) {
return false;
}
TheTwelfthDoctorWatcher watcher = game.getState().getWatcher(TheTwelfthDoctorWatcher.class);
StackObject so = game.getStack().getFirst();
return watcher != null
StackObject so = game.getStack().getFirstOrNull();
return so != null
&& watcher != null
&& TheTwelfthDoctorWatcher.checkSpell(so, game);
}
}

View file

@ -74,12 +74,11 @@ class ValiantRescuerTriggeredAbility extends TriggeredAbilityImpl {
ValiantRescuerWatcher watcher = game.getState().getWatcher(ValiantRescuerWatcher.class);
if (watcher == null
|| !watcher.checkSpell(event.getPlayerId(), event.getSourceId())
|| game.getState().getStack().isEmpty()
|| !event.getPlayerId().equals(this.getControllerId())
|| event.getSourceId().equals(this.getSourceId())) {
return false;
}
StackObject item = game.getState().getStack().getFirst();
StackObject item = game.getState().getStack().getFirstOrNull();
return item instanceof StackAbility
&& item.getStackAbility() instanceof CyclingAbility;
}
@ -106,11 +105,10 @@ class ValiantRescuerWatcher extends Watcher {
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() != GameEvent.EventType.ACTIVATED_ABILITY
|| game.getState().getStack().isEmpty()) {
if (event.getType() != GameEvent.EventType.ACTIVATED_ABILITY) {
return;
}
StackObject item = game.getState().getStack().getFirst();
StackObject item = game.getState().getStack().getFirstOrNull();
if (item instanceof StackAbility
&& item.getStackAbility() instanceof CyclingAbility) {
playerMap.computeIfAbsent(event.getPlayerId(), u -> new HashMap<>());

View file

@ -2,7 +2,10 @@ package mage.cards.w;
import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.keyword.CascadeAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
@ -11,14 +14,12 @@ import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.players.Player;
/**
* @author TheElk801
@ -35,7 +36,7 @@ public final class WildMagicSorcerer extends CardImpl {
// The first spell you cast from exile each turn has cascade.
this.addAbility(new SimpleStaticAbility(
new WildMagicSorcererGainCascadeFirstSpellCastFromExileEffect()),
new WildMagicSorcererGainCascadeFirstSpellCastFromExileEffect()),
new WildMagicSorcererWatcher());
}
@ -96,12 +97,10 @@ enum FirstSpellCastFromExileEachTurnCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
if (game.getStack().isEmpty()) {
return false;
}
WildMagicSorcererWatcher watcher = game.getState().getWatcher(WildMagicSorcererWatcher.class);
StackObject so = game.getStack().getFirst();
return watcher != null
StackObject so = game.getStack().getFirstOrNull();
return so != null
&& watcher != null
&& WildMagicSorcererWatcher.checkSpell(so, game);
}
}

View file

@ -55,7 +55,7 @@ public class DisguiseTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dog Walker using Disguise");
runCode("face up on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Assert.assertEquals("stack, server - can't find spell", 1, currentGame.getStack().size());
SpellAbility spellAbility = (SpellAbility) currentGame.getStack().getFirst().getStackAbility();
SpellAbility spellAbility = (SpellAbility) currentGame.getStack().getFirstOrNull().getStackAbility();
Assert.assertEquals("stack, server - can't find spell", "Cast Dog Walker using Disguise", spellAbility.getName());
CardView spellView = getGameView(playerA).getStack().values().stream().findFirst().orElse(null);
Assert.assertNotNull("stack, client: can't find spell", spellView);

View file

@ -40,7 +40,7 @@ public class DisturbTest extends CardTestPlayerBase {
checkStackObject("on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", 1);
runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
// Stack must contain another card side, so spell/card characteristics must be diff from main side (only mana value is same)
Spell spell = (Spell) game.getStack().getFirst();
Spell spell = (Spell) game.getStack().getFirstOrNull();
Assert.assertEquals("Hook-Haunt Drifter", spell.getName());
Assert.assertEquals(1, spell.getCardType(game).size());
Assert.assertEquals(CardType.CREATURE, spell.getCardType(game).get(0));
@ -91,7 +91,7 @@ public class DisturbTest extends CardTestPlayerBase {
checkStackObject("on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Waildrifter using Disturb", 1);
runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
// Stack must contain another card side, so spell/card characteristics must be diff from main side (only mana value is same)
Spell spell = (Spell) game.getStack().getFirst();
Spell spell = (Spell) game.getStack().getFirstOrNull();
Assert.assertEquals("Waildrifter", spell.getName());
Assert.assertEquals(1, spell.getCardType(game).size());
Assert.assertEquals(CardType.CREATURE, spell.getCardType(game).get(0));
@ -187,7 +187,7 @@ public class DisturbTest extends CardTestPlayerBase {
// cast with disturb
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb");
runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Spell spell = (Spell) game.getStack().getFirst();
Spell spell = (Spell) game.getStack().getFirstOrNull();
Assert.assertEquals("mana value must be from main side", 2, spell.getManaValue());
Assert.assertEquals("mana cost to pay must be modified", "{U}", spell.getSpellAbility().getManaCostsToPay().getText());
});

View file

@ -334,11 +334,8 @@ public class TestPlayer implements Player {
return true;
} else if (groups[2].startsWith("spellOnTopOfStack=")) {
String spellOnTopOFStack = groups[2].substring(18);
if (!game.getStack().isEmpty()) {
StackObject stackObject = game.getStack().getFirst();
return stackObject != null && stackObject.getStackAbility().toString().contains(spellOnTopOFStack);
}
return false;
StackObject stackObject = game.getStack().getFirstOrNull();
return stackObject != null && stackObject.getStackAbility().toString().contains(spellOnTopOFStack);
} else if (groups[2].startsWith("manaInPool=")) {
String manaInPool = groups[2].substring(11);
int amountOfMana = Integer.parseInt(manaInPool);

View file

@ -53,11 +53,9 @@ public abstract class SpecialAction extends ActivatedAbilityImpl {
if (isManaAction()) {
// limit play mana abilities by steps
int currentStepOrder = 0;
if (!game.getStack().isEmpty()) {
StackObject stackObject = game.getStack().getFirst();
if (stackObject instanceof Spell) {
currentStepOrder = ((Spell) stackObject).getCurrentActivatingManaAbilitiesStep().getStepOrder();
}
StackObject stackObject = game.getStack().getFirstOrNull();
if (stackObject instanceof Spell) {
currentStepOrder = ((Spell) stackObject).getCurrentActivatingManaAbilitiesStep().getStepOrder();
}
if (currentStepOrder > manaAbility.useOnActivationManaAbilityStep().getStepOrder()) {
return ActivationStatus.getFalse();

View file

@ -31,10 +31,7 @@ public class CycleAllTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.getState().getStack().isEmpty()) {
return false;
}
StackObject item = game.getState().getStack().getFirst();
StackObject item = game.getState().getStack().getFirstOrNull();
return item instanceof StackAbility
&& item.getStackAbility() instanceof CyclingAbility;
}

View file

@ -42,12 +42,11 @@ public class CycleControllerTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (game.getState().getStack().isEmpty()
|| !event.getPlayerId().equals(this.getControllerId())
if (!event.getPlayerId().equals(this.getControllerId())
|| (event.getSourceId().equals(this.getSourceId()) && excludeSource)) {
return false;
}
StackObject item = game.getState().getStack().getFirst();
StackObject item = game.getState().getStack().getFirstOrNull();
return item instanceof StackAbility
&& item.getStackAbility() instanceof CyclingAbility;
}

View file

@ -18,25 +18,24 @@ public enum SunburstCount implements DynamicValue {
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
int count = 0;
if (!game.getStack().isEmpty()) {
StackObject spell = game.getStack().getFirst();
if (spell instanceof Spell && ((Spell) spell).getSourceId().equals(sourceAbility.getSourceId())) {
Mana mana = ((Spell) spell).getSpellAbility().getManaCostsToPay().getUsedManaToPay();
if (mana.getBlack() > 0) {
count++;
}
if (mana.getBlue() > 0) {
count++;
}
if (mana.getGreen() > 0) {
count++;
}
if (mana.getRed() > 0) {
count++;
}
if (mana.getWhite() > 0) {
count++;
}
StackObject spell = game.getStack().getFirstOrNull();
if (spell instanceof Spell && spell.getSourceId().equals(sourceAbility.getSourceId())) {
Mana mana = ((Spell) spell).getSpellAbility().getManaCostsToPay().getUsedManaToPay();
if (mana.getBlack() > 0) {
count++;
}
if (mana.getBlue() > 0) {
count++;
}
if (mana.getGreen() > 0) {
count++;
}
if (mana.getRed() > 0) {
count++;
}
if (mana.getWhite() > 0) {
count++;
}
}
return count;

View file

@ -513,13 +513,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
// If the top card of your library changes while youre casting a spell, playing a land, or activating an ability,
// you cant look at the new top card until you finish doing so. This means that if you cast the top card of
// your library, you cant look at the next one until youre done paying for that spell. (2019-05-03)
if (!game.getStack().isEmpty()) {
StackObject stackObject = game.getStack().getFirst();
return !(stackObject instanceof Spell)
|| !Zone.LIBRARY.equals(((Spell) stackObject).getFromZone())
|| stackObject.getStackAbility().getManaCostsToPay().isPaid(); // mana payment finished
}
return true;
StackObject stackObject = game.getStack().getFirstOrNull();
return !(stackObject instanceof Spell)
|| !Zone.LIBRARY.equals(((Spell) stackObject).getFromZone())
|| stackObject.getStackAbility().getManaCostsToPay().isPaid(); // mana payment finished
}
@Override

View file

@ -44,19 +44,16 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl
public ActivationStatus canActivate(UUID playerId, Game game) {
// check if player is in the process of playing spell costs and they are no longer allowed to use
// activated mana abilities (e.g. because they started to use improvise or convoke)
if (!game.getStack().isEmpty()) {
StackObject stackObject = game.getStack().getFirst();
if (stackObject instanceof Spell) {
switch (((Spell) stackObject).getCurrentActivatingManaAbilitiesStep()) {
case BEFORE:
case NORMAL:
break;
case AFTER:
return ActivationStatus.getFalse();
}
StackObject stackObject = game.getStack().getFirstOrNull();
if (stackObject instanceof Spell) {
switch (((Spell) stackObject).getCurrentActivatingManaAbilitiesStep()) {
case BEFORE:
case NORMAL:
break;
case AFTER:
return ActivationStatus.getFalse();
}
}
return super.canActivate(playerId, game);
}

View file

@ -51,6 +51,16 @@ public class SpellStack extends ArrayDeque<StackObject> {
}
}
@Override
@Deprecated // must use getFirstOrNull instead
public StackObject getFirst() {
return super.getFirst();
}
public StackObject getFirstOrNull() {
return this.isEmpty() ? null : this.getFirst();
}
public boolean remove(StackObject object, Game game) {
for (StackObject spell : this) {
if (spell.getId().equals(object.getId())) {