Refactor: added docs for a copy stack object code (#7662);

This commit is contained in:
Oleg Agafonov 2021-08-15 17:12:59 +04:00
parent c328c71ef9
commit f21c492ce2
16 changed files with 136 additions and 85 deletions

View file

@ -17,12 +17,12 @@ import java.util.List;
*/
public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect {
private static final class CopyApplier implements StackObjectCopyApplier {
private static final class ForEachCopyApplier implements StackObjectCopyApplier {
private final Iterator<MageObjectReferencePredicate> iterator;
private final Iterator<MageObjectReferencePredicate> newTargetTypes;
private CopyApplier(List<MageObjectReferencePredicate> predicates) {
this.iterator = predicates.iterator();
private ForEachCopyApplier(List<MageObjectReferencePredicate> copiesWithTargets) {
this.newTargetTypes = copiesWithTargets.iterator();
}
@Override
@ -30,9 +30,9 @@ public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect
}
@Override
public MageObjectReferencePredicate getNextPredicate() {
if (iterator.hasNext()) {
return iterator.next();
public MageObjectReferencePredicate getNextNewTargetType(int copyNumber) {
if (newTargetTypes.hasNext()) {
return newTargetTypes.next();
}
return null;
}
@ -50,7 +50,16 @@ public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect
protected abstract Player getPlayer(Game game, Ability source);
protected abstract List<MageObjectReferencePredicate> getPossibleTargets(StackObject stackObject, Player player, Ability source, Game game);
/**
* Prepare copies list. Each item must contain filter for new target (target type).
*
* @param stackObject
* @param player
* @param source
* @param game
* @return
*/
protected abstract List<MageObjectReferencePredicate> prepareCopiesWithTargets(StackObject stackObject, Player player, Ability source, Game game);
@Override
public boolean apply(Game game, Ability source) {
@ -59,10 +68,10 @@ public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect
if (actingPlayer == null || stackObject == null) {
return false;
}
List<MageObjectReferencePredicate> predicates = getPossibleTargets(stackObject, actingPlayer, source, game);
List<MageObjectReferencePredicate> copies = prepareCopiesWithTargets(stackObject, actingPlayer, source, game);
stackObject.createCopyOnStack(
game, source, actingPlayer.getId(), false,
predicates.size(), new CopyApplier(predicates)
copies.size(), new ForEachCopyApplier(copies)
);
return true;
}

View file

@ -1058,18 +1058,21 @@ public class Spell extends StackObjectImpl implements Card {
}
@Override
public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate predicate, Game game, Ability source, boolean chooseNewTargets) {
public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate newTargetFilterPredicate, Game game, Ability source, boolean chooseNewTargets) {
Spell spellCopy = this.copySpell(game, source, newControllerId);
if (applier != null) {
applier.modifySpell(spellCopy, game);
}
spellCopy.setZone(Zone.STACK, game); // required for targeting ex: Nivmagus Elemental
game.getStack().push(spellCopy);
if (predicate != null) {
spellCopy.chooseNewTargets(game, newControllerId, true, false, predicate);
// new targets
if (newTargetFilterPredicate != null) {
spellCopy.chooseNewTargets(game, newControllerId, true, false, newTargetFilterPredicate);
} else if (chooseNewTargets || applier != null) { // if applier is non-null but predicate is null then it's extra
spellCopy.chooseNewTargets(game, newControllerId);
}
game.fireEvent(new CopiedStackObjectEvent(this, spellCopy, newControllerId));
}

View file

@ -607,16 +607,19 @@ public class StackAbility extends StackObjectImpl implements Ability {
}
@Override
public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate predicate, Game game, Ability source, boolean chooseNewTargets) {
public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate newTargetFilterPredicate, Game game, Ability source, boolean chooseNewTargets) {
Ability newAbility = this.copy();
newAbility.newId();
StackAbility newStackAbility = new StackAbility(newAbility, newControllerId);
game.getStack().push(newStackAbility);
if (predicate != null) {
newStackAbility.chooseNewTargets(game, newControllerId, true, false, predicate);
// new targets
if (newTargetFilterPredicate != null) {
newStackAbility.chooseNewTargets(game, newControllerId, true, false, newTargetFilterPredicate);
} else if (chooseNewTargets || applier != null) { // if applier is non-null but predicate is null then it's extra
newStackAbility.chooseNewTargets(game, newControllerId);
}
game.fireEvent(new CopiedStackObjectEvent(this, newStackAbility, newControllerId));
}

View file

@ -29,7 +29,7 @@ public interface StackObject extends MageObject, Controllable {
Ability getStackAbility();
boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, Predicate<MageItem> extraPredicate);
boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, Predicate<MageItem> newTargetFilterPredicate);
boolean canTarget(Game game, UUID targetId);
@ -39,7 +39,7 @@ public interface StackObject extends MageObject, Controllable {
void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount, StackObjectCopyApplier applier);
void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate predicate, Game game, Ability source, boolean chooseNewTargets);
void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate newTargetFilterPredicate, Game game, Ability source, boolean chooseNewTargets);
boolean isTargetChanged();

View file

@ -42,18 +42,30 @@ public abstract class StackObjectImpl implements StackObject {
createCopyOnStack(game, source, newControllerId, chooseNewTargets, amount, null);
}
private static final class PredicateIterator implements Iterator<MageObjectReferencePredicate> {
/**
* Copy logic:
* - multiple copies allows
* - new targets for each copy allows
* - each next copy can have new target type/filter
* - player can choose copies order on stack
* <p>
* Code logic:
* - find all possible target types (any target or custom)
* - user can choose next target type to put on stack
* - put all copies with that target type and request next choice
* - implemented by iterator mechanic (target type -> target filter)
*/
private static final class NewTargetTypeIterator implements Iterator<MageObjectReferencePredicate> {
private final StackObjectCopyApplier applier;
private final Player player;
private final int amount;
private final Game game;
private Map<String, MageObjectReferencePredicate> predicateMap = null;
private int anyCount = 0;
private int setCount = 0;
private Iterator<MageObjectReferencePredicate> iterator = null;
private Choice choice = null;
private Map<String, MageObjectReferencePredicate> newTargetTypes = null;
private Iterator<MageObjectReferencePredicate> currentNewTargetType = null;
private Choice newTargetTypeChoiceDialog = null;
private PredicateIterator(Game game, UUID newControllerId, int amount, StackObjectCopyApplier applier) {
private NewTargetTypeIterator(Game game, UUID newControllerId, int amount, StackObjectCopyApplier applier) {
this.applier = applier;
this.player = game.getPlayer(newControllerId);
this.amount = amount;
@ -65,39 +77,47 @@ public abstract class StackObjectImpl implements StackObject {
return true;
}
private void makeMap() {
if (predicateMap != null) {
private void prepareNewTargetTypes() {
if (newTargetTypes != null) {
// already prepared
return;
}
predicateMap = new HashMap<>();
int currentAnyTargetNumber = 0;
int currentFilteredTargetNumber = 0;
newTargetTypes = new HashMap<>();
for (int i = 0; i < amount; i++) {
MageObjectReferencePredicate predicate = applier.getNextPredicate();
if (predicate == null) {
anyCount++;
MageObjectReferencePredicate newTargetType = applier.getNextNewTargetType(i + 1);
if (newTargetType == null) {
currentAnyTargetNumber++;
String message = "Any target";
if (anyCount > 1) {
message += " (" + anyCount + ")";
if (currentAnyTargetNumber > 1) {
message += " (" + currentAnyTargetNumber + ")";
}
predicateMap.put(message, predicate);
newTargetTypes.put(message, null);
continue;
}
setCount++;
predicateMap.put(predicate.getName(game), predicate);
currentFilteredTargetNumber++;
newTargetTypes.put(newTargetType.getName(game), newTargetType);
}
if ((setCount == 1 && anyCount == 0) || setCount == 0) {
iterator = predicateMap.values().stream().collect(Collectors.toList()).iterator();
// if only one target type then choose it by default
if ((currentFilteredTargetNumber == 1 && currentAnyTargetNumber == 0) || currentFilteredTargetNumber == 0) {
currentNewTargetType = newTargetTypes.values().stream().collect(Collectors.toList()).iterator();
}
}
private void makeChoice() {
if (choice != null) {
private void prepareFilterChooseDialog() {
// used choices removes from the dialog
if (newTargetTypeChoiceDialog != null) {
// already prepared
return;
}
choice = new ChoiceImpl(false, ChoiceHintType.CARD);
choice.setMessage("Choose the order of copies to go on the stack");
choice.setSubMessage("Press cancel to put the rest in any order");
choice.setChoices(new HashSet<>(predicateMap.keySet()));
newTargetTypeChoiceDialog = new ChoiceImpl(false, ChoiceHintType.CARD);
newTargetTypeChoiceDialog.setMessage("Choose the order of copies to go on the stack");
newTargetTypeChoiceDialog.setSubMessage("Press cancel to put the rest in any order");
newTargetTypeChoiceDialog.setChoices(new HashSet<>(newTargetTypes.keySet()));
}
@Override
@ -105,24 +125,30 @@ public abstract class StackObjectImpl implements StackObject {
if (player == null || applier == null) {
return null;
}
makeMap();
if (iterator != null) {
return iterator.hasNext() ? iterator.next() : null;
prepareNewTargetTypes();
if (currentNewTargetType != null) {
// target type already selected
return currentNewTargetType.hasNext() ? currentNewTargetType.next() : null;
}
makeChoice();
if (choice.getChoices().size() < 2) {
iterator = choice.getChoices().stream().map(predicateMap::get).iterator();
prepareFilterChooseDialog();
if (newTargetTypeChoiceDialog.getChoices().size() < 2) {
// no more unused target types - select the last one
currentNewTargetType = newTargetTypeChoiceDialog.getChoices().stream().map(newTargetTypes::get).iterator();
return next();
}
choice.clearChoice();
player.choose(Outcome.AIDontUseIt, choice, game);
String chosen = choice.getChoice();
// choose next target type for usage
newTargetTypeChoiceDialog.clearChoice();
player.choose(Outcome.AIDontUseIt, newTargetTypeChoiceDialog, game);
String chosen = newTargetTypeChoiceDialog.getChoice();
if (chosen == null) {
iterator = choice.getChoices().stream().map(predicateMap::get).iterator();
currentNewTargetType = newTargetTypeChoiceDialog.getChoices().stream().map(newTargetTypes::get).iterator();
return next();
}
choice.getChoices().remove(chosen);
return predicateMap.get(chosen);
newTargetTypeChoiceDialog.getChoices().remove(chosen);
return newTargetTypes.get(chosen);
}
}
@ -132,9 +158,9 @@ public abstract class StackObjectImpl implements StackObject {
if (game.replaceEvent(gameEvent)) {
return;
}
Iterator<MageObjectReferencePredicate> predicates = new PredicateIterator(game, newControllerId, gameEvent.getAmount(), applier);
Iterator<MageObjectReferencePredicate> newTargetTypeIterator = new NewTargetTypeIterator(game, newControllerId, gameEvent.getAmount(), applier);
for (int i = 0; i < gameEvent.getAmount(); i++) {
createSingleCopy(newControllerId, applier, predicates.next(), game, source, chooseNewTargets);
createSingleCopy(newControllerId, applier, newTargetTypeIterator.next(), game, source, chooseNewTargets);
}
Player player = game.getPlayer(newControllerId);
if (player == null) {
@ -210,18 +236,17 @@ public abstract class StackObjectImpl implements StackObject {
* targets will be, the copy is put onto the stack with those targets.
*
* @param game
* @param targetControllerId - player that can/has to change the target of
* the spell
* @param forceChange - does only work for targets with maximum of one
* targetId
* @param onlyOneTarget - 114.6b one target must be changed to another
* target
* @param extraPredicate restriction for the new target, if null nothing is
* cheched
* @param targetControllerId - player that can/has to change the target of
* the spell
* @param forceChange - does only work for targets with maximum of one
* targetId
* @param onlyOneTarget - 114.6b one target must be changed to another
* target
* @param newTargetFilterPredicate restriction for the new target (null - can select same targets)
* @return
*/
@Override
public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, Predicate<MageItem> extraPredicate) {
public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, Predicate<MageItem> newTargetFilterPredicate) {
Player targetController = game.getPlayer(targetControllerId);
if (targetController != null) {
StringBuilder oldTargetDescription = new StringBuilder();
@ -240,7 +265,7 @@ public abstract class StackObjectImpl implements StackObject {
ability.getModes().setActiveMode(mode);
oldTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game));
for (Target target : mode.getTargets()) {
Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, extraPredicate, game);
Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, newTargetFilterPredicate, game);
// clear the old target and copy all targets from new target
target.clearChosen();
for (UUID targetId : newTarget.getTargets()) {
@ -263,21 +288,25 @@ public abstract class StackObjectImpl implements StackObject {
/**
* Handles the change of one target instance of a mode
*
* @param targetController - player that can choose the new target
* @param targetController - player that can choose the new target
* @param ability
* @param mode
* @param target
* @param forceChange
* @param newTargetFilterPredicate
* @param game
* @return
*/
private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, Predicate predicate, Game game) {
private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, Predicate newTargetFilterPredicate, Game game) {
Target newTarget = target.copy();
if (predicate != null) {
newTarget.getFilter().add(predicate);
// filter targets
if (newTargetFilterPredicate != null) {
newTarget.getFilter().add(newTargetFilterPredicate);
// If adding a predicate, there will only be one choice and therefore target can be automatic
newTarget.setRandom(true);
}
newTarget.setEventReporting(false);
if (!targetController.getId().equals(getControllerId())) {
newTarget.setTargetController(targetController.getId()); // target controller for the change is different from spell controller
@ -318,8 +347,8 @@ public abstract class StackObjectImpl implements StackObject {
} else {
// build a target definition with exactly one possible target to select that replaces old target
Target tempTarget = target.copy();
if (predicate != null) {
tempTarget.getFilter().add(predicate);
if (newTargetFilterPredicate != null) {
tempTarget.getFilter().add(newTargetFilterPredicate);
// If adding a predicate, there will only be one choice and therefore target can be automatic
tempTarget.setRandom(true);
}

View file

@ -13,5 +13,12 @@ public interface StackObjectCopyApplier extends Serializable {
void modifySpell(StackObject stackObject, Game game);
MageObjectReferencePredicate getNextPredicate();
/**
* For multi copies: allows change new target filter for each next copy (e.g. add some restict)
* Return null to use same target type as original spell
*
* @param copyNumber current number of copy, starts with 1
* @return
*/
MageObjectReferencePredicate getNextNewTargetType(int copyNumber);
}