AI: improved stability:

- fixed game errors with source related filters (related to #13713);
- fixed game freeze in hand's cards selection (related to #13290);
- fixed game freeze in target amount selection with X=0 (related to #13290);
This commit is contained in:
Oleg Agafonov 2025-06-14 16:03:44 +04:00
parent e8342e1f11
commit 6ad2cdaa78
5 changed files with 38 additions and 55 deletions

View file

@ -692,7 +692,7 @@ public class ComputerPlayer extends PlayerImpl {
|| target.getOriginalTarget() instanceof TargetCardInHand) { || target.getOriginalTarget() instanceof TargetCardInHand) {
isAddedSomething = false; isAddedSomething = false;
if (outcome.isGood()) { if (outcome.isGood()) {
// good // good - choose max possible
Cards cards = new CardsImpl(possibleTargets); Cards cards = new CardsImpl(possibleTargets);
List<Card> cardsInHand = new ArrayList<>(cards.getCards(game)); List<Card> cardsInHand = new ArrayList<>(cards.getCards(game));
while (!target.isChosen(game) while (!target.isChosen(game)
@ -703,36 +703,42 @@ public class ComputerPlayer extends PlayerImpl {
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) { if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
target.addTarget(card.getId(), source, game); target.addTarget(card.getId(), source, game);
isAddedSomething = true; isAddedSomething = true;
cardsInHand.remove(card);
if (target.isChoiceCompleted(game)) { if (target.isChoiceCompleted(game)) {
return true; return true;
} }
} }
cardsInHand.remove(card);
} }
} }
} else { } else {
// bad // bad - choose the lowest possible
findPlayables(game); findPlayables(game);
for (Card card : unplayable.values()) { for (Card card : unplayable.values()) {
if (target.isChosen(game)) {
return isAddedSomething;
}
if (possibleTargets.contains(card.getId()) if (possibleTargets.contains(card.getId())
&& target.canTarget(abilityControllerId, card.getId(), source, game) && target.canTarget(abilityControllerId, card.getId(), source, game)
&& !target.contains(card.getId())) { && !target.contains(card.getId())) {
target.addTarget(card.getId(), source, game); target.addTarget(card.getId(), source, game);
isAddedSomething = true; isAddedSomething = true;
if (target.isChoiceCompleted(game)) { if (target.isChosen(game)) {
return true; return isAddedSomething;
} }
} }
} }
if (!hand.isEmpty()) { if (!hand.isEmpty()) {
for (Card card : hand.getCards(game)) { for (Card card : hand.getCards(game)) {
if (target.isChosen(game)) {
return isAddedSomething;
}
if (possibleTargets.contains(card.getId()) if (possibleTargets.contains(card.getId())
&& target.canTarget(abilityControllerId, card.getId(), source, game) && target.canTarget(abilityControllerId, card.getId(), source, game)
&& !target.contains(card.getId())) { && !target.contains(card.getId())) {
target.addTarget(card.getId(), source, game); target.addTarget(card.getId(), source, game);
isAddedSomething = true; isAddedSomething = true;
if (target.isChoiceCompleted(game)) { if (target.isChosen(game)) {
return true; return isAddedSomething;
} }
} }
} }
@ -854,39 +860,6 @@ public class ComputerPlayer extends PlayerImpl {
if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) { if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
List<Permanent> targets; List<Permanent> targets;
TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget()); TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget());
if (outcome.isGood()) {
targets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
} else {
targets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
}
if (targets.isEmpty()) {
if (outcome.isGood()) {
if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
return tryAddTarget(target, getId(), source, game);
}
} else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
return tryAddTarget(target, randomOpponentId, source, game);
}
}
if (targets.isEmpty() && target.isRequired(source)) {
targets = game.getBattlefield().getActivePermanents(((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), playerId, game);
}
for (Permanent permanent : targets) {
List<UUID> alreadyTargeted = target.getTargets();
if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
return tryAddTarget(target, permanent.getId(), source, game);
}
}
}
}
if (target.getOriginalTarget() instanceof TargetPlayerOrPlaneswalker
|| target.getOriginalTarget() instanceof TargetOpponentOrPlaneswalker) {
List<Permanent> targets;
TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget());
// TODO: in multiplayer game there many opponents - if random opponents don't have targets then AI must use next opponent, but it skips // TODO: in multiplayer game there many opponents - if random opponents don't have targets then AI must use next opponent, but it skips
// (e.g. you randomOpponentId must be replaced by List<UUID> randomOpponents) // (e.g. you randomOpponentId must be replaced by List<UUID> randomOpponents)
@ -1239,6 +1212,7 @@ public class ComputerPlayer extends PlayerImpl {
throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName()); throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName());
} //end of chooseTarget method } //end of chooseTarget method
@Deprecated // TODO: replace by source only version
protected Card selectCard(UUID abilityControllerId, List<Card> cards, Outcome outcome, Target target, Game game) { protected Card selectCard(UUID abilityControllerId, List<Card> cards, Outcome outcome, Target target, Game game) {
return selectCardInner(abilityControllerId, cards, outcome, target, null, game); return selectCardInner(abilityControllerId, cards, outcome, target, null, game);
} }
@ -1279,6 +1253,10 @@ public class ComputerPlayer extends PlayerImpl {
log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString()); log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString());
} }
if (target.getAmountRemaining() <= 0) {
return false;
}
UUID sourceId = source != null ? source.getSourceId() : null; UUID sourceId = source != null ? source.getSourceId() : null;
// sometimes a target selection can be made from a player that does not control the ability // sometimes a target selection can be made from a player that does not control the ability
@ -2576,6 +2554,7 @@ public class ComputerPlayer extends PlayerImpl {
tournament.submitDeck(playerId, deck); tournament.submitDeck(playerId, deck);
} }
@Deprecated // TODO: replace by source only version
public Card selectBestCard(List<Card> cards, List<ColoredManaSymbol> chosenColors) { public Card selectBestCard(List<Card> cards, List<ColoredManaSymbol> chosenColors) {
return selectBestCardInner(cards, chosenColors, null, null, null); return selectBestCardInner(cards, chosenColors, null, null, null);
} }
@ -2623,14 +2602,6 @@ public class ComputerPlayer extends PlayerImpl {
return bestCard; return bestCard;
} }
public Card selectWorstCard(List<Card> cards, List<ColoredManaSymbol> chosenColors) {
return selectWorstCardInner(cards, chosenColors, null, null, null);
}
public Card selectWorstCardTarget(List<Card> cards, List<ColoredManaSymbol> chosenColors, Target target, Ability targetingSource, Game game) {
return selectWorstCardInner(cards, chosenColors, target, targetingSource, game);
}
/** /**
* @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget * @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
*/ */
@ -3097,6 +3068,7 @@ public class ComputerPlayer extends PlayerImpl {
return before != after; return before != after;
} }
@Deprecated // TODO: replace by source only version
private boolean selectPlayer(Outcome outcome, Target target, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) { private boolean selectPlayer(Outcome outcome, Target target, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) {
return selectPlayerInner(outcome, target, null, abilityControllerId, randomOpponentId, game, required); return selectPlayerInner(outcome, target, null, abilityControllerId, randomOpponentId, game, required);
} }

View file

@ -288,6 +288,10 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
@Override @Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
if (target.getAmountRemaining() <= 0) {
return false;
}
Set<UUID> possibleTargets = target.possibleTargets(playerId, source, game); Set<UUID> possibleTargets = target.possibleTargets(playerId, source, game);
if (possibleTargets.isEmpty()) { if (possibleTargets.isEmpty()) {
return !target.isRequired(source); return !target.isRequired(source);

View file

@ -1042,6 +1042,10 @@ public class HumanPlayer extends PlayerImpl {
return false; return false;
} }
if (target.getAmountRemaining() <= 0) {
return false;
}
if (source == null) { if (source == null) {
return false; return false;
} }

View file

@ -2328,7 +2328,7 @@ public class TestPlayer implements Player {
continue; continue;
} }
if (hasObjectTargetNameOrAlias(permanent, targetName)) { if (hasObjectTargetNameOrAlias(permanent, targetName)) {
if (target.isNotTarget() || target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) {
target.add(permanent.getId(), game); target.add(permanent.getId(), game);
isAddedSomething = true; isAddedSomething = true;
@ -2336,7 +2336,7 @@ public class TestPlayer implements Player {
} }
} }
} else if ((permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove search by exp code? } else if ((permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove search by exp code?
if (target.isNotTarget() || target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) {
target.add(permanent.getId(), game); target.add(permanent.getId(), game);
isAddedSomething = true; isAddedSomething = true;
@ -2371,7 +2371,7 @@ public class TestPlayer implements Player {
isAddedSomething = false; isAddedSomething = false;
for (Player player : game.getPlayers().values()) { for (Player player : game.getPlayers().values()) {
if (player.getName().equals(choiceRecord)) { if (player.getName().equals(choiceRecord)) {
if (target.canTarget(abilityControllerId, player.getId(), null, game) && !target.contains(player.getId())) { if (target.canTarget(abilityControllerId, player.getId(), source, game) && !target.contains(player.getId())) {
target.add(player.getId(), game); target.add(player.getId(), game);
isAddedSomething = true; isAddedSomething = true;
} }
@ -2538,7 +2538,8 @@ public class TestPlayer implements Player {
String playerName = targetDefinition.substring(targetDefinition.indexOf("targetPlayer=") + 13); String playerName = targetDefinition.substring(targetDefinition.indexOf("targetPlayer=") + 13);
for (Player player : game.getPlayers().values()) { for (Player player : game.getPlayers().values()) {
if (player.getName().equals(playerName) if (player.getName().equals(playerName)
&& target.canTarget(abilityControllerId, player.getId(), source, game)) { && target.canTarget(abilityControllerId, player.getId(), source, game)
&& !target.contains(player.getId())) {
target.addTarget(player.getId(), source, game); target.addTarget(player.getId(), source, game);
targets.remove(targetDefinition); targets.remove(targetDefinition);
return true; return true;
@ -2887,7 +2888,7 @@ public class TestPlayer implements Player {
@Override @Override
public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) { public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) {
if (message.equals("Scry 1?")) { if (message != null && message.equals("Scry 1?")) {
return false; return false;
} }
assertAliasSupportInChoices(false); assertAliasSupportInChoices(false);
@ -4326,7 +4327,9 @@ public class TestPlayer implements Player {
// chooseTargetAmount calls for EACH target cycle (e.g. one target per click, see TargetAmount) // chooseTargetAmount calls for EACH target cycle (e.g. one target per click, see TargetAmount)
// if use want to stop choosing then chooseTargetAmount must return false (example: up to xxx) // if use want to stop choosing then chooseTargetAmount must return false (example: up to xxx)
Assert.assertNotEquals("chooseTargetAmount needs non zero amount remaining", 0, target.getAmountRemaining()); if (target.getAmountRemaining() <= 0) {
return false;
}
assertAliasSupportInTargets(true); assertAliasSupportInTargets(true);
if (!targets.isEmpty()) { if (!targets.isEmpty()) {

View file

@ -209,7 +209,7 @@ class OublietteTarget extends TargetSacrifice {
@Override @Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game) { public Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game) {
Set<UUID> possibleTargets = super.possibleTargets(sourceControllerId, source, game); Set<UUID> possibleTargets = super.possibleTargets(sourceControllerId, source, game);
possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, source, game));
return possibleTargets; return possibleTargets;
} }