Merge pull request 'master' (#10) from External/mage:master into master
All checks were successful
/ example-docker-compose (push) Successful in 19m29s

Reviewed-on: #10
This commit is contained in:
Failure 2024-12-27 16:18:24 -08:00
commit e69dd75c8f
15 changed files with 319 additions and 41 deletions

View file

@ -898,7 +898,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
return;
}
CombatUtil.sortByPower(attackers, false);
CombatUtil.sortByPower(attackers, false); // most powerfull go to first
CombatInfo combatInfo = CombatUtil.blockWithGoodTrade2(game, attackers, possibleBlockers);
Player player = game.getPlayer(playerId);
@ -909,6 +909,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
List<Permanent> blockers = entry.getValue();
if (blockers != null) {
for (Permanent blocker : blockers) {
// TODO: buggy or miss on multi blocker requirements?!
player.declareBlocker(player.getId(), blocker.getId(), attackerId, game);
blocked = true;
}

View file

@ -40,7 +40,7 @@ public final class CombatUtil {
}
}
sortByPower(blockableAttackers, true);
sortByPower(blockableAttackers, false); // most powerfull go to first
// imagine that most powerful will be blocked as 1-vs-1
List<Permanent> attackersThatWontBeBlocked = new ArrayList<>(blockableAttackers);
@ -83,28 +83,17 @@ public final class CombatUtil {
}
public static void sortByPower(List<Permanent> permanents, final boolean ascending) {
Collections.sort(permanents, new Comparator<Permanent>() {
@Override
public int compare(Permanent o1, Permanent o2) {
if (ascending) {
return o2.getPower().getValue() - o1.getPower().getValue();
} else {
return o1.getPower().getValue() - o2.getPower().getValue();
}
}
});
permanents.sort(Comparator.comparingInt(p -> p.getPower().getValue()));
if (!ascending) {
Collections.reverse(permanents);
}
}
public static Permanent getWorstCreature(List<Permanent> creatures) {
if (creatures.isEmpty()) {
return null;
}
Collections.sort(creatures, new Comparator<Permanent>() {
@Override
public int compare(Permanent o1, Permanent o2) {
return o2.getPower().getValue() - o1.getPower().getValue();
}
});
creatures.sort(Comparator.comparingInt(p -> p.getPower().getValue()));
return creatures.get(0);
}

View file

@ -70,7 +70,7 @@ enum BeckoningWillOWispPredicate implements ObjectSourcePlayerPredicate<Permanen
@Override
public boolean apply(ObjectSourcePlayer<Permanent> input, Game game) {
UUID playerId = (UUID) game.getState().getValue(input.getSourceId() + "_" + game.getState().getZoneChangeCounter(input.getSourceId()) + "_chosenOpponent");
return playerId != null && playerId.equals(game.getCombat().getDefendingPlayerId(input.getObject().getId(), game));
return playerId != null && playerId.equals(game.getCombat().getDefendingPlayerId(input.getObject().getId(), game, false));
}
}

View file

@ -1,7 +1,6 @@
package mage.cards.d;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
@ -16,11 +15,7 @@ import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect;
import mage.abilities.effects.common.search.SearchLibraryPutInPlayEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.ComparisonType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.constants.*;
import mage.filter.common.FilterPermanentCard;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game;
@ -29,14 +24,18 @@ import mage.game.permanent.Permanent;
import mage.target.common.TargetCardInLibrary;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
*
* @author bunchOfDevs
*/
public final class DefiantVanguard extends CardImpl {
protected static final String EFFECT_KEY = "DefiantVanguardEffect_";
private static final FilterPermanentCard filter = new FilterPermanentCard("Rebel permanent card with mana value 4 or less");
static {
filter.add(SubType.REBEL.getPredicate());
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 5));
@ -100,8 +99,8 @@ class DefiantVanguardTriggeredAbility extends TriggeredAbilityImpl {
Permanent blocked = game.getPermanent(event.getTargetId());
if (blocker != null
&& blocked != null) {
game.getState().setValue(blocked.toString(), blocked.getZoneChangeCounter(game)); // in case the attacker changes zone
game.getState().setValue(blocker.toString(), blocker.getZoneChangeCounter(game)); // in case the blocker changes zone
game.getState().setValue(DefiantVanguard.EFFECT_KEY + blocked.getId(), blocked.getZoneChangeCounter(game)); // in case the attacker changes zone
game.getState().setValue(DefiantVanguard.EFFECT_KEY + blocker.getId(), blocker.getZoneChangeCounter(game)); // in case the blocker changes zone
getAllEffects().setTargetPointer(new FixedTarget(blocked.getId()));
return true;
}
@ -131,13 +130,13 @@ class DefiantVanguardEffect extends OneShotEffect {
Permanent blockedCreature = game.getPermanent(getTargetPointer().getFirst(game, source));
Permanent defiantVanguard = game.getPermanent(source.getSourceId());
if (blockedCreature != null) {
if (game.getState().getValue(blockedCreature.toString()).equals(blockedCreature.getZoneChangeCounter(game))) { // true if it did not change zones
if (game.getState().getValue(DefiantVanguard.EFFECT_KEY + blockedCreature.getId()).equals(blockedCreature.getZoneChangeCounter(game))) { // true if it did not change zones
blockedCreature.destroy(source, game, false);
result = true;
}
}
if (defiantVanguard != null) {
if (game.getState().getValue(defiantVanguard.toString()).equals(defiantVanguard.getZoneChangeCounter(game))) { // true if it did not change zones
if (game.getState().getValue(DefiantVanguard.EFFECT_KEY + defiantVanguard.getId()).equals(defiantVanguard.getZoneChangeCounter(game))) { // true if it did not change zones
defiantVanguard.destroy(source, game, false);
result = true;
}

View file

@ -21,7 +21,6 @@ import mage.constants.SubType;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent;
/**
*

View file

@ -83,7 +83,7 @@ class SharaeOfNumbingDepthsTriggeredAbility extends TriggeredAbilityImpl {
public boolean checkTrigger(GameEvent event, Game game) {
Permanent permanent = game.getPermanent(event.getTargetId());
return permanent != null
&& StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE.match(permanent, game)
&& isControlledBy(event.getPlayerId());
&& isControlledBy(event.getPlayerId()) // whenever you tap
&& StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE.match(permanent, this.getControllerId(), this, game);
}
}
}

View file

@ -18,7 +18,6 @@ import mage.filter.predicate.mageobject.ToughnessPredicate;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.target.common.TargetCreaturePermanent;
/**

View file

@ -19,7 +19,6 @@ import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import mage.players.Player;
@ -113,7 +112,7 @@ class UlamogAttackTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Permanent sourcePermanent = game.getPermanent(this.getSourceId());
Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(this.getSourceId());
if (sourcePermanent != null
&& event.getSourceId() != null
&& event.getSourceId().equals(this.getSourceId())) {

View file

@ -13,7 +13,6 @@ import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
@ -73,7 +72,7 @@ class WardscaleDragonRuleEffect extends ContinuousRuleModifyingEffectImpl {
public boolean applies(GameEvent event, Ability source, Game game) {
Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (sourcePermanent != null && sourcePermanent.isAttacking()) {
return event.getPlayerId().equals(game.getCombat().getDefendingPlayerId(sourcePermanent.getId(), game));
return event.getPlayerId().equals(game.getCombat().getDefendingPlayerId(sourcePermanent.getId(), game, false));
}
return false;
}

View file

@ -98,6 +98,9 @@ class ZurgoAndOjutaiTriggeredAbility extends TriggeredAbilityImpl implements Bat
@Override
public boolean checkEvent(DamagedEvent event, Game game) {
if (!event.isCombatDamage()) {
return false;
}
Permanent permanent = game.getPermanent(event.getSourceId());
Permanent defender = game.getPermanent(event.getTargetId());
return permanent != null

View file

@ -0,0 +1,197 @@
package org.mage.test.AI.basic;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
/**
* @author JayDi85
*/
public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
@Test
public void test_Block_1_small_attacker_vs_1_big_blocker() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2
attack(1, playerA, "Arbor Elf");
// ai must block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Arbor Elf", 1);
}
@Test
public void test_Block_1_small_attacker_vs_2_big_blockers() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
attack(1, playerA, "Arbor Elf");
// ai must block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Arbor Elf", 1);
}
@Test
public void test_Block_1_small_attacker_vs_1_small_blocker() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
attack(1, playerA, "Arbor Elf");
// ai must block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Arbor Elf", 1);
assertGraveyardCount(playerB, "Arbor Elf", 1);
}
@Test
public void test_Block_1_big_attacker_vs_1_small_blocker() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
attack(1, playerA, "Balduvian Bears");
// ai must not block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20 - 2);
assertGraveyardCount(playerA, "Balduvian Bears", 0);
assertGraveyardCount(playerB, "Arbor Elf", 0);
}
@Test
public void test_Block_2_big_attackers_vs_1_small_blocker() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Deadbridge Goliath", 1); // 5/5
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
attack(1, playerA, "Balduvian Bears");
attack(1, playerA, "Deadbridge Goliath");
// ai must not block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20 - 2 - 5);
assertGraveyardCount(playerA, "Balduvian Bears", 0);
assertGraveyardCount(playerA, "Deadbridge Goliath", 0);
assertGraveyardCount(playerB, "Arbor Elf", 0);
}
@Test
public void test_Block_2_big_attackers_vs_1_big_blocker_a() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Deadbridge Goliath", 1); // 5/5
addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6
attack(1, playerA, "Balduvian Bears");
attack(1, playerA, "Deadbridge Goliath");
// ai must block bigger attacker and survive (6/6 must block 5/5)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20 - 2);
assertGraveyardCount(playerA, "Balduvian Bears", 0);
assertGraveyardCount(playerA, "Deadbridge Goliath", 1);
assertGraveyardCount(playerB, "Colossal Dreadmaw", 0);
}
@Test
public void test_Block_2_big_attackers_vs_1_big_blocker_b() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Deadbridge Goliath", 1); // 5/5
addCard(Zone.BATTLEFIELD, playerA, "Colossal Dreadmaw", 1); // 6/6
//
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
attack(1, playerA, "Arbor Elf");
attack(1, playerA, "Balduvian Bears");
attack(1, playerA, "Deadbridge Goliath");
attack(1, playerA, "Colossal Dreadmaw");
// ai must block bigger attacker and survive (3/3 must block 2/2)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20 - 1 - 5 - 6);
assertGraveyardCount(playerA, "Balduvian Bears", 1);
assertPermanentCount(playerB, "Spectral Bears", 1);
}
@Test
public void test_Block_1_attacker_vs_many_blockers() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
//
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6
attack(1, playerA, "Balduvian Bears");
// ai must use smaller blocker and survive (3/3 must block 2/2)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Balduvian Bears", 1);
assertDamageReceived(playerB, "Spectral Bears", 2);
}
// TODO: add tests with multi blocker requirement effects
// TODO: add tests for DeathtouchAbility
// TODO: add tests for FirstStrikeAbility
// TODO: add tests for DoubleStrikeAbility
// TODO: add tests for IndestructibleAbility
// TODO: add tests for FlyingAbility
// TODO: add tests for ReachAbility
// TODO: add tests for ExaltedAbility???
}

View file

@ -106,4 +106,30 @@ public class RemoveFromCombatTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 2);
assertGraveyardCount(playerB, "Jace, Memory Adept", 1);
}
/**
* Validate rule 806.2a: Abilities which refer to Defending Player still mean that defending player, even if the
* attacking creature is removed from combat.
*/
@Test
public void test_RemoveAttackerWithDefendingPlayerTriggeredAbilityOnStack() {
addCard(Zone.HAND, playerA, "Swords to Plowshares", 1);
addCard(Zone.BATTLEFIELD, playerA, "Agate-Blade Assassin", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
// attack player
attack(1, playerA, "Agate-Blade Assassin", playerB);
// remove Agate-Blade Assassin from combat
castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerA, "Swords to Plowshares");
addTarget(playerA, "Agate-Blade Assassin");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 1);
assertLife(playerA, 20 + 1 /* StP */ + 1 /* Agate-Blade Assassin trigger */);
}
}

View file

@ -39,6 +39,7 @@ import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author BetaSteward_at_googlemail.com
@ -59,6 +60,7 @@ public class Combat implements Serializable, Copyable<Combat> {
private final List<FilterCreaturePermanent> useToughnessForDamageFilters = new ArrayList<>();
protected List<CombatGroup> groups = new ArrayList<>();
protected List<CombatGroup> formerGroups = new ArrayList<>();
protected Map<UUID, CombatGroup> blockingGroups = new HashMap<>();
// all possible defenders (players, planeswalkers or battle)
protected Set<UUID> defenders = new HashSet<>();
@ -83,6 +85,9 @@ public class Combat implements Serializable, Copyable<Combat> {
for (CombatGroup group : combat.groups) {
groups.add(group.copy());
}
for (CombatGroup group : combat.formerGroups) {
formerGroups.add(group.copy());
}
defenders.addAll(combat.defenders);
for (Map.Entry<UUID, CombatGroup> group : combat.blockingGroups.entrySet()) {
blockingGroups.put(group.getKey(), group.getValue());
@ -181,6 +186,7 @@ public class Combat implements Serializable, Copyable<Combat> {
public void clear() {
groups.clear();
formerGroups.clear();
blockingGroups.clear();
defenders.clear();
attackingPlayerId = null;
@ -858,6 +864,7 @@ public class Combat implements Serializable, Copyable<Combat> {
// map with attackers (UUID) that must be blocked by at least one blocker and a set of all creatures that can block it and don't block yet
Map<UUID, Set<UUID>> mustBeBlockedByAtLeastX = new HashMap<>();
Map<UUID, Integer> minNumberOfBlockersMap = new HashMap<>();
Map<UUID, Integer> minPossibleBlockersMap = new HashMap<>();
// check mustBlock requirements of creatures from opponents of attacking player
for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, player.getId(), game)) {
@ -876,6 +883,12 @@ public class Combat implements Serializable, Copyable<Combat> {
CombatGroup toBeBlockedGroup = findGroup(toBeBlockedCreature);
if (toBeBlockedGroup != null && toBeBlockedGroup.getDefendingPlayerId().equals(creature.getControllerId())) {
minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers());
Permanent toBeBlockedCreaturePermanent = game.getPermanent(toBeBlockedCreature);
if (toBeBlockedCreaturePermanent != null) {
minPossibleBlockersMap.put(toBeBlockedCreature, toBeBlockedCreaturePermanent.getMinBlockedBy());
} else {
minPossibleBlockersMap.put(toBeBlockedCreature, 1);
}
Set<UUID> potentialBlockers;
if (mustBeBlockedByAtLeastX.containsKey(toBeBlockedCreature)) {
potentialBlockers = mustBeBlockedByAtLeastX.get(toBeBlockedCreature);
@ -973,6 +986,12 @@ public class Combat implements Serializable, Copyable<Combat> {
CombatGroup toBeBlockedGroup = findGroup(toBeBlockedCreature);
if (toBeBlockedGroup != null && toBeBlockedGroup.getDefendingPlayerId().equals(creature.getControllerId())) {
minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers());
Permanent toBeBlockedCreaturePermanent = game.getPermanent(toBeBlockedCreature);
if (toBeBlockedCreaturePermanent != null) {
minPossibleBlockersMap.put(toBeBlockedCreature, toBeBlockedCreaturePermanent.getMinBlockedBy());
} else {
minPossibleBlockersMap.put(toBeBlockedCreature, 1);
}
Set<UUID> potentialBlockers;
if (mustBeBlockedByAtLeastX.containsKey(toBeBlockedCreature)) {
potentialBlockers = mustBeBlockedByAtLeastX.get(toBeBlockedCreature);
@ -1059,6 +1078,13 @@ public class Combat implements Serializable, Copyable<Combat> {
for (UUID toBeBlockedCreatureId : mustBeBlockedByAtLeastX.keySet()) {
for (CombatGroup combatGroup : game.getCombat().getGroups()) {
if (combatGroup.getAttackers().contains(toBeBlockedCreatureId)) {
// Neyith of the Dire Hunt: If the target creature has menace, two creatures must block it if able.
// (2020-06-23)
// This is a basic check to avoid deadlocking on one blocker plus 'must be blocked if able' with menace;
// a full solution is more complicated but this prevents the most common case.
if (mustBeBlockedByAtLeastX.get(toBeBlockedCreatureId).size() < minPossibleBlockersMap.get(toBeBlockedCreatureId)) {
continue;
}
boolean requirementFulfilled = false;
// Check whether an applicable creature is blocking.
for (UUID blockerId : combatGroup.getBlockers()) {
@ -1659,6 +1685,36 @@ public class Combat implements Serializable, Copyable<Combat> {
* @return
*/
public UUID getDefendingPlayerId(UUID attackingCreatureId, Game game) {
return getDefendingPlayerId(attackingCreatureId, game, true);
}
/**
* Returns the playerId of the player that is attacked by given attacking
* creature or formerly-attacking creature.
*
* @param attackingCreatureId
* @param game
* @return
*/
public UUID getDefendingPlayerId(UUID attackingCreatureId, Game game, boolean allowFormer) {
if (allowFormer) {
/*
* 802.2a. Any rule, object, or effect that refers to a "defending player" refers to one specific defending
* player, not to all of the defending players. If an ability of an attacking creature refers to a
* defending player, or a spell or ability refers to both an attacking creature and a defending player,
* then unless otherwise specified, the defending player it's referring to is the player that creature is
* attacking, the controller of the planeswalker that creature is attacking, or the protector of the battle
* that player is attacking. If that creature is no longer attacking, the defending player it's referring
* to is the player that creature was attacking before it was removed from combat, the controller of the
* planeswalker that creature was attacking before it was removed from combat, or the protector of the
* battle that player was attacking before it was removed from combat.
*/
return Stream.concat(groups.stream(), formerGroups.stream())
.filter(group -> (group.getAttackers().contains(attackingCreatureId) || group.getFormerAttackers().contains(attackingCreatureId)))
.map(CombatGroup::getDefendingPlayerId)
.findFirst()
.orElse(null);
}
return groups
.stream()
.filter(group -> group.getAttackers().contains(attackingCreatureId))
@ -1723,6 +1779,7 @@ public class Combat implements Serializable, Copyable<Combat> {
}
}
if (group.attackers.isEmpty()) {
formerGroups.add(group);
groups.remove(group);
}
return;

View file

@ -27,6 +27,7 @@ import java.util.stream.Stream;
public class CombatGroup implements Serializable, Copyable<CombatGroup> {
protected List<UUID> attackers = new ArrayList<>();
protected List<UUID> formerAttackers = new ArrayList<>();
protected List<UUID> blockers = new ArrayList<>();
protected List<UUID> blockerOrder = new ArrayList<>();
protected List<UUID> attackerOrder = new ArrayList<>();
@ -49,6 +50,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
protected CombatGroup(final CombatGroup group) {
this.attackers.addAll(group.attackers);
this.formerAttackers.addAll(group.formerAttackers);
this.blockers.addAll(group.blockers);
this.blockerOrder.addAll(group.blockerOrder);
this.attackerOrder.addAll(group.attackerOrder);
@ -81,6 +83,10 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
return attackers;
}
public List<UUID> getFormerAttackers() {
return formerAttackers;
}
public List<UUID> getBlockers() {
return blockers;
}
@ -737,6 +743,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
public boolean remove(UUID creatureId) {
boolean result = false;
if (attackers.contains(creatureId)) {
formerAttackers.add(creatureId);
attackers.remove(creatureId);
result = true;
attackerOrder.remove(creatureId);

View file

@ -21,7 +21,7 @@ import mage.constants.*;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.counters.Counters;
import mage.filter.*;
import mage.filter.FilterOpponent;
import mage.game.Game;
import mage.game.GameState;
import mage.game.ZoneChangeInfo;
@ -210,8 +210,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
+ ", " + getBasicMageObject().getClass().getSimpleName()
+ ", " + imageInfo
+ ", " + this.getPower() + "/" + this.getToughness()
+ (this.getDamage() > 0 ? ", damage " + this.getDamage() : "")
+ (this.isCopy() ? ", copy" : "")
+ (this.isTapped() ? ", tapped" : "");
+ (this.isTapped() ? ", tapped" : "")
+ (this.isAttacking() ? ", attacking" : "")
+ (this.getBlocking() > 0 ? ", blocking" : "");
}
@Override