- Use the original implementation for orderAndPlaySimultaneousSa for the AI for the time being until the AI side of the implementation can be improved.

This commit is contained in:
Hans Mackowiak
2021-02-10 04:00:37 +00:00
committed by Michael Kamensky
parent 06e0ed9a79
commit 9d0433f812
27 changed files with 347 additions and 380 deletions

View File

@@ -1,6 +1,9 @@
package forge.match.input;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import forge.FThreads;
import forge.game.GameEntity;
import forge.game.GameObject;
@@ -22,6 +25,7 @@ import forge.util.ITriggerEvent;
import forge.util.TextUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -33,21 +37,25 @@ public final class InputSelectTargets extends InputSyncronizedBase {
private final Map<GameEntity, Integer> targetDepth = new HashMap<>();
private final TargetRestrictions tgt;
private final SpellAbility sa;
private final Collection<Integer> divisionValues;
private Card lastTarget = null;
private boolean bCancel = false;
private boolean bOk = false;
private final boolean mandatory;
private Predicate<GameObject> filter;
private static final long serialVersionUID = -1091595663541356356L;
public final boolean hasCancelled() { return bCancel; }
public final boolean hasPressedOk() { return bOk; }
public InputSelectTargets(final PlayerControllerHuman controller, final List<Card> choices, final SpellAbility sa, final boolean mandatory) {
public InputSelectTargets(final PlayerControllerHuman controller, final List<Card> choices, final SpellAbility sa, final boolean mandatory, Collection<Integer> divisionValues, Predicate<GameObject> filter) {
super(controller);
this.choices = choices;
this.tgt = sa.getTargetRestrictions();
this.sa = sa;
this.mandatory = mandatory;
this.divisionValues = divisionValues;
this.filter = filter;
controller.getGui().setSelectables(CardView.getCollection(choices));
final PlayerZoneUpdates zonesToUpdate = new PlayerZoneUpdates();
for (final Card c : choices) {
@@ -97,7 +105,7 @@ public final class InputSelectTargets extends InputSyncronizedBase {
sb.append(sa.getUniqueTargets());
}
final int maxTargets = tgt.getMaxTargets(sa.getHostCard(), sa);
final int maxTargets = sa.getMaxTargets();
final int targeted = sa.getTargets().size();
if(maxTargets > 1) {
sb.append(TextUtil.concatNoSpace("\n(", String.valueOf(maxTargets - targeted), " more can be targeted)"));
@@ -108,10 +116,10 @@ public final class InputSelectTargets extends InputSyncronizedBase {
"(Targeting ERROR)", "");
showMessage(message, sa.getView());
if (tgt.isDividedAsYouChoose() && tgt.getMinTargets(sa.getHostCard(), sa) == 0 && sa.getTargets().size() == 0) {
if (sa.isDividedAsYouChoose() && sa.getMinTargets() == 0 && sa.getTargets().size() == 0) {
// extra logic for Divided with min targets = 0, should only work if num targets are 0 too
getController().getGui().updateButtons(getOwner(), true, true, false);
} else if (!tgt.isMinTargetsChosen(sa.getHostCard(), sa) || tgt.isDividedAsYouChoose()) {
} else if (!sa.isMinTargetChosen() || sa.isDividedAsYouChoose()) {
// If reached Minimum targets, enable OK button
if (mandatory && tgt.hasCandidates(sa, true)) {
// Player has to click on a target
@@ -239,40 +247,11 @@ public final class InputSelectTargets extends InputSyncronizedBase {
return false;
}
if (tgt.isDividedAsYouChoose()) {
final int stillToDivide = tgt.getStillToDivide();
int allocatedPortion = 0;
// allow allocation only if the max targets isn't reached and there are more candidates
if ((sa.getTargets().size() + 1 < tgt.getMaxTargets(sa.getHostCard(), sa))
&& (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) {
final ImmutableList.Builder<Integer> choices = ImmutableList.builder();
for (int i = 1; i <= stillToDivide; i++) {
choices.add(Integer.valueOf(i));
}
String apiBasedMessage = "Distribute how much to ";
if (sa.getApi() == ApiType.DealDamage) {
apiBasedMessage = "Select how much damage to deal to ";
}
else if (sa.getApi() == ApiType.PreventDamage) {
apiBasedMessage = "Select how much damage to prevent to ";
}
else if (sa.getApi() == ApiType.PutCounter) {
apiBasedMessage = "Select how many counters to distribute to ";
}
final StringBuilder sb = new StringBuilder();
sb.append(apiBasedMessage);
sb.append(card.toString());
final Integer chosen = getController().getGui().oneOrNone(sb.toString(), choices.build());
if (chosen == null) {
return true; //still return true since there was a valid choice
}
allocatedPortion = chosen;
if (sa.isDividedAsYouChoose()) {
Boolean val = onDividedAsYouChoose(card);
if (val != null) {
return val;
}
else { // otherwise assign the rest of the damage/protection
allocatedPortion = stillToDivide;
}
tgt.setStillToDivide(stillToDivide - allocatedPortion);
tgt.addDividedAllocation(card, allocatedPortion);
}
addTarget(card);
return true;
@@ -305,12 +284,49 @@ public final class InputSelectTargets extends InputSyncronizedBase {
showMessage(sa.getHostCard() + " - Cannot target this player (Hexproof? Protection? Restrictions?).");
return;
}
if (filter != null && !filter.apply(player)) {
showMessage(sa.getHostCard() + " - Cannot target this player (Hexproof? Protection? Restrictions?).");
return;
}
if (tgt.isDividedAsYouChoose()) {
final int stillToDivide = tgt.getStillToDivide();
if (sa.isDividedAsYouChoose()) {
Boolean val = onDividedAsYouChoose(player);
if (val != null) {
return;
}
}
addTarget(player);
}
protected Boolean onDividedAsYouChoose(GameObject go) {
if (divisionValues != null) {
if (divisionValues.isEmpty()) {
return false;
}
String apiBasedMessage = "Distribute how much to ";
if (sa.getApi() == ApiType.DealDamage) {
apiBasedMessage = "Select how much damage to deal to ";
}
else if (sa.getApi() == ApiType.PreventDamage) {
apiBasedMessage = "Select how much damage to prevent to ";
}
else if (sa.getApi() == ApiType.PutCounter) {
apiBasedMessage = "Select how many counters to distribute to ";
}
final StringBuilder sb = new StringBuilder();
sb.append(apiBasedMessage);
sb.append(go.toString());
final Integer chosen = getController().getGui().oneOrNone(sb.toString(), Lists.newArrayList(divisionValues));
if (chosen == null) {
return true; //still return true since there was a valid choice
}
divisionValues.remove(chosen);
sa.addDividedAllocation(go, chosen);
} else {
final int stillToDivide = sa.getStillToDivide();
int allocatedPortion = 0;
// allow allocation only if the max targets isn't reached and there are more candidates
if ((sa.getTargets().size() + 1 < tgt.getMaxTargets(sa.getHostCard(), sa)) && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) {
if ((sa.getTargets().size() + 1 < sa.getMaxTargets()) && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) {
final ImmutableList.Builder<Integer> choices = ImmutableList.builder();
for (int i = 1; i <= stillToDivide; i++) {
choices.add(Integer.valueOf(i));
@@ -323,19 +339,18 @@ public final class InputSelectTargets extends InputSyncronizedBase {
}
final StringBuilder sb = new StringBuilder();
sb.append(apiBasedMessage);
sb.append(player.getName());
sb.append(go.toString());
final Integer chosen = getController().getGui().oneOrNone(sb.toString(), choices.build());
if (null == chosen) {
return;
return true;
}
allocatedPortion = chosen;
} else { // otherwise assign the rest of the damage/protection
allocatedPortion = stillToDivide;
}
tgt.setStillToDivide(stillToDivide - allocatedPortion);
tgt.addDividedAllocation(player, allocatedPortion);
sa.addDividedAllocation(go, allocatedPortion);
}
addTarget(player);
return null;
}
private void addTarget(final GameEntity ge) {
@@ -366,7 +381,7 @@ public final class InputSelectTargets extends InputSyncronizedBase {
}
private boolean hasAllTargets() {
return tgt.isMaxTargetsChosen(sa.getHostCard(), sa) || ( tgt.getStillToDivide() == 0 && tgt.isDividedAsYouChoose());
return sa.isMaxTargetChosen() || (sa.isDividedAsYouChoose() && sa.getStillToDivide() == 0);
}
@Override

View File

@@ -1061,19 +1061,20 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
* SpellAbility, forge.card.spellability.SpellAbilityStackInstance)
*/
@Override
public TargetChoices chooseNewTargetsFor(final SpellAbility ability) {
public TargetChoices chooseNewTargetsFor(final SpellAbility ability, Predicate<GameObject> filter, boolean optional) {
final SpellAbility sa = ability.isWrapper() ? ((WrappedAbility) ability).getWrappedAbility() : ability;
if (sa.getTargetRestrictions() == null) {
if (!sa.usesTargeting()) {
return null;
}
final TargetChoices oldTarget = sa.getTargets();
final TargetSelection select = new TargetSelection(this, sa);
sa.resetTargets();
if (select.chooseTargets(oldTarget.size())) {
if (select.chooseTargets(oldTarget.size(), Lists.newArrayList(oldTarget.getDividedValues()), filter, optional)) {
return sa.getTargets();
} else {
sa.setTargets(oldTarget);
// Return old target, since we had to reset them above
return oldTarget;
return null;
}
}
@@ -1795,11 +1796,8 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
player.getGame().getStackZone().add(next.getHostCard());
}
// TODO check if static abilities needs to be run for things affecting the copy?
if (next.isMayChooseNewTargets() && !next.setupTargets()) {
// if targets can't be done, remove copy from existence
if (next.isSpell()) {
next.getHostCard().ceaseToExist();
}
if (next.isMayChooseNewTargets()) {
next.setupNewTargets(player);
}
}
player.getGame().getStack().add(next);
@@ -1820,7 +1818,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
@Override
public boolean chooseTargetsFor(final SpellAbility currentAbility) {
final TargetSelection select = new TargetSelection(this, currentAbility);
return select.chooseTargets(null);
return select.chooseTargets(null, null, null, false);
}
@Override

View File

@@ -17,6 +17,8 @@
*/
package forge.player;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.game.Game;
@@ -25,6 +27,7 @@ import forge.game.GameEntityView;
import forge.game.GameEntityViewMap;
import forge.game.GameObject;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardUtil;
import forge.game.card.CardView;
import forge.game.player.PlayerView;
@@ -38,6 +41,7 @@ import forge.match.input.InputSelectTargets;
import forge.util.Aggregates;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -70,15 +74,15 @@ public class TargetSelection {
return ability.isTrigger() || getTgt().getMandatory();
}
public final boolean chooseTargets(Integer numTargets) {
public final boolean chooseTargets(Integer numTargets, Collection<Integer> divisionValues, Predicate<GameObject> filter, boolean optional) {
if (!ability.usesTargeting()) {
throw new RuntimeException("TargetSelection.chooseTargets called for ability that does not target - " + ability);
}
final TargetRestrictions tgt = getTgt();
// Number of targets is explicitly set only if spell is being redirected (ex. Swerve or Redirect)
final int minTargets = numTargets != null ? numTargets.intValue() : tgt.getMinTargets(ability.getHostCard(), ability);
final int maxTargets = numTargets != null ? numTargets.intValue() : tgt.getMaxTargets(ability.getHostCard(), ability);
final int minTargets = numTargets != null ? numTargets.intValue() : ability.getMinTargets();
final int maxTargets = numTargets != null ? numTargets.intValue() : ability.getMaxTargets();
//final int maxTotalCMC = tgt.getMaxTotalCMC(ability.getHostCard(), ability);
final int numTargeted = ability.getTargets().size();
final boolean isSingleZone = ability.getTargetRestrictions().isSingleZone();
@@ -92,7 +96,7 @@ public class TargetSelection {
return false;
}
if (this.bTargetingDone && hasEnoughTargets || hasAllTargets || tgt.isDividedAsYouChoose() && tgt.getStillToDivide() == 0) {
if (this.bTargetingDone && hasEnoughTargets || hasAllTargets || ability.isDividedAsYouChoose() && divisionValues == null && ability.getStillToDivide() == 0) {
return true;
}
@@ -107,11 +111,10 @@ public class TargetSelection {
}
final List<ZoneType> zones = tgt.getZone();
final boolean mandatory = isMandatory() && hasCandidates;
final boolean mandatory = isMandatory() && hasCandidates && !optional;
final boolean choiceResult;
final boolean random = tgt.isRandomTarget();
if (random) {
if (tgt.isRandomTarget() && numTargets == null) {
final List<GameEntity> candidates = tgt.getAllCandidates(this.ability, true);
final GameObject choice = Aggregates.random(candidates);
return ability.getTargets().add(choice);
@@ -122,7 +125,11 @@ public class TargetSelection {
return this.chooseCardFromStack(mandatory);
}
else {
final List<Card> validTargets = CardUtil.getValidCardsToTarget(tgt, ability);
List<Card> validTargets = CardUtil.getValidCardsToTarget(tgt, ability);
if (filter != null) {
validTargets = new CardCollection(Iterables.filter(validTargets, filter));
}
// single zone
if (isSingleZone) {
final List<Card> removeCandidates = new ArrayList<>();
@@ -149,8 +156,8 @@ public class TargetSelection {
//if only one valid target card for triggered ability, auto-target that card
//only do this for triggered abilities to prevent auto-targeting when user chooses
//to play a spell or activat an ability
if (tgt.isDividedAsYouChoose()) {
tgt.addDividedAllocation(validTargets.get(0), tgt.getStillToDivide());
if (ability.isDividedAsYouChoose()) {
ability.addDividedAllocation(validTargets.get(0), ability.getStillToDivide());
}
return ability.getTargets().add(validTargets.get(0));
}
@@ -162,7 +169,7 @@ public class TargetSelection {
PlayerView playerView = controller.getLocalPlayerView();
PlayerZoneUpdates playerZoneUpdates = controller.getGui().openZones(playerView, zones, playersWithValidTargets);
if (!zones.contains(ZoneType.Stack)) {
InputSelectTargets inp = new InputSelectTargets(controller, validTargets, ability, mandatory);
InputSelectTargets inp = new InputSelectTargets(controller, validTargets, ability, mandatory, divisionValues, filter);
inp.showAndWait();
choiceResult = !inp.hasCancelled();
bTargetingDone = inp.hasPressedOk();
@@ -174,7 +181,7 @@ public class TargetSelection {
}
}
// some inputs choose cards one-by-one and need to be called again
return choiceResult && chooseTargets(numTargets);
return choiceResult && chooseTargets(numTargets, divisionValues, filter, optional);
}
private final boolean chooseCardFromList(final List<Card> choices, final boolean targeted, final boolean mandatory) {
@@ -232,7 +239,7 @@ public class TargetSelection {
}
final String msgDone = "[FINISH TARGETING]";
if (this.getTgt().isMinTargetsChosen(this.ability.getHostCard(), this.ability)) {
if (ability.isMinTargetChosen()) {
// is there a more elegant way of doing this?
choicesFiltered.add(msgDone);
}
@@ -282,12 +289,12 @@ public class TargetSelection {
}
while(!bTargetingDone) {
if (tgt.isMaxTargetsChosen(this.ability.getHostCard(), this.ability)) {
if (ability.isMaxTargetChosen()) {
bTargetingDone = true;
return true;
}
if (!selectOptions.contains("[FINISH TARGETING]") && tgt.isMinTargetsChosen(this.ability.getHostCard(), this.ability)) {
if (!selectOptions.contains("[FINISH TARGETING]") && ability.isMinTargetChosen()) {
selectOptions.add("[FINISH TARGETING]");
}