forked from External/mage
Merge pull request 'master' (#10) from External/mage:master into master
All checks were successful
/ example-docker-compose (push) Successful in 19m29s
All checks were successful
/ example-docker-compose (push) Successful in 19m29s
Reviewed-on: #10
This commit is contained in:
commit
e69dd75c8f
15 changed files with 319 additions and 41 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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???
|
||||
}
|
||||
|
|
@ -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 */);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue