mirror of
https://github.com/magefree/mage.git
synced 2025-12-28 22:42:03 -08:00
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:
parent
80d62727e1
commit
133e4fe425
84 changed files with 2737 additions and 743 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue