other: reworked target selection: (#13638)

- WIP: AI and multi targets, human and X=0 use cases, human and impossible targets use cases;
- improved stability and shared logic (related to #13606, #11134, #11666, continue from a53eb66b58, close #13617, close #13613);
- improved test logs and debug info to show more target info on errors;
- improved test framework to support multiple addTarget calls;
- improved test framework to find bad commands order for targets (related to #11666);
- fixed game freezes on auto-choice usages with disconnected or under control players (related to #11285);
- gui, game: fixed that player doesn't mark avatar as selected/green in "up to" targeting;
- gui, game: fixed small font in some popup messages on big screens (related to #969);
- gui, game: added min targets info for target selection dialog;
- for devs: added new cheat option to call and test any game dialog (define own dialogs, targets, etc in HumanDialogsTester);
- for devs: now tests require complete an any or up to target selection by addTarget + TestPlayer.TARGET_SKIP or setChoice + TestPlayer.CHOICE_SKIP (if not all max/possible targets used);
- for devs: added detail targets info for activate/trigger/cast, can be useful to debug unit tests, auto-choose or AI (see DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS)
This commit is contained in:
Oleg Agafonov 2025-05-16 13:55:54 +04:00 committed by GitHub
parent 80d62727e1
commit 133e4fe425
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 2737 additions and 743 deletions

View file

@ -89,7 +89,7 @@ public class CollectEvidenceCost extends CostImpl {
);
}
}.withNotTarget(true);
player.choose(Outcome.Exile, target, source, game);
target.choose(Outcome.Exile, player.getId(), source.getSourceId(), source, game);
Cards cards = new CardsImpl(target.getTargets());
paid = cards
.getCards(game)

View file

@ -91,9 +91,7 @@ public class SacrificeAllEffect extends OneShotEffect {
continue;
}
TargetSacrifice target = new TargetSacrifice(numTargets, filter);
while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) {
player.choose(Outcome.Sacrifice, target, source, game);
}
target.choose(Outcome.Sacrifice, player.getId(), source, game);
perms.addAll(target.getTargets());
}

View file

@ -64,13 +64,12 @@ public class SacrificeEffect extends OneShotEffect {
continue;
}
TargetSacrifice target = new TargetSacrifice(amount, filter);
while (!target.isChosen(game) && target.canChoose(player.getId(), source, game) && player.canRespond()) {
player.choose(Outcome.Sacrifice, target, source, game);
}
for (UUID targetId : target.getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null && permanent.sacrifice(source, game)) {
applied = true;
if (target.choose(Outcome.Sacrifice, player.getId(), source.getSourceId(), source, game)) {
for (UUID targetId : target.getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null && permanent.sacrifice(source, game)) {
applied = true;
}
}
}
}

View file

@ -1522,7 +1522,7 @@ public abstract class GameImpl implements Game {
UUID[] players = getPlayers().keySet().toArray(new UUID[0]);
UUID playerId;
while (!hasEnded()) {
playerId = players[RandomUtil.nextInt(players.length)];
playerId = players[RandomUtil.nextInt(players.length)]; // test game
Player player = getPlayer(playerId);
if (player != null && player.canRespond()) {
fireInformEvent(state.getPlayer(playerId).getLogName() + " won the toss");
@ -1810,14 +1810,21 @@ public abstract class GameImpl implements Game {
protected void resolve() {
StackObject top = null;
boolean wasError = false;
try {
top = state.getStack().peek();
top.resolve(this);
resetControlAfterSpellResolve(top.getId());
} catch (Throwable e) {
// workaround to show real error in tests instead checkInfiniteLoop
wasError = true;
throw e;
} finally {
if (top != null) {
state.getStack().remove(top, this); // seems partly redundant because move card from stack to grave is already done and the stack removed
checkInfiniteLoop(top.getSourceId());
if (!wasError) {
checkInfiniteLoop(top.getSourceId());
}
if (!getTurn().isEndTurnRequested()) {
while (state.hasSimultaneousEvents()) {
state.handleSimultaneousEvent(this);
@ -3746,7 +3753,7 @@ public abstract class GameImpl implements Game {
@Override
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PutToBattlefieldInfo> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled) {
// fake test ability for triggers and events
Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards"));
Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("fake ability"));
fakeSourceAbilityTemplate.setControllerId(ownerId);
Player player = getPlayer(ownerId);

View file

@ -458,10 +458,14 @@ public final class ZonesHandler {
target.setRequired(true);
while (player.canRespond() && cards.size() > 1) {
player.choose(Outcome.Neutral, cards, target, source, game);
UUID targetObjectId = target.getFirstTarget();
order.add(cards.get(targetObjectId, game));
cards.remove(targetObjectId);
target.clearChosen();
Card card = cards.get(target.getFirstTarget(), game);
if (card != null) {
order.add(card);
cards.remove(target.getFirstTarget());
target.clearChosen();
} else {
break;
}
}
order.addAll(cards.getCards(game));
return order;

View file

@ -168,6 +168,8 @@ public class Library implements Serializable {
}
public Collection<Card> getUniqueCards(Game game) {
// TODO: on no performance issues - remove unique code after few releases, 2025-05-13
if (true) return getCards(game);
Map<String, Card> cards = new HashMap<>();
for (UUID cardId : library) {
Card card = game.getCard(cardId);

View file

@ -672,6 +672,10 @@ public interface Player extends MageItem, Copyable<Player> {
boolean priority(Game game);
/**
* Warning, any choose and chooseTarget dialogs must return false to stop choosing, e.g. no more possible targets
* Same logic as "something changes" in "apply"
*/
boolean choose(Outcome outcome, Target target, Ability source, Game game);
boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options);

View file

@ -886,7 +886,7 @@ public abstract class PlayerImpl implements Player, Serializable {
return toDiscard;
}
TargetDiscard target = new TargetDiscard(minAmount, maxAmount, StaticFilters.FILTER_CARD, getId());
choose(Outcome.Discard, target, source, game);
target.choose(Outcome.Discard, getId(), source, game);
toDiscard.addAll(target.getTargets());
return toDiscard;
}
@ -4472,14 +4472,14 @@ public abstract class PlayerImpl implements Player, Serializable {
List<Ability> options = new ArrayList<>();
if (ability.isModal()) {
addModeOptions(options, ability, game);
} else if (!ability.getTargets().getUnchosen(game).isEmpty()) {
} else if (ability.getTargets().getNextUnchosen(game) != null) {
// TODO: Handle other variable costs than mana costs
if (!ability.getManaCosts().getVariableCosts().isEmpty()) {
addVariableXOptions(options, ability, 0, game);
} else {
addTargetOptions(options, ability, 0, game);
}
} else if (!ability.getCosts().getTargets().getUnchosen(game).isEmpty()) {
} else if (ability.getCosts().getTargets().getNextUnchosen(game) != null) {
addCostTargetOptions(options, ability, 0, game);
}
@ -4497,13 +4497,13 @@ public abstract class PlayerImpl implements Player, Serializable {
newOption.getModes().clearSelectedModes();
newOption.getModes().addSelectedMode(mode.getId());
newOption.getModes().setActiveMode(mode);
if (!newOption.getTargets().getUnchosen(game).isEmpty()) {
if (newOption.getTargets().getNextUnchosen(game) != null) {
if (!newOption.getManaCosts().getVariableCosts().isEmpty()) {
addVariableXOptions(options, newOption, 0, game);
} else {
addTargetOptions(options, newOption, 0, game);
}
} else if (!newOption.getCosts().getTargets().getUnchosen(game).isEmpty()) {
} else if (newOption.getCosts().getTargets().getNextUnchosen(game) != null) {
addCostTargetOptions(options, newOption, 0, game);
} else {
options.add(newOption);
@ -4523,7 +4523,9 @@ public abstract class PlayerImpl implements Player, Serializable {
*/
protected void addTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
// TODO: target options calculated for triggered ability too, but do not used in real game
for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) {
// TODO: there are rare errors with wrong targetNum - maybe multiple game sims can change same target object somehow?
// do not hide NullPointError here, research instead
for (Target target : option.getTargets().getNextUnchosen(game, targetNum).getTargetOptions(option, game)) {
Ability newOption = option.copy();
if (target instanceof TargetAmount) {
for (UUID targetId : target.getTargets()) {
@ -5562,8 +5564,7 @@ public abstract class PlayerImpl implements Player, Serializable {
TargetPermanent target = new TargetControlledCreaturePermanent();
target.withNotTarget(true);
target.withChooseHint("to be your Ring-bearer");
choose(Outcome.Neutral, target, null, game);
target.choose(Outcome.Neutral, getId(), null, null, game);
newBearerId = target.getFirstTarget();
} else {
newBearerId = currentBearerId;

View file

@ -42,7 +42,9 @@ public class StubPlayer extends PlayerImpl {
public boolean choose(Outcome outcome, Target target, Ability source, Game game) {
if (target instanceof TargetPlayer) {
for (Player player : game.getPlayers().values()) {
if (player.getId().equals(getId()) && target.canTarget(getId(), game)) {
if (player.getId().equals(getId())
&& target.canTarget(getId(), game)
&& !target.contains(getId())) {
target.add(player.getId(), game);
return true;
}

View file

@ -24,12 +24,13 @@ 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
* use do-while logic and call "choose" one time min or use isChoiceCompleted
*/
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);
boolean isChoiceCompleted(Game game);
boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game);
void clearChosen();
@ -63,6 +64,9 @@ public interface Target extends Copyable<Target>, Serializable {
*/
Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game);
/**
* Priority method to make a choice from cards and other places, not a player.chooseXXX
*/
boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game);
/**
@ -101,10 +105,19 @@ public interface Target extends Copyable<Target>, Serializable {
Set<UUID> possibleTargets(UUID sourceControllerId, Game game);
@Deprecated // TODO: need replace to source only version?
boolean choose(Outcome outcome, UUID playerId, UUID sourceId, Ability source, Game game);
/**
* Priority method to make a choice from cards and other places, not a player.chooseXXX
*/
default boolean choose(Outcome outcome, UUID playerId, Ability source, Game game) {
return choose(outcome, playerId, source == null ? null : source.getSourceId(), source, game);
}
/**
* Add target from non targeting methods like choose
* TODO: need usage research, looks like there are wrong usage of addTarget, e.g. in choose method (must be add)
*/
void add(UUID id, Game game);

View file

@ -46,11 +46,11 @@ public abstract class TargetAmount extends TargetImpl {
@Override
public boolean isChosen(Game game) {
return doneChoosing(game);
return isChoiceCompleted(game);
}
@Override
public boolean doneChoosing(Game game) {
public boolean isChoiceCompleted(Game game) {
return amountWasSet
&& (remainingAmount == 0
|| (getMinNumberOfTargets() < getMaxNumberOfTargets()
@ -109,15 +109,16 @@ public abstract class TargetAmount extends TargetImpl {
if (!amountWasSet) {
setAmount(source, game);
}
chosen = false;
while (remainingAmount > 0) {
chosen = false;
if (!player.canRespond()) {
chosen = isChosen(game);
return chosen;
break;
}
if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) {
chosen = isChosen(game);
return chosen;
break;
}
chosen = isChosen(game);
}

View file

@ -50,6 +50,12 @@ public class TargetCard extends TargetObject {
return this.filter;
}
@Override
public TargetCard withNotTarget(boolean notTarget) {
super.withNotTarget(notTarget);
return this;
}
/**
* Checks if there are enough {@link Card cards} in the appropriate zone that the player can choose from among them
* or if they are autochosen since there are fewer than the minimum number.

View file

@ -173,11 +173,14 @@ public abstract class TargetImpl implements Target {
if (getMaxNumberOfTargets() != 1) {
StringBuilder sb = new StringBuilder();
sb.append("Select ").append(targetName);
sb.append(" (selected ").append(targets.size());
if (getMaxNumberOfTargets() > 0 && getMaxNumberOfTargets() != Integer.MAX_VALUE) {
sb.append(" (selected ").append(targets.size()).append(" of ").append(getMaxNumberOfTargets()).append(')');
} else {
sb.append(" (selected ").append(targets.size()).append(')');
sb.append(" of ").append(getMaxNumberOfTargets());
}
if (getMinNumberOfTargets() > 0) {
sb.append(", min ").append(getMinNumberOfTargets());
}
sb.append(')');
sb.append(suffix);
return sb.toString();
}
@ -245,12 +248,12 @@ public abstract class TargetImpl implements Target {
// limit by max amount
if (getMaxNumberOfTargets() > 0 && targets.size() > getMaxNumberOfTargets()) {
return chosen;
return false;
}
// limit by min amount
if (getMinNumberOfTargets() > 0 && targets.size() < getMinNumberOfTargets()) {
return chosen;
return false;
}
// all fine
@ -258,8 +261,44 @@ public abstract class TargetImpl implements Target {
}
@Override
public boolean doneChoosing(Game game) {
return isChoiceSelected() && isChosen(game);
@Deprecated // TODO: replace usage in cards by full version from choose methods
public boolean isChoiceCompleted(Game game) {
return isChoiceCompleted(null, null, game);
}
@Override
public boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game) {
// make sure target request called one time minimum (for "up to" targets)
// choice is selected after any addTarget call (by test, AI or human players)
if (!isChoiceSelected()) {
return false;
}
// make sure selected targets are valid
if (!isChosen(game)) {
return false;
}
// make sure to auto-finish on all targets selection
// - human player can select and deselect targets until fill all targets amount or press done button
// - AI player can select all new targets as much as possible
if (getMaxNumberOfTargets() > 0) {
if (getMaxNumberOfTargets() == Integer.MAX_VALUE) {
if (abilityControllerId != null && source != null) {
// any amount - nothing to choose
return this.getSize() >= this.possibleTargets(abilityControllerId, source, game).size();
} else {
// any amount - any selected
return this.getSize() > 0;
}
} else {
// check selected limit
return this.getSize() >= getMaxNumberOfTargets();
}
}
// all other use cases are fine
return true;
}
@Override
@ -289,7 +328,7 @@ public abstract class TargetImpl implements Target {
@Override
public void remove(UUID id) {
if (targets.containsKey(id)) {
targets.remove(id);
targets.remove(id); // TODO: miss chosen update here?
zoneChangeCounters.remove(id);
}
}
@ -315,6 +354,8 @@ public abstract class TargetImpl implements Target {
}
} else {
targets.put(id, 0);
rememberZoneChangeCounter(id, game);
chosen = isChosen(game);
}
}
}
@ -366,20 +407,45 @@ public abstract class TargetImpl implements Target {
return false;
}
UUID abilityControllerId = playerId;
if (this.getTargetController() != null && this.getAbilityController() != null) {
abilityControllerId = this.getAbilityController();
}
chosen = false;
do {
if (!targetController.canRespond()) {
chosen = isChosen(game);
return chosen;
}
if (!targetController.choose(outcome, this, source, game)) {
chosen = isChosen(game);
return chosen;
}
chosen = isChosen(game);
} while (!doneChoosing(game));
int prevTargetsCount = this.getTargets().size();
return isChosen(game);
// stop by disconnect
if (!targetController.canRespond()) {
break;
}
// stop by cancel/done
if (!targetController.choose(outcome, this, source, game)) {
break;
}
// TODO: miss auto-choose code? see chooseTarget below
// TODO: miss random code? see chooseTarget below
chosen = isChosen(game);
// stop by full complete
if (isChoiceCompleted(abilityControllerId, source, game)) {
break;
}
// stop by nothing to use (actual for human and done button)
if (prevTargetsCount == this.getTargets().size()) {
break;
}
// can select next target
} while (true);
chosen = isChosen(game);
return this.getTargets().size() > 0;
}
@Override
@ -389,44 +455,80 @@ public abstract class TargetImpl implements Target {
return false;
}
List<UUID> possibleTargets = new ArrayList<>(possibleTargets(playerId, source, game));
UUID abilityControllerId = playerId;
if (this.getTargetController() != null && this.getAbilityController() != null) {
abilityControllerId = this.getAbilityController();
}
List<UUID> randomPossibleTargets = new ArrayList<>(possibleTargets(playerId, source, game));
chosen = false;
do {
int prevTargetsCount = this.getTargets().size();
// stop by disconnect
if (!targetController.canRespond()) {
chosen = isChosen(game);
return chosen;
break;
}
// MAKE A CHOICE
if (isRandom()) {
if (possibleTargets.isEmpty()) {
chosen = isChosen(game);
return chosen;
// random choice
// stop on nothing to choose
if (randomPossibleTargets.isEmpty()) {
break;
}
// find valid target
while (!possibleTargets.isEmpty()) {
int index = RandomUtil.nextInt(possibleTargets.size());
if (this.canTarget(playerId, possibleTargets.get(index), source, game)) {
this.addTarget(possibleTargets.get(index), source, game);
possibleTargets.remove(index);
// add valid random target one by one
while (!randomPossibleTargets.isEmpty()) {
UUID possibleTarget = RandomUtil.randomFromCollection(randomPossibleTargets);
if (this.canTarget(playerId, possibleTarget, source, game) && !this.contains(possibleTarget)) {
this.addTarget(possibleTarget, source, game);
randomPossibleTargets.remove(possibleTarget);
break;
} else {
possibleTargets.remove(index);
randomPossibleTargets.remove(possibleTarget);
}
}
// continue to next target
} else {
// Try to autochoosen
// player's choice
UUID autoChosenId = tryToAutoChoose(playerId, source, game);
if (autoChosenId != null) {
if (autoChosenId != null && !this.contains(autoChosenId)) {
// auto-choose
addTarget(autoChosenId, source, game);
} else if (!targetController.chooseTarget(outcome, this, source, game)) { // If couldn't autochoose ask player
chosen = isChosen(game);
return chosen;
// continue to next target (example: auto-choose must fill min/max = 2 from 2 possible cards)
} else {
// manual
// stop by cancel/done
if (!targetController.chooseTarget(outcome, this, source, game)) {
break;
}
// continue to next target
}
}
chosen = isChosen(game);
} while (!doneChoosing(game));
return isChosen(game);
chosen = isChosen(game);
// stop by full complete
if (isChoiceCompleted(abilityControllerId, source, game)) {
break;
}
// stop by nothing to choose (actual for human and done button?)
if (prevTargetsCount == this.getTargets().size()) {
break;
}
// can select next target
} while (true);
chosen = isChosen(game);
return this.getTargets().size() > 0;
}
@Override
@ -665,6 +767,7 @@ public abstract class TargetImpl implements Target {
public void setTargetAmount(UUID targetId, int amount, Game game) {
targets.put(targetId, amount);
rememberZoneChangeCounter(targetId, game);
chosen = isChosen(game);
}
@Override
@ -716,9 +819,20 @@ public abstract class TargetImpl implements Target {
} else {
playerAutoTargetLevel = 2;
}
// freeze protection on disconnect - auto-choice works for online players only
boolean isOnline = player.canRespond();
if (!player.isGameUnderControl()) {
Player controllingPlayer = game.getPlayer(player.getTurnControlledBy());
if (player.isHuman()) {
isOnline = controllingPlayer.canRespond();
}
}
String abilityText = source.getRule(true).toLowerCase();
boolean strictModeEnabled = player.getStrictChooseMode();
boolean canAutoChoose = this.getMinNumberOfTargets() == this.getMaxNumberOfTargets() // Targets must be picked
&& isOnline
&& possibleTargets.size() == this.getMinNumberOfTargets() - this.getSize() // Available targets are equal to the number that must be picked
&& !strictModeEnabled // Test AI is not set to strictChooseMode(true)
&& playerAutoTargetLevel > 0 // Human player has enabled auto-choose in settings
@ -775,4 +889,13 @@ public abstract class TargetImpl implements Target {
return null;
}
@Override
public String toString() {
return this.getClass().getSimpleName()
+ ", from " + this.getMinNumberOfTargets()
+ " to " + this.getMaxNumberOfTargets()
+ ", " + this.getDescription()
+ ", selected " + this.getTargets().size();
}
}

View file

@ -1,11 +1,13 @@
package mage.target;
import mage.MageObject;
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 mage.util.DebugUtil;
import java.util.*;
import java.util.stream.Collectors;
@ -38,12 +40,19 @@ public class Targets extends ArrayList<Target> implements Copyable<Targets> {
return this;
}
public List<Target> getUnchosen(Game game) {
return stream().filter(target -> !target.isChoiceSelected()).collect(Collectors.toList());
public Target getNextUnchosen(Game game) {
return getNextUnchosen(game, 0);
}
public boolean doneChoosing(Game game) {
return stream().allMatch(t -> t.doneChoosing(game));
public Target getNextUnchosen(Game game, int unchosenIndex) {
List<Target> res = stream()
.filter(target -> !target.isChoiceSelected())
.collect(Collectors.toList());
return unchosenIndex < res.size() ? res.get(unchosenIndex) : null;
}
public boolean isChoiceCompleted(Game game) {
return stream().allMatch(t -> t.isChoiceCompleted(game));
}
public void clearChosen() {
@ -62,19 +71,38 @@ public class Targets extends ArrayList<Target> implements Copyable<Targets> {
return false;
}
if (this.size() > 0 && !this.doneChoosing(game)) {
// in test mode some targets can be predefined already, e.g. by cast/activate command
// so do not clear chosen status here
if (this.size() > 0) {
do {
// stop on disconnect or nothing to choose
if (!player.canRespond() || !canChoose(playerId, source, game)) {
return false;
break;
}
Target target = this.getUnchosen(game).get(0);
if (!target.choose(outcome, playerId, sourceId, source, game)) {
return false;
// stop on complete
Target target = this.getNextUnchosen(game);
if (target == null) {
break;
}
} while (!doneChoosing(game));
// stop on cancel/done
if (!target.choose(outcome, playerId, sourceId, source, game)) {
if (!target.isChosen(game)) {
break;
}
}
// target done, can take next one
} while (true);
}
return true;
if (DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS && !game.isSimulation()) {
printDebugTargets("choose finish", this, source, game);
}
return isChosen(game);
}
public boolean chooseTargets(Outcome outcome, UUID playerId, Ability source, boolean noMana, Game game, boolean canCancel) {
@ -83,43 +111,100 @@ public class Targets extends ArrayList<Target> implements Copyable<Targets> {
return false;
}
if (this.size() > 0 && !this.doneChoosing(game)) {
// in test mode some targets can be predefined already, e.g. by cast/activate command
// so do not clear chosen status here
if (this.size() > 0) {
do {
// stop on disconnect or nothing to choose
if (!player.canRespond() || !canChoose(playerId, source, game)) {
return false;
break;
}
Target target = this.getUnchosen(game).get(0);
UUID targetController = playerId;
// stop on complete
Target target = this.getNextUnchosen(game);
if (target == null) {
break;
}
// some targets can have controller different than ability controller
UUID targetController = playerId;
if (target.getTargetController() != null) {
targetController = target.getTargetController();
}
// if cast without mana (e.g. by suspend you may not be able to cancel the casting if you are able to cast it
// disable cancel button - if cast without mana (e.g. by suspend you may not be able to cancel the casting if you are able to cast it
if (noMana) {
target.setRequired(true);
}
// can be cancel by user
// enable cancel button
if (canCancel) {
target.setRequired(false);
}
// make response checks
// stop on cancel/done
if (!target.chooseTarget(outcome, targetController, source, game)) {
return false;
if (!target.isChosen(game)) {
break;
}
}
// Check if there are some rules for targets are violated, if so reset the targets and start again
if (this.getUnchosen(game).isEmpty()
// reset on wrong restrictions and start from scratch
if (this.getNextUnchosen(game) == null
&& game.replaceEvent(new GameEvent(GameEvent.EventType.TARGETS_VALID, source.getSourceId(), source, source.getControllerId()), source)) {
//game.restoreState(state, "Targets");
clearChosen();
}
} while (!doneChoosing(game));
// target done, can take next one
} while (true);
}
if (DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS && !game.isSimulation()) {
printDebugTargets("chooseTargets finish", this, source, game);
}
return isChosen(game);
}
public static void printDebugTargets(String name, Targets targets, Ability source, Game game) {
List<String> output = new ArrayList<>();
printDebugTargets(name, targets, source, game, output);
output.forEach(System.out::println);
}
public static void printDebugTargets(String name, Targets targets, Ability source, Game game, List<String> output) {
output.add("");
output.add(name + ":");
output.add(String.format("* chosen: %s", targets.isChosen(game) ? "yes" : "no"));
output.add(String.format("* ability: %s", source));
for (int i = 0; i < targets.size(); i++) {
Target target = targets.get(i);
output.add(String.format("* target %d: %s", i + 1, target));
if (target.getTargets().isEmpty()) {
output.add(" - no choices");
} else {
for (int j = 0; j < target.getTargets().size(); j++) {
UUID targetId = target.getTargets().get(j);
String targetInfo;
Player targetPlayer = game.getPlayer(targetId);
if (targetPlayer != null) {
targetInfo = targetPlayer.toString();
} else {
MageObject targetObject = game.getObject(targetId);
if (targetObject != null) {
targetInfo = targetObject.toString();
} else {
targetInfo = "unknown " + targetId;
}
}
if (target instanceof TargetAmount) {
output.add(String.format(" - choice %d.%d: amount %d, %s", i + 1, j + 1, target.getTargetAmount(targetId), targetInfo));
} else {
output.add(String.format(" - choice %d.%d: %s", i + 1, j + 1, targetInfo));
}
}
}
}
return true;
}
public boolean stillLegal(Ability source, Game game) {

View file

@ -76,20 +76,44 @@ public class TargetCardInLibrary extends TargetCard {
Cards cardsId = new CardsImpl();
cards.forEach(cardsId::add);
UUID abilityControllerId = playerId;
if (this.getTargetController() != null && this.getAbilityController() != null) {
abilityControllerId = this.getAbilityController();
}
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 = isChosen(game);
} while (!doneChoosing(game));
int prevTargetsCount = this.getTargets().size();
return isChosen(game);
// stop by disconnect
if (!player.canRespond()) {
break;
}
// stop by cancel/done
// TODO: need research - why it used chooseTarget instead choose? Need random and other options?
// someday must be replaced by player.choose (require whole tests fix from addTarget to setChoice)
if (!player.chooseTarget(outcome, cardsId, this, source, game)) {
return chosen;
}
chosen = isChosen(game);
// stop by full complete
if (isChoiceCompleted(abilityControllerId, source, game)) {
break;
}
// stop by nothing to use (actual for human and done button)
if (prevTargetsCount == this.getTargets().size()) {
break;
}
// can select next target
} while (true);
chosen = isChosen(game);
return this.getTargets().size() > 0;
}
@Override

View file

@ -133,7 +133,7 @@ public class TargetPermanentOrPlayer extends TargetImpl {
}
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) {
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) { // TODO: miss source?
if (permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game) && filter.match(permanent, sourceControllerId, source, game)) {
count++;
if (count >= this.minNumberOfTargets) {

View file

@ -17,6 +17,11 @@ public class DebugUtil {
public static boolean AI_ENABLE_DEBUG_MODE = false;
public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount
// GAME
// print detail target info for activate/cast/trigger only, not a single choose dialog
// can be useful to debug unit tests, auto-choose or AI
public static boolean GAME_SHOW_CHOOSE_TARGET_LOGS = false;
// cards basic (card panels)
public static boolean GUI_CARD_DRAW_OUTER_BORDER = false;
public static boolean GUI_CARD_DRAW_INNER_BORDER = false;