FDN New Combat Rules (#13279)

* Remove all combat ordering code

* Use MultiAmount division for damage

* Remove damage selection division skipping in tests

* Fix Banding, Multi-block, add tests

* Fix test

* Fix random iteration order, fix new tests

* Add more info to choose dialog, make MultiAmountType class instead of enum

* Don't prompt for trample damage assignment if none possible

* Mark "Assign default damage" on tests, minor other test improvements
This commit is contained in:
ssk97 2025-03-30 14:42:05 -07:00 committed by GitHub
parent f53e43fd46
commit 969ffa1c98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 323 additions and 503 deletions

View file

@ -40,7 +40,7 @@ public class CombatGroupView implements Serializable {
attackers.put(id, new PermanentView(attacker, game.getCard(attacker.getId()),null, game));
}
}
for (UUID id: combatGroup.getBlockerOrder()) {
for (UUID id: combatGroup.getBlockers()) {
Permanent blocker = game.getPermanent(id);
if (blocker != null) {
blockers.put(id, new PermanentView(blocker, game.getCard(blocker.getId()), null, game));

View file

@ -13,10 +13,8 @@ import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.constants.RangeOfInfluence;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.tournament.Tournament;
import mage.players.Player;
import mage.target.Target;
@ -287,24 +285,6 @@ public class ComputerPlayerControllableProxy extends ComputerPlayer7 {
}
}
@Override
public UUID chooseAttackerOrder(java.util.List<Permanent> attackers, Game game) {
if (isUnderMe(game)) {
return super.chooseAttackerOrder(attackers, game);
} else {
return getControllingPlayer(game).chooseAttackerOrder(attackers, game);
}
}
@Override
public UUID chooseBlockerOrder(java.util.List<Permanent> blockers, CombatGroup combatGroup, java.util.List<UUID> blockerOrder, Game game) {
if (isUnderMe(game)) {
return super.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
} else {
return getControllingPlayer(game).chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
}
@Override
public int getAmount(int min, int max, String message, Game game) {
if (isUnderMe(game)) {

View file

@ -2177,9 +2177,6 @@ public class ComputerPlayer extends PlayerImpl {
// TODO: add AI support with outcome and replace random with min/max
public int getAmount(int min, int max, String message, Game game) {
log.debug("getAmount");
if (message.startsWith("Assign damage to ")) {
return min;
}
if (min < max && min == 0) {
return RandomUtil.nextInt(CardUtil.overflowInc(max, 1));
}
@ -2192,7 +2189,7 @@ public class ComputerPlayer extends PlayerImpl {
log.debug("getMultiAmount");
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, totalMin, totalMax);
List<Integer> defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0) {
return defaultList;
}
@ -2210,18 +2207,6 @@ public class ComputerPlayer extends PlayerImpl {
return MultiAmountType.prepareMaxValues(messages, totalMin, totalMax);
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attackers, Game game) {
//TODO: improve this
return attackers.iterator().next().getId();
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game) {
//TODO: improve this
return blockers.iterator().next().getId();
}
@Override
public List<MageObject> getAvailableManaProducers(Game game) {
return super.getAvailableManaProducers(game);

View file

@ -374,22 +374,6 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
return super.chooseMode(modes, source, game);
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attackers, Game game) {
if (this.isHuman()) {
return attackers.get(RandomUtil.nextInt(attackers.size())).getId();
}
return super.chooseAttackerOrder(attackers, game);
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game) {
if (this.isHuman()) {
return blockers.get(RandomUtil.nextInt(blockers.size())).getId();
}
return super.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
@Override
public int getAmount(int min, int max, String message, Game game) {
if (this.isHuman()) {

View file

@ -2122,56 +2122,6 @@ public class HumanPlayer extends PlayerImpl {
}
}
@Override
public UUID chooseAttackerOrder(java.util.List<Permanent> attackers, Game game) {
if (gameInCheckPlayableState(game)) {
return null;
}
while (canRespond()) {
prepareForResponse(game);
if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, "Pick attacker", attackers, true);
}
waitForResponse(game);
UUID responseId = getFixedResponseUUID(game);
if (responseId != null) {
for (Permanent perm : attackers) {
if (perm.getId().equals(responseId)) {
return perm.getId();
}
}
}
}
return null;
}
@Override
public UUID chooseBlockerOrder(java.util.List<Permanent> blockers, CombatGroup combatGroup, java.util.List<UUID> blockerOrder, Game game) {
if (gameInCheckPlayableState(game)) {
return null;
}
while (canRespond()) {
prepareForResponse(game);
if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, "Pick blocker", blockers, true);
}
waitForResponse(game);
UUID responseId = getFixedResponseUUID(game);
if (responseId != null) {
for (Permanent perm : blockers) {
if (perm.getId().equals(responseId)) {
return perm.getId();
}
}
}
}
return null;
}
protected void selectCombatGroup(UUID defenderId, UUID blockerId, Game game) {
if (gameInCheckPlayableState(game)) {
return;
@ -2260,7 +2210,7 @@ public class HumanPlayer extends PlayerImpl {
Game game
) {
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, totalMin, totalMax);
List<Integer> defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0 || (needCount == 1 && totalMin == totalMax)
|| messages.stream().map(m -> m.min == m.max).reduce(true, Boolean::logicalAnd)) {
// nothing to choose

View file

@ -153,10 +153,6 @@ class BalduvianWarlordUnblockEffect extends OneShotEffect {
game.fireEvent(new BlockerDeclaredEvent(chosenPermanent.getId(), permanent.getId(), permanent.getControllerId()));
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.CREATURE_BLOCKS, permanent.getId(), source, null));
}
CombatGroup blockGroup = findBlockingGroup(permanent, game); // a new blockingGroup is formed, so it's necessary to find it again
if (blockGroup != null) {
blockGroup.pickAttackerOrder(permanent.getControllerId(), game);
}
}
}
return true;
@ -164,15 +160,4 @@ class BalduvianWarlordUnblockEffect extends OneShotEffect {
}
return false;
}
private CombatGroup findBlockingGroup(Permanent blocker, Game game) {
if (game.getCombat().blockingGroupsContains(blocker.getId())) { // if (blocker.getBlocking() > 1) {
for (CombatGroup group : game.getCombat().getBlockingGroups()) {
if (group.getBlockers().contains(blocker.getId())) {
return group;
}
}
}
return null;
}
}

View file

@ -1,7 +1,6 @@
package mage.cards.b;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
@ -22,6 +21,8 @@ import mage.game.permanent.token.CatSoldierCreatureToken;
import mage.game.permanent.token.Token;
import mage.players.Player;
import java.util.UUID;
/**
*
* @author LevelX2
@ -99,7 +100,6 @@ class BrimazKingOfOreskosEffect extends OneShotEffect {
combatGroup.addBlocker(tokenId, source.getControllerId(), game);
game.getCombat().addBlockingGroup(tokenId, attackingCreature.getId(), controller.getId(), game);
}
combatGroup.pickBlockerOrder(attackingCreature.getControllerId(), game);
return true;
}

View file

@ -169,21 +169,6 @@ class FalseOrdersUnblockEffect extends OneShotEffect {
game.fireEvent(new BlockerDeclaredEvent(chosenPermanent.getId(), permanent.getId(), permanent.getControllerId()));
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.CREATURE_BLOCKS, permanent.getId(), source, null));
}
CombatGroup blockGroup = findBlockingGroup(permanent, game); // a new blockingGroup is formed, so it's necessary to find it again
if (blockGroup != null) {
blockGroup.pickAttackerOrder(permanent.getControllerId(), game);
}
return true;
}
private CombatGroup findBlockingGroup(Permanent blocker, Game game) {
if (game.getCombat().blockingGroupsContains(blocker.getId())) { // if (blocker.getBlocking() > 1) {
for (CombatGroup group : game.getCombat().getBlockingGroups()) {
if (group.getBlockers().contains(blocker.getId())) {
return group;
}
}
}
return null;
}
}

View file

@ -90,7 +90,6 @@ class FlashFoliageEffect extends OneShotEffect {
game.getCombat().addBlockingGroup(tokenId, attackingCreature.getId(), controller.getId(), game);
}
}
combatGroup.pickBlockerOrder(attackingCreature.getControllerId(), game);
}
}
return true;

View file

@ -1,10 +1,6 @@
package mage.cards.g;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.condition.common.IsStepCondition;
@ -13,18 +9,18 @@ import mage.abilities.decorator.ConditionalActivatedAbility;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.constants.*;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetAttackingCreature;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
*
* @author L_J
@ -138,11 +134,9 @@ class GeneralJarkeldSwitchBlockersEffect extends OneShotEffect {
// the ability doesn't unblock a group that loses all blockers, however it will newly block a previously unblocked group if it gains a blocker this way
if (!(chosenGroup1.getBlockers().isEmpty())) {
chosenGroup1.setBlocked(true, game);
chosenGroup1.pickBlockerOrder(attacker1.getControllerId(), game);
}
if (!(chosenGroup2.getBlockers().isEmpty())) {
chosenGroup2.setBlocked(true, game);
chosenGroup2.pickBlockerOrder(attacker2.getControllerId(), game);
}
return true;
}
@ -197,7 +191,6 @@ class GeneralJarkeldSwitchBlockersEffect extends OneShotEffect {
// 10/4/2004 The new blocker does not trigger any abilities which trigger on creatures becoming blockers, because the creatures were already blockers and the simple change of who is blocking does not trigger such abilities.
game.getCombat().addBlockingGroup(blocker.getId(), attacker, controller.getId(), game);
}
blockGroup.pickAttackerOrder(blocker.getControllerId(), game);
}
}
}

View file

@ -1,13 +1,12 @@
package mage.cards.m;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.CastOnlyDuringPhaseStepSourceAbility;
import mage.abilities.common.delayed.AtTheEndOfCombatDelayedTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.abilities.effects.common.CreateTokenCopyTargetEffect;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -20,6 +19,8 @@ import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.target.targetpointer.FixedTargets;
import java.util.UUID;
/**
*
* @author LevelX2
@ -75,20 +76,15 @@ class MirrorMatchEffect extends OneShotEffect {
effect.setTargetPointer(new FixedTarget(attacker, game));
effect.apply(game, source);
CombatGroup group = game.getCombat().findGroup(attacker.getId());
boolean isCreature = false;
if (group != null) {
for (Permanent addedToken : effect.getAddedPermanents()) {
if (addedToken.isCreature(game)) {
group.addBlockerToGroup(addedToken.getId(), attackerId, game);
isCreature = true;
}
}
ExileTargetEffect exileEffect = new ExileTargetEffect("Exile those tokens at end of combat");
exileEffect.setTargetPointer(new FixedTargets(effect.getAddedPermanents(), game));
game.addDelayedTriggeredAbility(new AtTheEndOfCombatDelayedTriggeredAbility(exileEffect), source);
if (isCreature) {
group.pickBlockerOrder(attacker.getControllerId(), game);
}
}
}
}

View file

@ -176,14 +176,8 @@ class SorrowsPathSwitchBlockersEffect extends OneShotEffect {
group.addBlockerToGroup(blocker.getId(), blocker.getControllerId(), game);
game.getCombat().addBlockingGroup(blocker.getId(), attacker.getId(), blocker.getControllerId(), game);
game.fireEvent(new BlockerDeclaredEvent(attacker.getId(), blocker.getId(), blocker.getControllerId()));
group.pickBlockerOrder(attacker.getControllerId(), game);
}
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.CREATURE_BLOCKS, blocker.getId(), source, null));
CombatGroup blockGroup = findBlockingGroup(blocker, game); // a new blockingGroup is formed, so it's necessary to find it again
if (blockGroup != null) {
blockGroup.pickAttackerOrder(blocker.getControllerId(), game);
}
}
}

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class AfflictTest extends CardTestPlayerBase {
private final String khenra = "Khenra Eternal";
@ -35,7 +37,7 @@ public class AfflictTest extends CardTestPlayerBase {
attack(1, playerA, khenra);
block(1, playerB, elves + ":0", khenra);
block(1, playerB, elves + ":1", khenra);
setChoice(playerA, "X=1"); // assign damage
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -0,0 +1,104 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author notgreat
*/
public class BandingTest extends CardTestPlayerBase {
@Test
public void BandingAttackSimple() {
addCard(Zone.BATTLEFIELD, playerA, "Squire"); // 1/2
addCard(Zone.BATTLEFIELD, playerA, "Benalish Infantry"); // Banding 1/3
addCard(Zone.BATTLEFIELD, playerA, "Eager Cadet"); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Naga Eternal"); // 3/2
attack(1, playerA, "Squire");
attack(1, playerA, "Benalish Infantry");
attack(1, playerA, "Eager Cadet");
setChoice(playerA, true);
setChoice(playerA, "Squire");
block(1, playerB, "Naga Eternal", "Squire");
setChoiceAmount(playerA, 1, 2); //1 to Squire, 2 to Infantry, attacking player chooses
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, 0);
assertDamageReceived(playerA, "Squire", 1);
assertDamageReceived(playerA, "Benalish Infantry", 2);
assertGraveyardCount(playerB, 1);
assertLife(playerB, 19); // Only Eager Cadet gets through
}
@Test
public void BandingBlockSimple() {
addCard(Zone.BATTLEFIELD, playerA, "Alpine Grizzly"); // 4/2
addCard(Zone.BATTLEFIELD, playerB, "Squire"); // 1/2
addCard(Zone.BATTLEFIELD, playerB, "Sanctuary Cat"); // 1/2
addCard(Zone.BATTLEFIELD, playerB, "Benalish Infantry"); // Banding 1/3
attack(1, playerA, "Alpine Grizzly");
block(1, playerB, "Squire", "Alpine Grizzly");
block(1, playerB, "Sanctuary Cat", "Alpine Grizzly");
block(1, playerB, "Benalish Infantry", "Alpine Grizzly");
setChoiceAmount(playerB, 1, 1, 2); //1 to Squire, 1 to Cat, 2 to Infantry, defending player chooses
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, 1);
assertGraveyardCount(playerB, 0);
assertDamageReceived(playerB, "Squire", 1);
assertDamageReceived(playerB, "Sanctuary Cat", 1);
assertDamageReceived(playerB, "Benalish Infantry", 2);
assertLife(playerB, 20);
}
@Test
public void DoubleBanding() {
addCard(Zone.BATTLEFIELD, playerA, "Benalish Infantry"); // Banding 1/3
addCard(Zone.BATTLEFIELD, playerA, "Fortress Crab"); // 1/6
addCard(Zone.BATTLEFIELD, playerA, "Eager Cadet"); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Catacomb Slug"); // 2/6
addCard(Zone.BATTLEFIELD, playerB, "War Elephant"); // Banding 2/2 Trample
attack(1, playerA, "Benalish Infantry");
attack(1, playerA, "Fortress Crab");
attack(1, playerA, "Eager Cadet");
setChoice(playerA, true);
setChoice(playerA, "Fortress Crab");
block(1, playerB, "Catacomb Slug", "Benalish Infantry");
block(1, playerB, "War Elephant", "Benalish Infantry");
setChoiceAmount(playerB, 0, 1); // Damage from Benalish Infantry
setChoiceAmount(playerB, 0, 1); // Damage from Fortress Crab
setChoiceAmount(playerA, 1, 1); // Damage from War Elephant
setChoiceAmount(playerA, 0, 2); // Damage from Catacomb Slug
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, 0);
assertDamageReceived(playerA, "Benalish Infantry", 1);
assertDamageReceived(playerA, "Fortress Crab", 3);
assertGraveyardCount(playerB, "War Elephant", 1);
assertDamageReceived(playerB, "Catacomb Slug", 0);
assertLife(playerB, 19); // Only Eager Cadet gets through
}
}

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author noxx
*/
@ -21,6 +23,7 @@ public class BushidoTest extends CardTestPlayerBase {
attack(2, playerB, "Isao, Enlightened Bushi");
block(2, playerA, "Elite Vanguard", "Isao, Enlightened Bushi");
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_COMBAT);
execute();
@ -39,6 +42,7 @@ public class BushidoTest extends CardTestPlayerBase {
attack(2, playerB, "Elite Vanguard");
block(2, playerA, "Isao, Enlightened Bushi", "Elite Vanguard");
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_COMBAT);
execute();
@ -58,9 +62,9 @@ public class BushidoTest extends CardTestPlayerBase {
attack(2, playerB, "Isao, Enlightened Bushi");
block(2, playerA, "Llanowar Elves", "Isao, Enlightened Bushi");
block(2, playerA, "Elvish Mystic", "Isao, Enlightened Bushi");
setChoice(playerB, "X=1"); // assign damage
setChoice(playerB, "X=1"); // assign damage
setChoice(playerB, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_COMBAT);
execute();

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author TheElk801
*/
@ -158,15 +160,7 @@ public class ExcessDamageTest extends CardTestPlayerBase {
block(2, playerA, bear, myrSuperion);
block(2, playerA, envoy, myrSuperion);
block(2, playerA, bondedConstruct, myrSuperion);
//Assign this much damage to the first blocking creature
setChoice(playerB, "X=2");
//Assign this much damage to the second blocking creature
setChoice(playerB, "X=1");
//Assign this much damage to the third blocking creature
setChoice(playerB, "X=1");
setChoice(playerB, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(2, PhaseStep.END_TURN);

View file

@ -15,6 +15,8 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author JayDi85
*/
@ -247,9 +249,10 @@ public class TheRingEmblemTest extends CardTestPlayerBase {
attack(3, playerA, "Ashiok's Skulker");
block(3, playerB, "Alabaster Kirin", "Ashiok's Skulker");
block(3, playerB, "Alaborn Trooper", "Ashiok's Skulker");
setChoice(playerA, "Whenever your Ring-bearer becomes blocked"); // 2x triggers from two blockers
setChoice(playerA, "At end of combat, that permanent"); // 2x triggers from two blockers
setChoice(playerA, "Mountain"); // draw/discard on attack trigger
setChoice(playerA, "Whenever your Ring-bearer becomes blocked"); // 2x triggers from two blockers
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setChoice(playerA, "At end of combat, that permanent"); // 2x triggers from two blockers
checkPermanentCount("after attack on 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerA, "Ashiok's Skulker", 1);
checkPermanentCount("after attack on 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerB, "Academy Manufactor", 0);
checkPermanentCount("after attack on 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerB, "Alabaster Kirin", 0);

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
@Test
@ -67,7 +69,7 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
attack(3, playerA, "Magma Pummeler", playerB);
block(3, playerB, "Memnite", "Magma Pummeler");
block(3, playerB, "Goblin Piker", "Magma Pummeler");
setChoice(playerA, "X=5"); // damage for Pummeler, does not really matter for this test.
setChoice(playerA, CHOICE_SKIP); // Assign default damage
addTarget(playerA, playerB); // For the one trigger
setStopAt(3, PhaseStep.END_TURN);
@ -117,7 +119,7 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
attack(3, playerA, "Magma Pummeler", playerB);
block(3, playerB, "Centaur Courser", "Magma Pummeler");
block(3, playerB, "Air Elemental", "Magma Pummeler");
setChoice(playerA, "X=5"); // damage for Pummeler, does not really matter for this test.
setChoice(playerA, CHOICE_SKIP); // Assign default damage
addTarget(playerA, playerB); // For the one trigger
setStopAt(3, PhaseStep.END_TURN);
@ -148,7 +150,7 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase {
attack(1, playerA, "Undergrowth Champion", playerB);
block(1, playerB, "Grizzly Bears", "Undergrowth Champion");
block(1, playerB, "Elite Vanguard", "Undergrowth Champion");
setChoice(playerA, "X=2"); // damage attribution
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_COMBAT);
execute();

View file

@ -6,6 +6,8 @@ import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class RiteOfPassageTest extends CardTestPlayerBase {
@ -38,12 +40,7 @@ public class RiteOfPassageTest extends CardTestPlayerBase {
attack(1, playerA, "Watchwolf", playerB);
block(1, playerB, "Memnite", "Watchwolf");
block(1, playerB, "Agent of Stromgald", "Watchwolf");
// Assign this much damage to Memnite
setChoice(playerA, "X=1");
// Assign this much damage to Agent of Stromgald
setChoice(playerA, "X=1");
setChoice(playerA, CHOICE_SKIP);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author Susucr
*/
@ -33,7 +35,9 @@ public class BindingAgonyTest extends CardTestPlayerBase {
attack(1, playerA, "Grizzly Bears");
block(1, playerB, "Centaur Courser", "Grizzly Bears");
block(1, playerB, "Memnite", "Grizzly Bears");
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -53,6 +57,7 @@ public class BindingAgonyTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, agony, "Grizzly Bears", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

View file

@ -6,6 +6,8 @@ import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author Susucr
*/
@ -32,6 +34,7 @@ public class BloatflySwarmTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, swarm, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", swarm);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
@ -55,7 +58,9 @@ public class BloatflySwarmTest extends CardTestPlayerBase {
attack(1, playerA, swarm);
block(1, playerB, "Brimstone Dragon", swarm);
block(1, playerB, "Giant Spider", swarm);
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -79,8 +84,9 @@ public class BloatflySwarmTest extends CardTestPlayerBase {
attack(1, playerA, swarm);
block(1, playerB, "Wind Drake", swarm);
block(1, playerB, "Giant Spider", swarm);
setChoice(playerA, "X=5"); // damage attribution
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();

View file

@ -48,7 +48,7 @@ public class WallOfEssenceTest extends CardTestPlayerBase {
block(1, playerB, "Memnite", "Grizzly Bears");
block(1, playerB, wall, "Grizzly Bears");
setChoice(playerA, "X=2"); // 2 damage on Memnite, no damage to Wall
setChoiceAmount(playerA, 2, 0); // 2 damage on Memnite, no damage to Wall
setStopAt(1, PhaseStep.END_TURN);
execute();

View file

@ -6,6 +6,8 @@ import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* @author Susucr
*/
@ -31,9 +33,7 @@ public class PhantomWurmTest extends CardTestPlayerBase {
attack(1, playerA, wurm, playerB);
block(1, playerB, "Memnite", wurm);
block(1, playerB, "Eager Cadet", wurm);
setChoice(playerA, "X=1"); // damage assignment
setChoice(playerA, "X=3"); // damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -115,9 +115,7 @@ public class PhantomWurmTest extends CardTestPlayerBase {
attack(1, playerA, wurm, playerB);
block(1, playerB, "Memnite", wurm);
block(1, playerB, "Goblin Striker", wurm);
setChoice(playerA, "X=1"); // damage assignment
setChoice(playerA, "X=3"); // damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_TURN);
execute();
@ -139,9 +137,7 @@ public class PhantomWurmTest extends CardTestPlayerBase {
attack(1, playerA, wurm, playerB);
block(1, playerB, "Boros Recruit", wurm);
block(1, playerB, "Goblin Striker", wurm);
setChoice(playerA, "X=1"); // damage assignment
setChoice(playerA, "X=3"); // damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
setStopAt(1, PhaseStep.END_TURN);
execute();

View file

@ -57,7 +57,7 @@ public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
private void assertDefaultValuesUnconstrained(String need, int count, int min, int max) {
List<MultiAmountMessage> constraints = getUnconstrainedConstraints(count);
List<Integer> defaultValues = MultiAmountType.prepareDefaltValues(constraints, min, max);
List<Integer> defaultValues = MultiAmountType.prepareDefaultValues(constraints, min, max);
String current = defaultValues
.stream()
.map(String::valueOf)
@ -122,7 +122,7 @@ public class TargetMultiAmountTest extends CardTestPlayerBaseWithAIHelps {
getUnconstrainedConstraints(4));
// good values are checking in test_DefaultValues, it's an additional
List<Integer> list = MultiAmountType.prepareDefaltValues(constraints.get(3), 0, 0);
List<Integer> list = MultiAmountType.prepareDefaultValues(constraints.get(3), 0, 0);
// count (0, 0, 0)
Assert.assertFalse("count", MultiAmountType.isGoodValues(list, constraints.get(0), 0, 0));

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestCommander4Players;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
*
* @author jimga150
@ -32,12 +34,7 @@ public class DonnaNobleTests extends CardTestCommander4Players {
attack(5, playerA, "Impervious Greatwurm", playerB);
block(5, playerB, "Memnite", "Impervious Greatwurm");
block(5, playerB, "Expedition Envoy", "Impervious Greatwurm");
//Assign this much damage to the first blocking creature
setChoice(playerA, "X=1");
//Assign this much damage to the second blocking creature
setChoice(playerA, "X=1");
setChoice(playerA, CHOICE_SKIP); // Assign default damage
//Target this player with Donna Noble
addTarget(playerA, playerB);

View file

@ -6,6 +6,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestCommander4Players;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
*
* @author jimga150
@ -25,9 +27,7 @@ public class DamagedBatchTests extends CardTestCommander4Players {
attack(1, playerA, "Donna Noble", playerB);
block(1, playerB, "Memnite", "Donna Noble");
block(1, playerB, "Expedition Envoy", "Donna Noble");
//Assign this much damage to the first blocking creature
setChoice(playerA, "X=1");
setChoice(playerA, CHOICE_SKIP); // Assign default damage
//Target this player with Donna Noble
addTarget(playerA, playerB);

View file

@ -11,6 +11,7 @@ import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
/**
* Test restrictions for choosing attackers and blockers.
@ -753,9 +754,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
setChoice(playerA, CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
@ -783,7 +784,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
// ai must choose all blockers anyway
attack(1, playerA, "Sonorous Howlbonder");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
setChoiceAmount(playerA, 1, 1, 0); // assign damage to blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
@ -813,9 +814,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
setChoice(playerA, CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
@ -845,7 +846,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
// ai must choose all blockers
attack(1, playerA, "Sonorous Howlbonder");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
setChoiceAmount(playerA, 1, 1, 0, 0, 0); // assign damage to blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
@ -879,7 +880,6 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("one possible blocker", 1, playerB, "Memnite");
@ -998,8 +998,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
setChoiceAmount(playerA, 1); // assign damage to 1 of 2 blocking memnites
setChoiceAmount(playerA, 1); // assign damage to 2 of 2 blocking memnites
setChoiceAmount(playerA, 1, 1); // assign damage to blocking memnites
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite");

View file

@ -197,4 +197,35 @@ public class DamageDistributionTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 5);
}
@Test
public void test2x2Block() {
addCard(Zone.BATTLEFIELD, playerA, "Catacomb Slug"); // 2/6
addCard(Zone.BATTLEFIELD, playerA, "Catacomb Crocodile"); // 3/7
addCard(Zone.BATTLEFIELD, playerB, "Brave the Sands"); //can block 2
addCard(Zone.BATTLEFIELD, playerB, "Marsh Hulk"); // 4/6
addCard(Zone.BATTLEFIELD, playerB, "Fortress Crab"); // 1/6
attack(1, playerA, "Catacomb Slug");
attack(1, playerA, "Catacomb Crocodile");
block(1, playerB, "Fortress Crab", "Catacomb Slug");
block(1, playerB, "Fortress Crab", "Catacomb Crocodile");
block(1, playerB, "Marsh Hulk", "Catacomb Slug");
block(1, playerB, "Marsh Hulk", "Catacomb Crocodile");
setChoiceAmount(playerA, 1, 1); // Catacomb Slug
setChoiceAmount(playerA, 1, 2); // Catacomb Crocodile
setChoiceAmount(playerB, 1, 0); // Fortress Crab
setChoiceAmount(playerB, 2, 2); // Marsh Hulk
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertDamageReceived(playerA, "Catacomb Slug", 3);
assertDamageReceived(playerA, "Catacomb Crocodile", 2);
assertDamageReceived(playerB, "Fortress Crab", 2);
assertDamageReceived(playerB, "Marsh Hulk", 3);
}
}

View file

@ -5,6 +5,8 @@ import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.player.TestPlayer.CHOICE_SKIP;
public class LifelinkInCombatTest extends CardTestPlayerBase {
@Test
public void testOneBlockerTrample() {
@ -43,8 +45,7 @@ public class LifelinkInCombatTest extends CardTestPlayerBase {
attack(1, playerA, "Brion Stoutarm");
block(1, playerB, "Boros Recruit", "Brion Stoutarm");
block(1, playerB, "Suntail Hawk", "Brion Stoutarm");
setChoice(playerA, "X=1"); // Damage assignment
setChoice(playerA, "X=1"); // Damage assignment
setChoice(playerA, CHOICE_SKIP); // Assign default damage
addTarget(playerA, "Brion Stoutarm");
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -2975,11 +2975,12 @@ public class TestPlayer implements Player {
assertAliasSupportInChoices(false);
int needCount = messages.size();
List<Integer> defaultList = MultiAmountType.prepareDefaltValues(messages, totalMin, totalMax);
List<Integer> defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0) {
return defaultList;
}
List<Integer> answer = new ArrayList<>(defaultList);
if (!choices.isEmpty()) {
// must fill all possible choices or skip it
@ -4477,19 +4478,6 @@ public class TestPlayer implements Player {
return computerPlayer.playMana(ability, unpaid, promptText, game);
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attacker, Game game
) {
return computerPlayer.chooseAttackerOrder(attacker, game);
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup,
List<UUID> blockerOrder, Game game
) {
return computerPlayer.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
@Override
public void sideboard(Match match, Deck deck
) {

View file

@ -8,23 +8,24 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public enum MultiAmountType {
public class MultiAmountType {
MANA("Add mana", "Distribute mana among colors"),
DAMAGE("Assign damage", "Assign damage among targets"),
P1P1("Add +1/+1 counters", "Distribute +1/+1 counters among creatures"),
COUNTERS("Choose counters", "Move counters"),
CHEAT_LANDS("Choose lands", "Add lands to your battlefield", true);
public static final MultiAmountType MANA = new MultiAmountType("Add mana", "Distribute mana among colors");
public static final MultiAmountType DAMAGE = new MultiAmountType("Assign damage", "Assign damage among targets");
public static final MultiAmountType P1P1 = new MultiAmountType("Add +1/+1 counters", "Distribute +1/+1 counters among creatures");
public static final MultiAmountType COUNTERS = new MultiAmountType("Choose counters", "Move counters");
public static final MultiAmountType CHEAT_LANDS = new MultiAmountType("Choose lands", "Add lands to your battlefield", true);
private final String title;
private final String header;
private final boolean canCancel; // choice dialog will return null instead default values
MultiAmountType(String title, String header) {
public MultiAmountType(String title, String header) {
this(title, header, false);
}
MultiAmountType(String title, String header, boolean canCancel) {
public MultiAmountType(String title, String header, boolean canCancel) {
this.title = title;
this.header = header;
this.canCancel = canCancel;
@ -42,7 +43,7 @@ public enum MultiAmountType {
return canCancel;
}
public static List<Integer> prepareDefaltValues(List<MultiAmountMessage> constraints, int min, int max) {
public static List<Integer> prepareDefaultValues(List<MultiAmountMessage> constraints, int min, int max) {
// default values must be assigned from first to last by minimum values
List<Integer> res = constraints.stream().map(m -> m.defaultValue > Integer.MIN_VALUE ? m.defaultValue : Math.min(0, max))
.collect(Collectors.toList());
@ -50,7 +51,7 @@ public enum MultiAmountType {
return res;
}
int total = res.stream().mapToInt(x -> x).sum();;
int total = res.stream().mapToInt(x -> x).sum();
// Fill values until we reach the overall minimum. Do this by filling values up until either their max or however much is leftover, starting with the first option.
if (min > 0 && total < min) {
@ -174,7 +175,7 @@ public enum MultiAmountType {
// data check
if (returnDefaultOnError && !isGoodValues(res, constraints, min, max)) {
// on broken data - return default
return prepareDefaltValues(constraints, min, max);
return prepareDefaultValues(constraints, min, max);
}
return res;

View file

@ -60,7 +60,7 @@ public class Combat implements Serializable, Copyable<Combat> {
protected List<CombatGroup> groups = new ArrayList<>();
protected List<CombatGroup> formerGroups = new ArrayList<>();
protected Map<UUID, CombatGroup> blockingGroups = new HashMap<>();
protected Map<UUID, CombatGroup> blockingGroups = new LinkedHashMap<>();
// all possible defenders (players, planeswalkers or battle)
protected Set<UUID> defenders = new HashSet<>();
// how many creatures attack defending player
@ -200,7 +200,7 @@ public class Combat implements Serializable, Copyable<Combat> {
StringBuilder sb = new StringBuilder();
sb.append(attackingPlayerId).append(defenders);
for (CombatGroup group : groups) {
sb.append(group.defenderId).append(group.attackers).append(group.attackerOrder).append(group.blockers).append(group.blockerOrder);
sb.append(group.defenderId).append(group.attackers).append(group.blockers);
}
return sb.toString();
}
@ -785,7 +785,7 @@ public class Combat implements Serializable, Copyable<Combat> {
if (attackerExists) {
if (!group.getBlockers().isEmpty()) {
sb.append("blocked by ");
for (UUID blockingCreatureId : group.getBlockerOrder()) {
for (UUID blockingCreatureId : group.getBlockers()) {
Permanent blockingCreature = game.getPermanent(blockingCreatureId);
if (blockingCreature != null) {
sb.append(blockingCreature.getLogName()).append(" (");
@ -1799,24 +1799,11 @@ public class Combat implements Serializable, Copyable<Combat> {
return playerDefenders;
}
public void damageAssignmentOrder(Game game) {
for (CombatGroup group : groups) {
group.pickBlockerOrder(attackingPlayerId, game);
}
for (Map.Entry<UUID, CombatGroup> blockingGroup : blockingGroups.entrySet()) {
Permanent blocker = game.getPermanent(blockingGroup.getKey());
if (blocker != null) {
blockingGroup.getValue().pickAttackerOrder(blocker.getControllerId(), game);
}
}
}
@SuppressWarnings("deprecation")
public void removeAttacker(UUID attackerId, Game game) {
for (CombatGroup group : groups) {
if (group.attackers.contains(attackerId)) {
group.attackers.remove(attackerId);
group.attackerOrder.remove(attackerId);
for (Set<UUID> attackingCreatures : numberCreaturesDefenderAttackedBy.values()) {
attackingCreatures.remove(attackerId);
}
@ -1869,7 +1856,6 @@ public class Combat implements Serializable, Copyable<Combat> {
}
for (CombatGroup group : groupsToCheck) {
group.blockers.remove(blockerId);
group.blockerOrder.remove(blockerId);
if (group.blockers.isEmpty()) {
group.blocked = false;
}
@ -1885,11 +1871,9 @@ public class Combat implements Serializable, Copyable<Combat> {
if (blockGroup.blockers.contains(blockerId)) {
for (UUID attackerId : group.getAttackers()) {
blockGroup.attackers.remove(attackerId);
blockGroup.attackerOrder.remove(attackerId);
}
if (creature.getBlocking() == 0) {
blockGroup.blockers.remove(blockerId);
blockGroup.attackerOrder.clear();
}
}
if (blockGroup.blockers.isEmpty()) {
@ -1914,7 +1898,6 @@ public class Combat implements Serializable, Copyable<Combat> {
for (CombatGroup group : groups) {
if (group.blockers.contains(blockerId)) {
group.blockers.remove(blockerId);
group.blockerOrder.remove(blockerId);
if (group.blockers.isEmpty()) {
group.blocked = false;
}
@ -1924,7 +1907,6 @@ public class Combat implements Serializable, Copyable<Combat> {
for (CombatGroup group : getBlockingGroups()) {
if (group.blockers.contains(blockerId)) {
group.blockers.remove(blockerId);
group.attackerOrder.clear();
}
if (group.blockers.isEmpty()) {
canRemove = true;

View file

@ -6,6 +6,7 @@ import mage.abilities.common.ControllerDivideCombatDamageAbility;
import mage.abilities.common.DamageAsThoughNotBlockedAbility;
import mage.abilities.keyword.*;
import mage.constants.AsThoughEffectType;
import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.filter.StaticFilters;
import mage.game.Game;
@ -15,6 +16,7 @@ import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.Copyable;
import mage.util.MultiAmountMessage;
import mage.watchers.common.FirstStrikeWatcher;
import java.io.Serializable;
@ -29,8 +31,6 @@ 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<>();
protected Map<UUID, UUID> players = new HashMap<>();
protected boolean blocked;
protected UUID defenderId; // planeswalker, player, or battle id, can be null after remove from combat (e.g. due damage)
@ -52,8 +52,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
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);
this.players.putAll(group.players);
this.blocked = group.blocked;
this.defenderId = group.defenderId;
@ -91,10 +89,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
return blockers;
}
public List<UUID> getBlockerOrder() {
return blockerOrder;
}
private static boolean hasFirstOrDoubleStrike(Permanent perm) {
return hasFirstStrike(perm) || hasDoubleStrike(perm);
}
@ -175,7 +169,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
if (attacker != null && !assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(attacker, attacker.getControllerId(), first, game, true)) {
if (blockers.isEmpty()) {
unblockedDamage(first, game);
return;
} else {
Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : attacker.getControllerId());
if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) &&
@ -186,11 +179,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
blocked = false;
unblockedDamage(first, game);
}
if (blockers.size() == 1) {
singleBlockerDamage(player, first, game);
} else {
multiBlockerDamage(player, first, game);
}
blockerDamage(player, first, game);
}
}
}
@ -206,9 +195,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
if (attackers.size() != 1) {
multiAttackerDamage(first, game);
// } else {
// singleAttackerDamage(first, game);
attackerDamage(first, game);
}
}
}
@ -269,51 +256,16 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
}
private void singleBlockerDamage(Player player, boolean first, Game game) {
Permanent blocker = game.getPermanent(blockers.get(0));
Permanent attacker = game.getPermanent(attackers.get(0));
if (blocker != null && attacker != null) {
int blockerDamage = getDamageValueFromPermanent(blocker, game); // must be set before attacker damage marking because of effects like Test of Faith
if (blocked && dealsDamageThisStep(attacker, first, game)) {
int damage = getDamageValueFromPermanent(attacker, game);
if (hasTrample(attacker)) {
int lethalDamage = getLethalDamage(blocker, attacker, game);
if (lethalDamage >= damage) {
blocker.markDamage(damage, attacker.getId(), null, game, true, true);
} else {
int damageAssigned = player.getAmount(lethalDamage, damage, "Assign damage to " + blocker.getName(), game);
blocker.markDamage(damageAssigned, attacker.getId(), null, game, true, true);
damage -= damageAssigned;
if (damage > 0) {
defenderDamage(attacker, damage, game, false);
}
}
} else {
blocker.markDamage(damage, attacker.getId(), null, game, true, true);
}
}
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
attacker.markDamage(blockerDamage, blocker.getId(), null, game, true, true);
}
}
}
}
}
private void multiBlockerDamage(Player player, boolean first, Game game) {
private void blockerDamage(Player player, boolean first, Game game) {
Permanent attacker = game.getPermanent(attackers.get(0));
if (attacker == null) {
return;
}
boolean oldRuleDamage = (Objects.equals(player.getId(), defendingPlayerId));
int damage = getDamageValueFromPermanent(attacker, game);
if (dealsDamageThisStep(attacker, first, game)) {
// must be set before attacker damage marking because of effects like Test of Faith
Map<UUID, Integer> blockerPower = new HashMap<>();
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
@ -322,42 +274,62 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
Map<UUID, Integer> assigned = new HashMap<>();
List<MultiAmountMessage> damageDivision = new ArrayList<>();
List<UUID> blockersCopy = new ArrayList<>(blockers);
if (blocked) {
boolean excessDamageToDefender = true;
for (UUID blockerId : new ArrayList<>(blockerOrder)) { // prevent ConcurrentModificationException
int remainingDamage = damage;
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (blocker != null) {
int lethalDamage = getLethalDamage(blocker, attacker, game);
if (lethalDamage >= damage) {
if (!oldRuleDamage) {
assigned.put(blockerId, damage);
damage = 0;
break;
} else if (damage == 0) {
break;
}
}
int damageAssigned = 0;
if (!oldRuleDamage) {
damageAssigned = player.getAmount(lethalDamage, damage, "Assign damage to " + blocker.getName(), game);
} else {
damageAssigned = player.getAmount(0, damage, "Assign damage to " + blocker.getName(), game);
if (damageAssigned < lethalDamage) {
excessDamageToDefender = false; // all blockers need to have lethal damage assigned before it can trample over to the defender
}
}
assigned.put(blockerId, damageAssigned);
damage -= damageAssigned;
int defaultDamage = Math.min(remainingDamage, blocker.getLethalDamage(attacker.getId(), game));
remainingDamage -= defaultDamage;
String message = String.format("%s, P/T: %d/%d",
blocker.getLogName(),
blocker.getPower().getValue(),
blocker.getToughness().getValue());
damageDivision.add(new MultiAmountMessage(message, 0, damage, defaultDamage));
}
}
if (damage > 0 && hasTrample(attacker) && excessDamageToDefender) {
defenderDamage(attacker, damage, game, false);
} else if (!blockerOrder.isEmpty()) {
// Assign the damage left to first blocker
assigned.put(blockerOrder.get(0), assigned.get(blockerOrder.get(0)) == null ? 0 : assigned.get(blockerOrder.get(0)) + damage);
List<Integer> amounts;
if (hasTrample(attacker)){
if (remainingDamage > 0 || damageDivision.size() > 1) {
MultiAmountType dialogue = new MultiAmountType("Assign combat damage (with trample)",
String.format("Assign combat damage among creatures blocking %s, P/T: %d/%d (Unassigned damage tramples through)",
attacker.getLogName(), attacker.getPower().getValue(), attacker.getToughness().getValue()));
amounts = player.getMultiAmountWithIndividualConstraints(Outcome.Damage, damageDivision, damage - remainingDamage, damage, dialogue, game);
} else {
amounts = new ArrayList<>();
if (damageDivision.size() == 1) { // Assign all damage to one blocker
amounts.add(damage);
}
}
int trampleDamage = damage - (amounts.stream().mapToInt(x -> x).sum());
if (trampleDamage > 0) {
defenderDamage(attacker, trampleDamage, game, false);
}
} else {
if (remainingDamage > 0){
damageDivision.get(0).defaultValue += remainingDamage;
}
if (damageDivision.size() > 1) {
MultiAmountType dialogue = new MultiAmountType("Assign combat damage",
String.format("Assign combat damage among creatures blocking %s, P/T: %d/%d",
attacker.getLogName(), attacker.getPower().getValue(), attacker.getToughness().getValue()));
amounts = player.getMultiAmountWithIndividualConstraints(Outcome.Damage, damageDivision, damage, damage, dialogue, game);
} else {
amounts = new LinkedList<>();
if (damageDivision.size() == 1) { // Assign all damage to one blocker
amounts.add(damage);
}
}
}
if (!damageDivision.isEmpty()){
for (int i=0; i<blockersCopy.size(); i++) {
assigned.put(blockersCopy.get(i), amounts.get(i));
}
}
}
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Integer power = blockerPower.get(blockerId);
if (power != null) {
// might be missing canDamage condition?
@ -374,7 +346,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
} else {
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
@ -395,7 +367,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
if (dealsDamageThisStep(attacker, first, game)) {
// must be set before attacker damage marking because of effects like Test of Faith
Map<UUID, Integer> blockerPower = new HashMap<>();
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
@ -422,7 +394,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
if (isAttacking) {
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Integer power = blockerPower.get(blockerId);
if (power != null) {
// might be missing canDamage condition?
@ -439,7 +411,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
} else {
if (isAttacking) {
for (UUID blockerId : blockerOrder) {
for (UUID blockerId : blockers) {
Permanent blocker = game.getPermanent(blockerId);
if (dealsDamageThisStep(blocker, first, game)) {
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
@ -473,81 +445,62 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
/**
* Damages attacking creatures by a creature that blocked several ones
* Damages only attackers as blocker was damage in
* {@link #singleBlockerDamage}.
* {@link #blockerDamage}.
* <p>
* Handles abilities like "{this} an block any number of creatures.".
* <p>
* Blocker damage for blockers blocking single creatures is handled in the
* single/multi blocker methods, so this shouldn't be used anymore.
*
* @param first
* @param game
* @deprecated
*/
@Deprecated
private void singleAttackerDamage(boolean first, Game game) {
Permanent blocker = game.getPermanent(blockers.get(0));
Permanent attacker = game.getPermanent(attackers.get(0));
if (blocker != null && attacker != null) {
if (dealsDamageThisStep(blocker, first, game)) {
int damage = getDamageValueFromPermanent(blocker, game);
attacker.markDamage(damage, blocker.getId(), null, game, true, true);
}
}
}
/**
* Damages attacking creatures by a creature that blocked several ones
* Damages only attackers as blocker was damage in either
* {@link #singleBlockerDamage} or {@link #multiBlockerDamage}.
* <p>
* Handles abilities like "{this} an block any number of creatures.".
* Handles abilities like "{this} can block any number of creatures.".
*
* @param first
* @param game
*/
private void multiAttackerDamage(boolean first, Game game) {
private void attackerDamage(boolean first, Game game) {
Permanent blocker = game.getPermanent(blockers.get(0));
if (blocker == null) {
return;
}
boolean oldRuleDamage = attackerAssignsCombatDamage(game); // handles banding
Player player = game.getPlayer(oldRuleDamage ? game.getCombat().getAttackingPlayerId() : blocker.getControllerId());
//Handle Banding
Player player = game.getPlayer(attackerAssignsCombatDamage(game) ? game.getCombat().getAttackingPlayerId() : blocker.getControllerId());
int damage = getDamageValueFromPermanent(blocker, game);
if (dealsDamageThisStep(blocker, first, game)) {
Map<UUID, Integer> assigned = new HashMap<>();
for (UUID attackerId : attackerOrder) {
List<MultiAmountMessage> damageDivision = new ArrayList<>();
List<UUID> attackersCopy = new ArrayList<>(attackers);
int remainingDamage = damage;
for (UUID attackerId : attackers) {
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null) {
int lethalDamage = getLethalDamage(attacker, blocker, game);
if (lethalDamage >= damage) {
if (!oldRuleDamage) {
assigned.put(attackerId, damage);
damage = 0;
break;
} else if (damage == 0) {
break;
}
}
int damageAssigned = 0;
if (!oldRuleDamage) {
damageAssigned = player.getAmount(lethalDamage, damage, "Assign damage to " + attacker.getName(), game);
} else {
damageAssigned = player.getAmount(0, damage, "Assign damage to " + attacker.getName(), game);
}
assigned.put(attackerId, damageAssigned);
damage -= damageAssigned;
int defaultDamage = Math.min(remainingDamage, attacker.getLethalDamage(blocker.getId(), game));
remainingDamage -= defaultDamage;
String message = String.format("%s, P/T: %d/%d",
attacker.getLogName(),
attacker.getPower().getValue(),
attacker.getToughness().getValue());
damageDivision.add(new MultiAmountMessage(message, 0, damage, defaultDamage));
}
}
if (damage > 0) {
// Assign the damage left to first attacker
assigned.put(attackerOrder.get(0), assigned.get(attackerOrder.get(0)) + damage);
List<Integer> amounts;
if (remainingDamage > 0){
damageDivision.get(0).defaultValue += remainingDamage;
}
if (damageDivision.size() > 1) {
MultiAmountType dialogue = new MultiAmountType("Assign blocker combat damage",
String.format("Assign combat damage among creatures blocked by %s, P/T: %d/%d",
blocker.getLogName(), blocker.getPower().getValue(), blocker.getToughness().getValue()));
amounts = player.getMultiAmountWithIndividualConstraints(Outcome.Damage, damageDivision, damage, damage, dialogue, game);
} else {
amounts = new LinkedList<>();
amounts.add(damage);
}
if (!damageDivision.isEmpty()){
for (int i=0; i<attackersCopy.size(); i++) {
assigned.put(attackersCopy.get(i), amounts.get(i));
}
}
for (Map.Entry<UUID, Integer> entry : assigned.entrySet()) {
Permanent attacker = game.getPermanent(entry.getKey());
attacker.markDamage(entry.getValue(), blocker.getId(), null, game, true, true);
if (attacker != null) {
attacker.markDamage(entry.getValue(), blocker.getId(), null, game, true, true);
}
}
}
}
@ -632,74 +585,11 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
if (blockerId != null && blocker != null) {
blocker.setBlocking(blocker.getBlocking() + 1);
blockers.add(blockerId);
blockerOrder.add(blockerId);
this.blocked = true;
this.players.put(blockerId, playerId);
}
}
public void pickBlockerOrder(UUID playerId, Game game) {
if (blockers.isEmpty()) {
return;
}
Player player = game.getPlayer(playerId); // game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : playerId); // this was incorrect because defenderAssignsCombatDamage might be false by the time damage is dealt
List<UUID> blockerList = new ArrayList<>(blockers);
blockerOrder.clear();
while (player.canRespond()) {
if (blockerList.size() == 1) {
blockerOrder.add(blockerList.get(0));
break;
} else {
List<Permanent> blockerPerms = new ArrayList<>();
for (UUID blockerId : blockerList) {
blockerPerms.add(game.getPermanent(blockerId));
}
UUID blockerId = player.chooseBlockerOrder(blockerPerms, this, blockerOrder, game);
blockerOrder.add(blockerId);
blockerList.remove(blockerId);
}
}
if (!game.isSimulation() && blockerOrder.size() > 1) {
logDamageAssignmentOrder("Creatures blocking ", attackers, blockerOrder, game);
}
}
public void pickAttackerOrder(UUID playerId, Game game) {
Player player = game.getPlayer(playerId);
if (attackers.isEmpty() || player == null) {
return;
}
List<UUID> attackerList = new ArrayList<>(attackers);
List<UUID> newAttackerOrder = new ArrayList<>();
while (true) {
if (attackerList.size() == 1) {
newAttackerOrder.add(attackerList.get(0));
break;
} else {
List<Permanent> attackerPerms = new ArrayList<>();
for (UUID attackerId : attackerList) {
attackerPerms.add(game.getPermanent(attackerId));
}
UUID attackerId = player.chooseAttackerOrder(attackerPerms, game);
if (attackerId == null) {
break;
}
newAttackerOrder.add(attackerId);
attackerList.remove(attackerId);
}
}
if (attackerOrder.isEmpty() || newAttackerOrder.size() == attackerOrder.size()) {
attackerOrder.clear();
attackerOrder.addAll(newAttackerOrder);
if (!game.isSimulation() && attackerOrder.size() > 1) {
logDamageAssignmentOrder("Creatures blocked by ", blockers, attackerOrder, game);
}
} else {
game.informPlayers(player.getLogName() + " try to skip choose attacker order");
}
}
private void logDamageAssignmentOrder(String prefix, List<UUID> assignedFor, List<UUID> assignedOrder, Game game) {
StringBuilder sb = new StringBuilder(prefix);
boolean first = true;
@ -746,12 +636,9 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
formerAttackers.add(creatureId);
attackers.remove(creatureId);
result = true;
attackerOrder.remove(creatureId);
} else if (blockers.contains(creatureId)) {
blockers.remove(creatureId);
result = true;
//20100423 - 509.2a
blockerOrder.remove(creatureId);
}
return result;
}
@ -825,7 +712,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
game.getCombat().removeBlocker(blockerId, game);
}
blockers.clear();
blockerOrder.clear();
if (!game.isSimulation()) {
game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded.");
}
@ -844,7 +730,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
game.getCombat().removeBlocker(blockerId, game);
}
blockers.clear();
blockerOrder.clear();
if (!game.isSimulation()) {
game.informPlayers(new StringBuilder(attacker.getLogName())
.append(" can't be blocked by more than ").append(attacker.getMaxBlockedBy())

View file

@ -1,12 +1,12 @@
package mage.game.turn;
import java.util.UUID;
import mage.constants.PhaseStep;
import mage.game.Game;
import mage.game.events.GameEvent.EventType;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
@ -37,7 +37,6 @@ public class DeclareBlockersStep extends Step {
game.getCombat().selectBlockers(game);
if (!game.isPaused() && !game.executingRollback()) {
game.getCombat().acceptBlockers(game);
game.getCombat().damageAssignmentOrder(game);
}
}
@ -46,7 +45,6 @@ public class DeclareBlockersStep extends Step {
super.resumeBeginStep(game, activePlayerId);
game.getCombat().resumeSelectBlockers(game);
game.getCombat().acceptBlockers(game);
game.getCombat().damageAssignmentOrder(game);
}
@Override

View file

@ -23,7 +23,6 @@ import mage.filter.FilterCard;
import mage.filter.FilterMana;
import mage.filter.FilterPermanent;
import mage.game.*;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.events.GameEvent;
import mage.game.match.Match;
@ -761,19 +760,6 @@ public interface Player extends MageItem, Copyable<Player> {
void selectBlockers(Ability source, Game game, UUID defendingPlayerId);
UUID chooseAttackerOrder(List<Permanent> attacker, Game game);
/**
* Choose the order in which blockers get damage assigned to
*
* @param blockers list of blockers where to choose the next one from
* @param combatGroup the concerning combat group
* @param blockerOrder the already set order of blockers
* @param game
* @return blocker next to add to the blocker order
*/
UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game);
int getAmount(int min, int max, String message, Game game);
/**

View file

@ -18,10 +18,8 @@ import mage.constants.Outcome;
import mage.constants.RangeOfInfluence;
import mage.filter.FilterMana;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.tournament.Tournament;
import mage.target.Target;
import mage.target.TargetAmount;
@ -190,16 +188,6 @@ public class StubPlayer extends PlayerImpl {
}
@Override
public UUID chooseAttackerOrder(List<Permanent> attacker, Game game) {
return null;
}
@Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, List<UUID> blockerOrder, Game game) {
return null;
}
@Override
public int getAmount(int min, int max, String message, Game game) {
return 0;