other: reworked target selection (now it use same logic and methods in all places), fixed AI and selection freezes in some use cases (related to #13606, #11285)

This commit is contained in:
Oleg Agafonov 2025-05-07 17:34:36 +04:00
parent 62aa310a4f
commit a53eb66b58
10 changed files with 133 additions and 53 deletions

View file

@ -7,6 +7,7 @@ import mage.constants.Zone;
import mage.filter.Filter;
import mage.game.Game;
import mage.players.Player;
import mage.util.Copyable;
import java.io.Serializable;
import java.util.Collection;
@ -17,14 +18,23 @@ import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
public interface Target extends Serializable {
public interface Target extends Copyable<Target>, Serializable {
/**
* All targets selected by a player
* <p>
* Warning, for "up to" targets it will return true all the time, so make sure your dialog
* use do-while logic and call "choose" one time min or use doneChoosing
*/
boolean isChosen(Game game);
// TODO: combine code or research usages (doneChoosing must be in while cycles, isChosen in other places, see #13606)
boolean doneChoosing(Game game);
void clearChosen();
boolean isChoiceSelected();
boolean isNotTarget();
/**
@ -161,6 +171,7 @@ public interface Target extends Serializable {
UUID getFirstTarget();
@Override
Target copy();
// some targets are chosen from players that are not the controller of the ability (e.g. Pandemonium)

View file

@ -109,17 +109,20 @@ public abstract class TargetAmount extends TargetImpl {
if (!amountWasSet) {
setAmount(source, game);
}
chosen = isChosen(game);
chosen = false;
while (remainingAmount > 0) {
if (!player.canRespond()) {
chosen = isChosen(game);
return chosen;
}
if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) {
chosen = isChosen(game);
return chosen;
}
chosen = isChosen(game);
}
return chosen;
return isChosen(game);
}
@Override

View file

@ -31,7 +31,16 @@ public abstract class TargetImpl implements Target {
protected int minNumberOfTargets;
protected boolean required = true;
protected boolean requiredExplicitlySet = false;
/**
* Simple chosen state due selected and require targets count
* Complex targets must override isChosen method or set chosen value manually
* For "up to" targets it will be false before first choose dialog:
* - chosen = false - player do not make a choice yet
* - chosen = true, targets.size = 0 - player choose 0 targets in "up to" and it's valid
* - chosen = true, targets.size >= 1 - player choose some targets and it's valid
*/
protected boolean chosen = false;
// is the target handled as targeted spell/ability (notTarget = true is used for not targeted effects like e.g. sacrifice)
protected boolean notTarget = false;
protected boolean atRandom = false; // for inner choose logic
@ -229,15 +238,28 @@ public abstract class TargetImpl implements Target {
@Override
public boolean isChosen(Game game) {
// min = max = 0 - for abilities with X=0, e.g. nothing to choose
if (getMaxNumberOfTargets() == 0 && getMinNumberOfTargets() == 0) {
return true;
}
return getMaxNumberOfTargets() != 0 && targets.size() == getMaxNumberOfTargets() || chosen;
// limit by max amount
if (getMaxNumberOfTargets() > 0 && targets.size() > getMaxNumberOfTargets()) {
return chosen;
}
// limit by min amount
if (getMinNumberOfTargets() > 0 && targets.size() < getMinNumberOfTargets()) {
return chosen;
}
// all fine
return chosen || (targets.size() >= getMinNumberOfTargets() && targets.size() <= getMaxNumberOfTargets());
}
@Override
public boolean doneChoosing(Game game) {
return getMaxNumberOfTargets() != 0 && targets.size() == getMaxNumberOfTargets();
return isChoiceSelected() && isChosen(game);
}
@Override
@ -247,13 +269,19 @@ public abstract class TargetImpl implements Target {
chosen = false;
}
@Override
public boolean isChoiceSelected() {
// min = max = 0 - for abilities with X=0, e.g. nothing to choose
return chosen || getMaxNumberOfTargets() == 0 && getMinNumberOfTargets() == 0;
}
@Override
public void add(UUID id, Game game) {
if (getMaxNumberOfTargets() == 0 || targets.size() < getMaxNumberOfTargets()) {
if (!targets.containsKey(id)) {
targets.put(id, 0);
rememberZoneChangeCounter(id, game);
chosen = targets.size() >= getMinNumberOfTargets();
chosen = isChosen(game);
}
}
}
@ -280,7 +308,7 @@ public abstract class TargetImpl implements Target {
if (!game.replaceEvent(new TargetEvent(id, source))) {
targets.put(id, 0);
rememberZoneChangeCounter(id, game);
chosen = targets.size() >= getMinNumberOfTargets();
chosen = isChosen(game);
if (!skipEvent && shouldReportEvents) {
game.addSimultaneousEvent(GameEvent.getEvent(GameEvent.EventType.TARGETED, id, source, source.getControllerId()));
}
@ -318,14 +346,16 @@ public abstract class TargetImpl implements Target {
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.TARGET, id, source, source.getControllerId()))) {
targets.put(id, amount);
rememberZoneChangeCounter(id, game);
chosen = targets.size() >= getMinNumberOfTargets();
chosen = isChosen(game);
if (!skipEvent && shouldReportEvents) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TARGETED, id, source, source.getControllerId()));
}
}
} else {
// AI targets simulation
targets.put(id, amount);
rememberZoneChangeCounter(id, game);
chosen = isChosen(game);
}
}
@ -336,17 +366,20 @@ public abstract class TargetImpl implements Target {
return false;
}
chosen = targets.size() >= getMinNumberOfTargets();
chosen = false;
do {
if (!targetController.canRespond()) {
chosen = isChosen(game);
return chosen;
}
if (!targetController.choose(outcome, this, source, game)) {
chosen = isChosen(game);
return chosen;
}
chosen = targets.size() >= getMinNumberOfTargets();
} while (!isChosen(game) && !doneChoosing(game));
return chosen;
chosen = isChosen(game);
} while (!doneChoosing(game));
return isChosen(game);
}
@Override
@ -358,13 +391,15 @@ public abstract class TargetImpl implements Target {
List<UUID> possibleTargets = new ArrayList<>(possibleTargets(playerId, source, game));
chosen = targets.size() >= getMinNumberOfTargets();
chosen = false;
do {
if (!targetController.canRespond()) {
chosen = isChosen(game);
return chosen;
}
if (isRandom()) {
if (possibleTargets.isEmpty()) {
chosen = isChosen(game);
return chosen;
}
// find valid target
@ -384,13 +419,14 @@ public abstract class TargetImpl implements Target {
if (autoChosenId != null) {
addTarget(autoChosenId, source, game);
} else if (!targetController.chooseTarget(outcome, this, source, game)) { // If couldn't autochoose ask player
chosen = isChosen(game);
return chosen;
}
}
chosen = targets.size() >= getMinNumberOfTargets();
} while (!isChosen(game) && !doneChoosing(game));
chosen = isChosen(game);
} while (!doneChoosing(game));
return chosen;
return isChosen(game);
}
@Override

View file

@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.util.Copyable;
import java.util.*;
@ -38,7 +39,11 @@ public class Targets extends ArrayList<Target> implements Copyable<Targets> {
}
public List<Target> getUnchosen(Game game) {
return stream().filter(target -> !target.isChosen(game)).collect(Collectors.toList());
return stream().filter(target -> !target.isChoiceSelected()).collect(Collectors.toList());
}
public boolean doneChoosing(Game game) {
return stream().allMatch(t -> t.doneChoosing(game));
}
public void clearChosen() {
@ -67,7 +72,7 @@ public class Targets extends ArrayList<Target> implements Copyable<Targets> {
if (!target.choose(outcome, playerId, sourceId, source, game)) {
return false;
}
}
} while (!doneChoosing(game));
}
return true;
}
@ -112,7 +117,7 @@ public class Targets extends ArrayList<Target> implements Copyable<Targets> {
//game.restoreState(state, "Targets");
clearChosen();
}
}
} while (!doneChoosing(game));
}
return true;
}

View file

@ -76,17 +76,20 @@ public class TargetCardInLibrary extends TargetCard {
Cards cardsId = new CardsImpl();
cards.forEach(cardsId::add);
chosen = targets.size() >= getMinNumberOfTargets();
chosen = false;
do {
if (!player.canRespond()) {
chosen = isChosen(game);
return chosen;
}
if (!player.chooseTarget(outcome, cardsId, this, source, game)) {
chosen = isChosen(game);
return chosen;
}
chosen = targets.size() >= getMinNumberOfTargets();
} while (!isChosen(game) && !doneChoosing(game));
return chosen;
chosen = isChosen(game);
} while (!doneChoosing(game));
return isChosen(game);
}
@Override