mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
AI: reworked blockers selections:
* fixed game freezes for no-possible block configurations like Menace (#13290); * fixed computer cheating to ignore block requirements like Menace (now AI will choose all required blockers instead 1); * improved computer logic for blockers selection (try to sacrifice a creature instead game loose, simple use cases only); * added freeze protection for bad or unsupported attacker-block configuration; * refactor: deleted outdated AI code;
This commit is contained in:
parent
a99871988d
commit
92b7ed8efc
12 changed files with 495 additions and 202 deletions
|
|
@ -155,7 +155,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
+ (p.isTapped() ? ",tapped" : "")
|
+ (p.isTapped() ? ",tapped" : "")
|
||||||
+ (p.isAttacking() ? ",attacking" : "")
|
+ (p.isAttacking() ? ",attacking" : "")
|
||||||
+ (p.getBlocking() > 0 ? ",blocking" : "")
|
+ (p.getBlocking() > 0 ? ",blocking" : "")
|
||||||
+ ":" + GameStateEvaluator2.evaluatePermanent(p, game))
|
+ ":" + GameStateEvaluator2.evaluatePermanent(p, game, true))
|
||||||
.collect(Collectors.joining("; "));
|
.collect(Collectors.joining("; "));
|
||||||
sb.append("-> Permanents: [").append(ownPermanentsInfo).append("]");
|
sb.append("-> Permanents: [").append(ownPermanentsInfo).append("]");
|
||||||
logger.info(sb.toString());
|
logger.info(sb.toString());
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ public final class GameStateEvaluator2 {
|
||||||
public static final int HAND_CARD_SCORE = 5;
|
public static final int HAND_CARD_SCORE = 5;
|
||||||
|
|
||||||
public static PlayerEvaluateScore evaluate(UUID playerId, Game game) {
|
public static PlayerEvaluateScore evaluate(UUID playerId, Game game) {
|
||||||
|
return evaluate(playerId, game, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerEvaluateScore evaluate(UUID playerId, Game game, boolean useCombatPermanentScore) {
|
||||||
// TODO: add multi opponents support, so AI can take better actions
|
// TODO: add multi opponents support, so AI can take better actions
|
||||||
Player player = game.getPlayer(playerId);
|
Player player = game.getPlayer(playerId);
|
||||||
Player opponent = game.getPlayer(game.getOpponents(playerId).stream().findFirst().orElse(null));
|
Player opponent = game.getPlayer(game.getOpponents(playerId).stream().findFirst().orElse(null));
|
||||||
|
|
@ -63,7 +67,7 @@ public final class GameStateEvaluator2 {
|
||||||
|
|
||||||
// add values of player
|
// add values of player
|
||||||
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) {
|
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) {
|
||||||
int onePermScore = evaluatePermanent(permanent, game);
|
int onePermScore = evaluatePermanent(permanent, game, useCombatPermanentScore);
|
||||||
playerPermanentsScore += onePermScore;
|
playerPermanentsScore += onePermScore;
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
sbPlayer.append(permanent.getName()).append('[').append(onePermScore).append("] ");
|
sbPlayer.append(permanent.getName()).append('[').append(onePermScore).append("] ");
|
||||||
|
|
@ -77,7 +81,7 @@ public final class GameStateEvaluator2 {
|
||||||
|
|
||||||
// add values of opponent
|
// add values of opponent
|
||||||
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(opponent.getId())) {
|
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(opponent.getId())) {
|
||||||
int onePermScore = evaluatePermanent(permanent, game);
|
int onePermScore = evaluatePermanent(permanent, game, useCombatPermanentScore);
|
||||||
opponentPermanentsScore += onePermScore;
|
opponentPermanentsScore += onePermScore;
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
sbOpponent.append(permanent.getName()).append('[').append(onePermScore).append("] ");
|
sbOpponent.append(permanent.getName()).append('[').append(onePermScore).append("] ");
|
||||||
|
|
@ -121,7 +125,7 @@ public final class GameStateEvaluator2 {
|
||||||
opponentLifeScore, opponentHandScore, opponentPermanentsScore);
|
opponentLifeScore, opponentHandScore, opponentPermanentsScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int evaluatePermanent(Permanent permanent, Game game) {
|
public static int evaluatePermanent(Permanent permanent, Game game, boolean useCombatPermanentScore) {
|
||||||
// prevent AI from attaching bad auras to its own permanents ex: Brainwash and Demonic Torment (no immediate penalty on the battlefield)
|
// prevent AI from attaching bad auras to its own permanents ex: Brainwash and Demonic Torment (no immediate penalty on the battlefield)
|
||||||
int value = 0;
|
int value = 0;
|
||||||
if (!permanent.getAttachments().isEmpty()) {
|
if (!permanent.getAttachments().isEmpty()) {
|
||||||
|
|
@ -137,14 +141,11 @@ public final class GameStateEvaluator2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value += ArtificialScoringSystem.getFixedPermanentScore(game, permanent)
|
value += ArtificialScoringSystem.getFixedPermanentScore(game, permanent);
|
||||||
+ ArtificialScoringSystem.getVariablePermanentScore(game, permanent);
|
value += ArtificialScoringSystem.getDynamicPermanentScore(game, permanent);
|
||||||
return value;
|
if (useCombatPermanentScore) {
|
||||||
|
value += ArtificialScoringSystem.getCombatPermanentScore(game, permanent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int evaluateCreature(Permanent creature, Game game) {
|
|
||||||
int value = ArtificialScoringSystem.getFixedPermanentScore(game, creature)
|
|
||||||
+ ArtificialScoringSystem.getVariablePermanentScore(game, creature);
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,11 @@ public final class ArtificialScoringSystem {
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getVariablePermanentScore(final Game game, final Permanent permanent) {
|
public static int getDynamicPermanentScore(final Game game, final Permanent permanent) {
|
||||||
|
|
||||||
int score = permanent.getCounters(game).getCount(CounterType.CHARGE) * 30;
|
int score = permanent.getCounters(game).getCount(CounterType.CHARGE) * 30;
|
||||||
score += permanent.getCounters(game).getCount(CounterType.LEVEL) * 30;
|
score += permanent.getCounters(game).getCount(CounterType.LEVEL) * 30;
|
||||||
score -= permanent.getDamage() * 2;
|
score -= permanent.getDamage() * 2;
|
||||||
if (!canTap(game, permanent)) {
|
|
||||||
score += getTappedScore(game, permanent);
|
|
||||||
}
|
|
||||||
if (permanent.getCardType(game).contains(CardType.CREATURE)) {
|
if (permanent.getCardType(game).contains(CardType.CREATURE)) {
|
||||||
final int power = permanent.getPower().getValue();
|
final int power = permanent.getPower().getValue();
|
||||||
final int toughness = permanent.getToughness().getValue();
|
final int toughness = permanent.getToughness().getValue();
|
||||||
|
|
@ -95,11 +92,19 @@ public final class ArtificialScoringSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
score += equipments + enchantments;
|
score += equipments + enchantments;
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getCombatPermanentScore(final Game game, final Permanent permanent) {
|
||||||
|
int score = 0;
|
||||||
|
if (!canTap(game, permanent)) {
|
||||||
|
score += getTappedScore(game, permanent);
|
||||||
|
}
|
||||||
|
if (permanent.getCardType(game).contains(CardType.CREATURE)) {
|
||||||
if (!permanent.canAttack(null, game)) {
|
if (!permanent.canAttack(null, game)) {
|
||||||
score -= 100;
|
score -= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permanent.canBlockAny(game)) {
|
if (!permanent.canBlockAny(game)) {
|
||||||
score -= 30;
|
score -= 30;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import mage.game.permanent.Permanent;
|
||||||
import mage.game.turn.CombatDamageStep;
|
import mage.game.turn.CombatDamageStep;
|
||||||
import mage.game.turn.EndOfCombatStep;
|
import mage.game.turn.EndOfCombatStep;
|
||||||
import mage.game.turn.Step;
|
import mage.game.turn.Step;
|
||||||
|
import mage.player.ai.GameStateEvaluator2;
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
|
|
||||||
|
|
@ -89,12 +90,22 @@ public final class CombatUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Permanent getWorstCreature(List<Permanent> creatures) {
|
public static Permanent getWorstCreature(List<Permanent>... lists) {
|
||||||
if (creatures.isEmpty()) {
|
for (List<Permanent> list : lists) {
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
list.sort(Comparator.comparingInt(p -> p.getPower().getValue()));
|
||||||
|
return list.get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
creatures.sort(Comparator.comparingInt(p -> p.getPower().getValue()));
|
|
||||||
return creatures.get(0);
|
public static void removeWorstCreature(Permanent permanent, List<Permanent>... lists) {
|
||||||
|
for (List<Permanent> list : lists) {
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
list.remove(permanent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int sumDamage(List<Permanent> attackersThatWontBeBlocked, Player defender) {
|
private static int sumDamage(List<Permanent> attackersThatWontBeBlocked, Player defender) {
|
||||||
|
|
@ -144,25 +155,98 @@ public final class CombatUtil {
|
||||||
return canBlock;
|
return canBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CombatInfo blockWithGoodTrade(Game game, List<Permanent> attackers, List<Permanent> blockers) {
|
/**
|
||||||
|
* AI related code, find better block combination for attackers
|
||||||
|
*/
|
||||||
|
public static CombatInfo blockWithGoodTrade2(Game game, List<Permanent> attackers, List<Permanent> blockers) {
|
||||||
UUID attackerId = game.getCombat().getAttackingPlayerId();
|
UUID attackerId = game.getCombat().getAttackingPlayerId();
|
||||||
UUID defenderId = game.getCombat().getDefenders().iterator().next();
|
UUID defenderId = game.getCombat().getDefenders().iterator().next();
|
||||||
if (attackerId == null || defenderId == null) {
|
if (attackerId == null || defenderId == null) {
|
||||||
log.warn("Couldn't find attacker or defender: " + attackerId + ' ' + defenderId);
|
|
||||||
return new CombatInfo();
|
return new CombatInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: implement full game simulations of all possible combinations (e.g. multiblockers support)
|
||||||
|
|
||||||
CombatInfo combatInfo = new CombatInfo();
|
CombatInfo combatInfo = new CombatInfo();
|
||||||
for (Permanent attacker : attackers) {
|
for (Permanent attacker : attackers) {
|
||||||
//TODO: handle attackers with "can't be blocked except"
|
// simple combat simulation (1 vs 1)
|
||||||
List<Permanent> possibleBlockers = getPossibleBlockers(game, attacker, blockers);
|
List<Permanent> allBlockers = getPossibleBlockers(game, attacker, blockers);
|
||||||
List<Permanent> survivedBlockers = getBlockersThatWillSurvive(game, attackerId, defenderId, attacker, possibleBlockers);
|
List<SurviveInfo> blockerStats = getBlockersThatWillSurvive2(game, attackerId, defenderId, attacker, allBlockers);
|
||||||
if (!survivedBlockers.isEmpty()) {
|
Map<Permanent, Integer> blockingDiffScore = new HashMap<>();
|
||||||
Permanent blocker = getWorstCreature(survivedBlockers);
|
Map<Permanent, Integer> nonBlockingDiffScore = new HashMap<>();
|
||||||
combatInfo.addPair(attacker, blocker);
|
blockerStats.forEach(s -> {
|
||||||
blockers.remove(blocker);
|
blockingDiffScore.put(s.getBlocker(), s.getDiffBlockingScore());
|
||||||
|
nonBlockingDiffScore.put(s.getBlocker(), s.getDiffNonblockingScore());
|
||||||
|
});
|
||||||
|
|
||||||
|
// split blockers usage priority
|
||||||
|
List<Permanent> survivedAndKillBlocker = new ArrayList<>();
|
||||||
|
List<Permanent> survivedBlockers2 = new ArrayList<>();
|
||||||
|
List<Permanent> diedBlockers = new ArrayList<>();
|
||||||
|
blockerStats.forEach(stats -> {
|
||||||
|
if (stats.isAttackerDied() && !stats.isBlockerDied()) {
|
||||||
|
survivedAndKillBlocker.add(stats.getBlocker());
|
||||||
|
} else if (!stats.isBlockerDied()) {
|
||||||
|
survivedBlockers2.add(stats.getBlocker());
|
||||||
|
} else {
|
||||||
|
diedBlockers.add(stats.getBlocker());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
int blockedCount = 0;
|
||||||
|
|
||||||
|
// find good blocker
|
||||||
|
Permanent blocker = getWorstCreature(survivedAndKillBlocker, survivedBlockers2);
|
||||||
|
if (blocker != null) {
|
||||||
|
combatInfo.addPair(attacker, blocker);
|
||||||
|
removeWorstCreature(blocker, blockers, survivedAndKillBlocker, survivedBlockers2);
|
||||||
|
blockedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find good sacrifices
|
||||||
|
// TODO: add chump blocking support here?
|
||||||
|
// TODO: there are many triggers on damage, attack, etc - it can't be processed without real game simulations
|
||||||
|
if (blocker == null) {
|
||||||
|
blocker = getWorstCreature(diedBlockers);
|
||||||
|
int diffBlockingScore = blockingDiffScore.getOrDefault(blocker, 0);
|
||||||
|
int diffNonBlockingScore = nonBlockingDiffScore.getOrDefault(blocker, 0);
|
||||||
|
if (diffBlockingScore >= 0 || diffBlockingScore > diffNonBlockingScore) {
|
||||||
|
// it's good use case - can block and kill
|
||||||
|
combatInfo.addPair(attacker, blocker);
|
||||||
|
removeWorstCreature(blocker, blockers, diedBlockers);
|
||||||
|
blockedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find blockers for restrictions
|
||||||
|
while (true) {
|
||||||
|
if (blockers.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add multiple use case support with min/max blockedBy conditional and other
|
||||||
|
// see all possible use cases in checkBlockRestrictions, checkBlockRequirementsAfter and checkBlockRestrictionsAfter
|
||||||
|
|
||||||
|
// effects support: can't be blocked except by xxx or more creatures
|
||||||
|
if (blockedCount > 0 && attacker.getMinBlockedBy() > blockedCount) {
|
||||||
|
// it already has 1 blocker (killer in best use case), so no needs in second killer
|
||||||
|
blocker = getWorstCreature(survivedBlockers2, survivedAndKillBlocker, diedBlockers);
|
||||||
|
if (blocker != null) {
|
||||||
|
combatInfo.addPair(attacker, blocker);
|
||||||
|
removeWorstCreature(blocker, blockers, survivedBlockers2, survivedAndKillBlocker, diedBlockers);
|
||||||
|
blockedCount++;
|
||||||
|
continue; // try to find next required blocker
|
||||||
|
} else {
|
||||||
|
// invalid configuration, must stop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no more active restrictions
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no more blockers to use
|
||||||
if (blockers.isEmpty()) {
|
if (blockers.isEmpty()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -171,40 +255,43 @@ public final class CombatUtil {
|
||||||
return combatInfo;
|
return combatInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Permanent> getBlockersThatWillSurvive(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
|
|
||||||
List<Permanent> blockers = new ArrayList<>();
|
|
||||||
for (Permanent blocker : possibleBlockers) {
|
|
||||||
SurviveInfo info = willItSurvive(game, attackerId, defenderId, attacker, blocker);
|
|
||||||
//if (info.isAttackerDied() && !info.isBlockerDied()) {
|
|
||||||
if (info != null) {
|
|
||||||
if (info.isAttackerDied()) {
|
|
||||||
blockers.add(blocker);
|
|
||||||
} else if (!info.isBlockerDied()) {
|
|
||||||
blockers.add(blocker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return blockers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated TODO: unused, can be deleted?
|
* Game simulations to find all survived/killer blocker
|
||||||
*/
|
*/
|
||||||
public static SurviveInfo willItSurvive(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
private static List<SurviveInfo> getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
|
||||||
Game sim = game.createSimulationForAI();
|
List<SurviveInfo> res = new ArrayList<>();
|
||||||
|
for (Permanent blocker : possibleBlockers) {
|
||||||
// TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?)
|
// TODO: enable willItSurviveSimulation and check stability
|
||||||
Combat combat = sim.getCombat();
|
SurviveInfo info = willItSurviveSimple(game, attackerId, defenderId, attacker, blocker);
|
||||||
combat.setAttacker(attackingPlayerId);
|
if (info == null) {
|
||||||
combat.setDefenders(sim);
|
continue;
|
||||||
|
}
|
||||||
|
info.setBlocker(blocker);
|
||||||
|
res.add(info);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SurviveInfo willItSurviveSimulation(Game originalGame, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
||||||
|
Game sim = originalGame.createSimulationForAI();
|
||||||
if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) {
|
if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: need code research, possible bugs in miss prepare code due real combat logic
|
||||||
|
// TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?)
|
||||||
|
Combat combat = sim.getCombat();
|
||||||
|
combat.setAttacker(attackingPlayerId);
|
||||||
|
combat.setDefenders(sim);
|
||||||
|
int startScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim).getTotalScore();
|
||||||
|
|
||||||
|
// real game simulation
|
||||||
|
// TODO: need debug and testing, old code from 2012
|
||||||
|
// must have infinite/freeze protection (e.g. limit stack resolves)
|
||||||
|
|
||||||
|
// declare
|
||||||
sim.getPlayer(defendingPlayerId).declareBlocker(defendingPlayerId, blocker.getId(), attacker.getId(), sim);
|
sim.getPlayer(defendingPlayerId).declareBlocker(defendingPlayerId, blocker.getId(), attacker.getId(), sim);
|
||||||
sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defendingPlayerId, defendingPlayerId));
|
sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defendingPlayerId, defendingPlayerId));
|
||||||
|
|
||||||
sim.checkStateAndTriggered();
|
sim.checkStateAndTriggered();
|
||||||
while (!sim.getStack().isEmpty()) {
|
while (!sim.getStack().isEmpty()) {
|
||||||
sim.getStack().resolve(sim);
|
sim.getStack().resolve(sim);
|
||||||
|
|
@ -212,101 +299,45 @@ public final class CombatUtil {
|
||||||
}
|
}
|
||||||
sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId()));
|
sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId()));
|
||||||
|
|
||||||
|
// combat
|
||||||
simulateStep(sim, new CombatDamageStep(true));
|
simulateStep(sim, new CombatDamageStep(true));
|
||||||
simulateStep(sim, new CombatDamageStep(false));
|
simulateStep(sim, new CombatDamageStep(false));
|
||||||
simulateStep(sim, new EndOfCombatStep());
|
simulateStep(sim, new EndOfCombatStep());
|
||||||
// The following commented out call produces random freezes.
|
|
||||||
//sim.checkStateAndTriggered();
|
// after
|
||||||
|
sim.checkStateAndTriggered();
|
||||||
while (!sim.getStack().isEmpty()) {
|
while (!sim.getStack().isEmpty()) {
|
||||||
sim.getStack().resolve(sim);
|
sim.getStack().resolve(sim);
|
||||||
sim.applyEffects();
|
sim.applyEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SurviveInfo(!sim.getBattlefield().containsPermanent(attacker.getId()), !sim.getBattlefield().containsPermanent(blocker.getId()));
|
int endBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim).getTotalScore();
|
||||||
|
int endNonBlockingScore = startScore; // TODO: implement
|
||||||
|
return new SurviveInfo(
|
||||||
|
!sim.getBattlefield().containsPermanent(attacker.getId()),
|
||||||
|
!sim.getBattlefield().containsPermanent(blocker.getId()),
|
||||||
|
endBlockingScore - startScore,
|
||||||
|
endNonBlockingScore - startScore
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static void simulateStep(Game game, Step step) {
|
public static SurviveInfo willItSurviveSimple(Game originalGame, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
||||||
game.getPhase().setStep(step);
|
Game sim = originalGame.createSimulationForAI();
|
||||||
if (!step.skipStep(game, game.getActivePlayerId())) {
|
|
||||||
step.beginStep(game, game.getActivePlayerId());
|
|
||||||
// The following commented out call produces random freezes.
|
|
||||||
//game.checkStateAndTriggered();
|
|
||||||
while (!game.getStack().isEmpty()) {
|
|
||||||
game.getStack().resolve(game);
|
|
||||||
game.applyEffects();
|
|
||||||
}
|
|
||||||
step.endStep(game, game.getActivePlayerId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean canBlock(Game game, Permanent blocker) {
|
|
||||||
boolean canBlock = true;
|
|
||||||
if (!blocker.isTapped()) {
|
|
||||||
try {
|
|
||||||
canBlock = blocker.canBlock(null, game);
|
|
||||||
} catch (Exception e) {
|
|
||||||
//e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return canBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CombatInfo blockWithGoodTrade2(Game game, List<Permanent> attackers, List<Permanent> blockers) {
|
|
||||||
|
|
||||||
UUID attackerId = game.getCombat().getAttackingPlayerId();
|
|
||||||
UUID defenderId = game.getCombat().getDefenders().iterator().next();
|
|
||||||
if (attackerId == null || defenderId == null) {
|
|
||||||
log.warn("Couldn't find attacker or defender: " + attackerId + ' ' + defenderId);
|
|
||||||
return new CombatInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
CombatInfo combatInfo = new CombatInfo();
|
|
||||||
for (Permanent attacker : attackers) {
|
|
||||||
//TODO: handle attackers with "can't be blocked except"
|
|
||||||
List<Permanent> possibleBlockers = getPossibleBlockers(game, attacker, blockers);
|
|
||||||
List<Permanent> survivedBlockers = getBlockersThatWillSurvive2(game, attackerId, defenderId, attacker, possibleBlockers);
|
|
||||||
if (!survivedBlockers.isEmpty()) {
|
|
||||||
Permanent blocker = getWorstCreature(survivedBlockers);
|
|
||||||
combatInfo.addPair(attacker, blocker);
|
|
||||||
blockers.remove(blocker);
|
|
||||||
}
|
|
||||||
if (blockers.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return combatInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Permanent> getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
|
|
||||||
List<Permanent> blockers = new ArrayList<>();
|
|
||||||
for (Permanent blocker : possibleBlockers) {
|
|
||||||
SurviveInfo info = willItSurvive2(game, attackerId, defenderId, attacker, blocker);
|
|
||||||
//if (info.isAttackerDied() && !info.isBlockerDied()) {
|
|
||||||
if (info != null) {
|
|
||||||
if (info.isAttackerDied()) {
|
|
||||||
blockers.add(blocker);
|
|
||||||
} else if (!info.isBlockerDied()) {
|
|
||||||
blockers.add(blocker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return blockers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SurviveInfo willItSurvive2(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
|
||||||
|
|
||||||
Game sim = game.createSimulationForAI();
|
|
||||||
|
|
||||||
// TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?)
|
|
||||||
Combat combat = sim.getCombat();
|
|
||||||
combat.setAttacker(attackingPlayerId);
|
|
||||||
combat.setDefenders(sim);
|
|
||||||
|
|
||||||
if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) {
|
if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Combat combat = sim.getCombat();
|
||||||
|
combat.setAttacker(attackingPlayerId);
|
||||||
|
combat.setDefenders(sim);
|
||||||
|
|
||||||
|
Game simNonBlocking = sim.copy();
|
||||||
|
|
||||||
|
// attacker tapped before attack, it will add additional score to blocker, but it must be ignored
|
||||||
|
// so blocker will block same creature with same score without penalty
|
||||||
|
int startScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore();
|
||||||
|
|
||||||
|
// fake game simulation (manual PT calculation)
|
||||||
if (attacker.getPower().getValue() >= blocker.getToughness().getValue()) {
|
if (attacker.getPower().getValue() >= blocker.getToughness().getValue()) {
|
||||||
sim.getBattlefield().removePermanent(blocker.getId());
|
sim.getBattlefield().removePermanent(blocker.getId());
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +345,38 @@ public final class CombatUtil {
|
||||||
sim.getBattlefield().removePermanent(attacker.getId());
|
sim.getBattlefield().removePermanent(attacker.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SurviveInfo(!sim.getBattlefield().containsPermanent(attacker.getId()), !sim.getBattlefield().containsPermanent(blocker.getId()));
|
// fake non-block simulation
|
||||||
|
simNonBlocking.getPlayer(defendingPlayerId).damage(
|
||||||
|
attacker.getPower().getValue(),
|
||||||
|
attacker.getId(),
|
||||||
|
null,
|
||||||
|
simNonBlocking,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
simNonBlocking.processAction();
|
||||||
|
|
||||||
|
int endBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore();
|
||||||
|
int endNonBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, simNonBlocking, false).getTotalScore();
|
||||||
|
return new SurviveInfo(
|
||||||
|
!sim.getBattlefield().containsPermanent(attacker.getId()),
|
||||||
|
!sim.getBattlefield().containsPermanent(blocker.getId()),
|
||||||
|
endBlockingScore - startScore,
|
||||||
|
endNonBlockingScore - startScore
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void simulateStep(Game sim, Step step) {
|
||||||
|
sim.getPhase().setStep(step);
|
||||||
|
if (!step.skipStep(sim, sim.getActivePlayerId())) {
|
||||||
|
step.beginStep(sim, sim.getActivePlayerId());
|
||||||
|
// The following commented out call produces random freezes.
|
||||||
|
//game.checkStateAndTriggered();
|
||||||
|
while (!sim.getStack().isEmpty()) {
|
||||||
|
sim.getStack().resolve(sim);
|
||||||
|
sim.applyEffects();
|
||||||
|
}
|
||||||
|
step.endStep(sim, sim.getActivePlayerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,48 @@
|
||||||
package mage.player.ai.util;
|
package mage.player.ai.util;
|
||||||
|
|
||||||
import mage.players.Player;
|
import mage.game.permanent.Permanent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author noxx
|
* AI: combat simulation result
|
||||||
|
*
|
||||||
|
* @author noxx, JayDi85
|
||||||
*/
|
*/
|
||||||
public class SurviveInfo {
|
public class SurviveInfo {
|
||||||
private boolean attackerDied;
|
private final boolean attackerDied;
|
||||||
private boolean blockerDied;
|
private final boolean blockerDied;
|
||||||
|
private final int diffBlockingScore;
|
||||||
|
private final int diffNonblockingScore;
|
||||||
|
|
||||||
private Player defender;
|
private Permanent blocker; // for final result
|
||||||
private boolean triggered;
|
|
||||||
|
|
||||||
public SurviveInfo(boolean attackerDied, boolean blockerDied, Player defender, boolean triggered) {
|
public SurviveInfo(boolean attackerDied, boolean blockerDied, int diffBlockingScore, int diffNonblockingScore) {
|
||||||
this.attackerDied = attackerDied;
|
this.attackerDied = attackerDied;
|
||||||
this.blockerDied = blockerDied;
|
this.blockerDied = blockerDied;
|
||||||
this.defender = defender;
|
this.diffBlockingScore = diffBlockingScore;
|
||||||
this.triggered = triggered;
|
this.diffNonblockingScore = diffNonblockingScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SurviveInfo(boolean attackerDied, boolean blockerDied) {
|
public void setBlocker(Permanent blocker) {
|
||||||
this.attackerDied = attackerDied;
|
this.blocker = blocker;
|
||||||
this.blockerDied = blockerDied;
|
}
|
||||||
|
|
||||||
|
public Permanent getBlocker() {
|
||||||
|
return this.blocker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDiffBlockingScore() {
|
||||||
|
return this.diffBlockingScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDiffNonblockingScore() {
|
||||||
|
return this.diffNonblockingScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAttackerDied() {
|
public boolean isAttackerDied() {
|
||||||
return attackerDied;
|
return attackerDied;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAttackerDied(boolean attackerDied) {
|
|
||||||
this.attackerDied = attackerDied;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isBlockerDied() {
|
public boolean isBlockerDied() {
|
||||||
return blockerDied;
|
return blockerDied;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBlockerDied(boolean blockerDied) {
|
|
||||||
this.blockerDied = blockerDied;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Player getDefender() {
|
|
||||||
return defender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTriggered() {
|
|
||||||
return triggered;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_Block_1_small_attacker_vs_1_small_blocker() {
|
public void test_Block_1_small_attacker_vs_1_small_blocker_same() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
|
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
|
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
|
||||||
|
|
||||||
|
|
@ -72,6 +72,54 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
|
||||||
assertGraveyardCount(playerB, "Arbor Elf", 1);
|
assertGraveyardCount(playerB, "Arbor Elf", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_Block_1_small_attacker_vs_1_small_blocker_better() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
|
||||||
|
//addCard(Zone.BATTLEFIELD, playerB, "Elvish Archers"); // 2/1 first strike
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1
|
||||||
|
|
||||||
|
attack(1, playerA, "Arbor Elf");
|
||||||
|
|
||||||
|
// ai must ignore block to keep better creature alive
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
|
checkBlockers("no blockers", 1, playerB, "");
|
||||||
|
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerA, 20);
|
||||||
|
assertLife(playerB, 20 - 1);
|
||||||
|
assertPermanentCount(playerA, "Arbor Elf", 1);
|
||||||
|
assertPermanentCount(playerB, "Dregscape Zombie", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_Block_1_small_attacker_vs_1_small_blocker_better_but_player_die() {
|
||||||
|
addCustomEffect_TargetDamage(playerA, 19);
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1
|
||||||
|
|
||||||
|
// prepare 1 life
|
||||||
|
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target damage 19", playerB);
|
||||||
|
|
||||||
|
attack(1, playerA, "Arbor Elf");
|
||||||
|
|
||||||
|
// ai must keep better blocker in normal case, but now it must protect from lose and sacrifice it
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
|
checkBlockers("x1 blocker", 1, playerB, "Dregscape Zombie");
|
||||||
|
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerA, 20);
|
||||||
|
assertLife(playerB, 1);
|
||||||
|
assertGraveyardCount(playerA, "Arbor Elf", 1);
|
||||||
|
assertGraveyardCount(playerB, "Dregscape Zombie", 1);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_Block_1_big_attacker_vs_1_small_blocker() {
|
public void test_Block_1_big_attacker_vs_1_small_blocker() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
|
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_MustBeBlocked_nothing() {
|
public void test_MustBeBlocked_nothing_human() {
|
||||||
// Fear of Being Hunted must be blocked if able.
|
// Fear of Being Hunted must be blocked if able.
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
||||||
|
|
||||||
|
|
@ -587,12 +587,30 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_MustBeBlocked_1_blocker() {
|
public void test_MustBeBlocked_nothing_AI() {
|
||||||
|
// Fear of Being Hunted must be blocked if able.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
||||||
|
|
||||||
|
attack(1, playerA, "Fear of Being Hunted");
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
|
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
||||||
|
checkBlockers("no blocker", 1, playerB, "");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 20 - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_MustBeBlocked_1_blocker_human() {
|
||||||
// Fear of Being Hunted must be blocked if able.
|
// Fear of Being Hunted must be blocked if able.
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
||||||
//
|
//
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
|
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
|
||||||
|
|
||||||
|
// auto-choose blocker
|
||||||
attack(1, playerA, "Fear of Being Hunted");
|
attack(1, playerA, "Fear of Being Hunted");
|
||||||
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
||||||
checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
|
checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
|
||||||
|
|
@ -606,15 +624,36 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_MustBeBlocked_many_blockers_good() {
|
public void test_MustBeBlocked_1_blocker_AI() {
|
||||||
|
// Fear of Being Hunted must be blocked if able.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
||||||
|
//
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
|
||||||
|
|
||||||
|
// auto-choose blocker with AI
|
||||||
|
attack(1, playerA, "Fear of Being Hunted");
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
|
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
||||||
|
checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 20);
|
||||||
|
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_MustBeBlocked_many_blockers_good_AI() {
|
||||||
// Fear of Being Hunted must be blocked if able.
|
// Fear of Being Hunted must be blocked if able.
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
||||||
//
|
//
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 10); // 3/3
|
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 10); // 3/3
|
||||||
|
|
||||||
// TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
|
// ai must choose any bear
|
||||||
// take first available blocker
|
|
||||||
attack(1, playerA, "Fear of Being Hunted");
|
attack(1, playerA, "Fear of Being Hunted");
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
||||||
checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears");
|
checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears");
|
||||||
|
|
||||||
|
|
@ -627,15 +666,15 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_MustBeBlocked_many_blockers_bad() {
|
public void test_MustBeBlocked_many_blockers_bad_AI() {
|
||||||
// Fear of Being Hunted must be blocked if able.
|
// Fear of Being Hunted must be blocked if able.
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
||||||
//
|
//
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
|
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
|
||||||
|
|
||||||
// TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
|
// ai don't want but must choose any bad memnite
|
||||||
// take first available blocker
|
|
||||||
attack(1, playerA, "Fear of Being Hunted");
|
attack(1, playerA, "Fear of Being Hunted");
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
||||||
checkBlockers("x1 optimal blocker", 1, playerB, "Memnite");
|
checkBlockers("x1 optimal blocker", 1, playerB, "Memnite");
|
||||||
|
|
||||||
|
|
@ -645,12 +684,11 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
assertLife(playerB, 20);
|
assertLife(playerB, 20);
|
||||||
assertPermanentCount(playerA, "Fear of Being Hunted", 1);
|
assertPermanentCount(playerA, "Fear of Being Hunted", 1);
|
||||||
|
assertGraveyardCount(playerB, "Memnite", 1); // x1 blocker die
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore
|
public void test_MustBeBlocked_many_blockers_optimal_AI() {
|
||||||
// TODO: enable and duplicate for AI -- after implement choose blocker logic and isHuman replace by ~isComputer
|
|
||||||
public void test_MustBeBlocked_many_blockers_optimal() {
|
|
||||||
// Fear of Being Hunted must be blocked if able.
|
// Fear of Being Hunted must be blocked if able.
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
|
||||||
//
|
//
|
||||||
|
|
@ -659,7 +697,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
|
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
|
addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
|
||||||
|
|
||||||
|
// ai must choose optimal creature to kill but survive
|
||||||
attack(1, playerA, "Fear of Being Hunted");
|
attack(1, playerA, "Fear of Being Hunted");
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
|
||||||
checkBlockers("x1 optimal blocker", 1, playerB, "Deadbridge Goliath");
|
checkBlockers("x1 optimal blocker", 1, playerB, "Deadbridge Goliath");
|
||||||
|
|
||||||
|
|
@ -669,6 +709,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
assertLife(playerB, 20);
|
assertLife(playerB, 20);
|
||||||
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
|
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
|
||||||
|
assertGraveyardCount(playerB, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -697,7 +738,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_MustBlocking_full_blockers() {
|
public void test_MustBlocking_full_blockers_human() {
|
||||||
// All creatures able to block target creature this turn do so.
|
// All creatures able to block target creature this turn do so.
|
||||||
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
|
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
|
||||||
|
|
@ -725,7 +766,37 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_MustBlocking_many_blockers() {
|
public void test_MustBlocking_full_blockers_AI() {
|
||||||
|
// All creatures able to block target creature this turn do so.
|
||||||
|
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
|
||||||
|
//
|
||||||
|
// Menace
|
||||||
|
// Each creature you control with menace can't be blocked except by three or more creatures.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
|
||||||
|
//
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 3); // 1/1
|
||||||
|
|
||||||
|
// prepare
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
|
||||||
|
|
||||||
|
// 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
|
||||||
|
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
|
||||||
|
checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 20);
|
||||||
|
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_MustBlocking_many_blockers_human() {
|
||||||
// possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
|
// possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
|
||||||
|
|
||||||
// All creatures able to block target creature this turn do so.
|
// All creatures able to block target creature this turn do so.
|
||||||
|
|
@ -754,6 +825,38 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
|
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_MustBlocking_many_blockers_AI() {
|
||||||
|
// possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
|
||||||
|
|
||||||
|
// All creatures able to block target creature this turn do so.
|
||||||
|
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
|
||||||
|
//
|
||||||
|
// Menace
|
||||||
|
// Each creature you control with menace can't be blocked except by three or more creatures.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
|
||||||
|
//
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 5); // 1/1
|
||||||
|
|
||||||
|
// prepare
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
|
||||||
|
|
||||||
|
// 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
|
||||||
|
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
|
||||||
|
checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 20);
|
||||||
|
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore
|
@Ignore
|
||||||
// TODO: need exception fix - java.lang.UnsupportedOperationException: Sonorous Howlbonder is blocked by 1 creature(s). It has to be blocked by 3 or more.
|
// TODO: need exception fix - java.lang.UnsupportedOperationException: Sonorous Howlbonder is blocked by 1 creature(s). It has to be blocked by 3 or more.
|
||||||
|
|
@ -814,6 +917,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
|
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
|
||||||
|
// looks like it's impossible for auto-fix (it's can remove blocker, but not add)
|
||||||
public void test_MustBeBlockedWithMenace_all_blockers() {
|
public void test_MustBeBlockedWithMenace_all_blockers() {
|
||||||
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
|
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
|
||||||
// power until end of turn. That creature must be blocked this combat if able.
|
// power until end of turn. That creature must be blocked this combat if able.
|
||||||
|
|
@ -844,7 +948,8 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
|
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
|
||||||
public void test_MustBeBlockedWithMenace_many_blockers() {
|
// looks like it's impossible for auto-fix (it's can remove blocker, but not add)
|
||||||
|
public void test_MustBeBlockedWithMenace_many_low_blockers_human() {
|
||||||
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
|
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
|
||||||
// power until end of turn. That creature must be blocked this combat if able.
|
// power until end of turn. That creature must be blocked this combat if able.
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
|
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
|
||||||
|
|
@ -872,6 +977,42 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
assertGraveyardCount(playerA, "Alley Strangler", 0);
|
assertGraveyardCount(playerA, "Alley Strangler", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore // TODO: blockWithGoodTrade2 must support additional restrictions
|
||||||
|
public void test_MustBeBlockedWithMenace_many_low_blockers_AI() {
|
||||||
|
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
|
||||||
|
// power until end of turn. That creature must be blocked this combat if able.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||||
|
//
|
||||||
|
// Menace
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
|
||||||
|
//
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
|
||||||
|
|
||||||
|
// If the target creature has menace, two creatures must block it if able.
|
||||||
|
// (2020-06-23)
|
||||||
|
|
||||||
|
// AI must be forced to choose min x2 low blockers (it's possible to fail here after AI logic improve someday)
|
||||||
|
|
||||||
|
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
|
||||||
|
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
|
||||||
|
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
|
||||||
|
checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 20);
|
||||||
|
assertGraveyardCount(playerA, "Alley Strangler", 0);
|
||||||
|
assertGraveyardCount(playerB, "Memnite", 2); // x2 blockers must die
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_MustBeBlockedWithMenace_low_blockers_auto() {
|
public void test_MustBeBlockedWithMenace_low_blockers_auto() {
|
||||||
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
|
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creature’s
|
||||||
|
|
@ -985,7 +1126,6 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // TODO: need to fix
|
|
||||||
public void test_MustBeBlockedWithMenace_low_big_blockers_AI() {
|
public void test_MustBeBlockedWithMenace_low_big_blockers_AI() {
|
||||||
// bug: #13290, AI can try to use bigger creature to block
|
// bug: #13290, AI can try to use bigger creature to block
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1996,7 +1996,7 @@ public class TestPlayer implements Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkMultipleBlockers(game, blockedCreaturesList);
|
checkMultipleBlockers(game, blockedCreaturesList); // search wrong block commands
|
||||||
|
|
||||||
// AI FULL play if no actions available
|
// AI FULL play if no actions available
|
||||||
if (!mustBlockByAction && (this.AIPlayer || this.AIRealGameSimulation)) {
|
if (!mustBlockByAction && (this.AIPlayer || this.AIRealGameSimulation)) {
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
||||||
*
|
*
|
||||||
* Warning, it will return leaved players until end of turn. For dialogs and one shot effects use excludeLeavedPlayers
|
* Warning, it will return leaved players until end of turn. For dialogs and one shot effects use excludeLeavedPlayers
|
||||||
*/
|
*/
|
||||||
// TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers
|
// TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers, see #13289
|
||||||
default Set<UUID> getOpponents(UUID playerId) {
|
default Set<UUID> getOpponents(UUID playerId) {
|
||||||
return getOpponents(playerId, false);
|
return getOpponents(playerId, false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3174,7 +3174,7 @@ public abstract class GameImpl implements Game {
|
||||||
if (simulation) {
|
if (simulation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.INFO, message, this);
|
tableEventSource.fireTableEvent(EventType.INFO, message, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3183,7 +3183,7 @@ public abstract class GameImpl implements Game {
|
||||||
if (simulation) {
|
if (simulation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.STATUS, message, withTime, withTurnInfo, this);
|
tableEventSource.fireTableEvent(EventType.STATUS, message, withTime, withTurnInfo, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3192,7 +3192,7 @@ public abstract class GameImpl implements Game {
|
||||||
if (simulation) {
|
if (simulation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.UPDATE, null, this);
|
tableEventSource.fireTableEvent(EventType.UPDATE, null, this);
|
||||||
getState().clearLookedAt();
|
getState().clearLookedAt();
|
||||||
getState().clearRevealed();
|
getState().clearRevealed();
|
||||||
|
|
@ -3203,23 +3203,23 @@ public abstract class GameImpl implements Game {
|
||||||
if (simulation) {
|
if (simulation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.END_GAME_INFO, null, this);
|
tableEventSource.fireTableEvent(EventType.END_GAME_INFO, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fireErrorEvent(String message, Exception ex) {
|
public void fireErrorEvent(String message, Exception ex) {
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.ERROR, message, ex, this);
|
tableEventSource.fireTableEvent(EventType.ERROR, message, ex, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void makeSureCalledOutsideLayersEffects() {
|
private void makeSureCalledOutsideLayerEffects() {
|
||||||
// very slow, enable/comment it for debug or load/stability tests only
|
// very slow, enable/comment it for debug or load/stability tests only
|
||||||
// TODO: enable check and remove/rework all wrong usages
|
// TODO: enable check and remove/rework all wrong usages
|
||||||
if (true) return;
|
if (true) return;
|
||||||
Arrays.stream(Thread.currentThread().getStackTrace()).forEach(e -> {
|
Arrays.stream(Thread.currentThread().getStackTrace()).forEach(e -> {
|
||||||
if (e.toString().contains("GameState.applyEffects")) {
|
if (e.toString().contains("GameState.applyEffects")) {
|
||||||
throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?");
|
throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?)");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -3890,7 +3890,7 @@ public abstract class GameImpl implements Game {
|
||||||
@Override
|
@Override
|
||||||
public void initTimer(UUID playerId) {
|
public void initTimer(UUID playerId) {
|
||||||
if (priorityTime > 0) {
|
if (priorityTime > 0) {
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.INIT_TIMER, playerId, null, this);
|
tableEventSource.fireTableEvent(EventType.INIT_TIMER, playerId, null, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3898,7 +3898,7 @@ public abstract class GameImpl implements Game {
|
||||||
@Override
|
@Override
|
||||||
public void resumeTimer(UUID playerId) {
|
public void resumeTimer(UUID playerId) {
|
||||||
if (priorityTime > 0) {
|
if (priorityTime > 0) {
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.RESUME_TIMER, playerId, null, this);
|
tableEventSource.fireTableEvent(EventType.RESUME_TIMER, playerId, null, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3906,7 +3906,7 @@ public abstract class GameImpl implements Game {
|
||||||
@Override
|
@Override
|
||||||
public void pauseTimer(UUID playerId) {
|
public void pauseTimer(UUID playerId) {
|
||||||
if (priorityTime > 0) {
|
if (priorityTime > 0) {
|
||||||
makeSureCalledOutsideLayersEffects();
|
makeSureCalledOutsideLayerEffects();
|
||||||
tableEventSource.fireTableEvent(EventType.PAUSE_TIMER, playerId, null, this);
|
tableEventSource.fireTableEvent(EventType.PAUSE_TIMER, playerId, null, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -670,9 +670,25 @@ public class Combat implements Serializable, Copyable<Combat> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// choosing until good block configuration
|
// choosing until good block configuration
|
||||||
|
int aiTries = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
aiTries++;
|
||||||
|
|
||||||
|
if (controller.isComputer() && aiTries > 20) {
|
||||||
|
// TODO: AI must use real attacker/blocker configuration with all possible combination
|
||||||
|
// (current human like logic will fail sometime, e.g. with menace and big/low creatures)
|
||||||
|
// real game: send warning
|
||||||
|
// test: fast fail
|
||||||
|
game.informPlayers(controller.getLogName() + ": WARNING - AI can't find good blocker combination and will skip it - report your battlefield to github - " + game.getCombat());
|
||||||
|
if (controller.isTestsMode()) {
|
||||||
|
// how-to fix: AI code must support failed abilities or use cases
|
||||||
|
throw new IllegalArgumentException("AI can't find good blocker combination");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// declare normal blockers
|
// declare normal blockers
|
||||||
// TODO: need reseach - is it possible to concede on bad blocker configuration (e.g. user can't continue)
|
// TODO: need research - is it possible to concede on bad blocker configuration (e.g. user can't continue)
|
||||||
controller.selectBlockers(source, game, defenderId);
|
controller.selectBlockers(source, game, defenderId);
|
||||||
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
|
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -776,18 +792,16 @@ public class Combat implements Serializable, Copyable<Combat> {
|
||||||
/**
|
/**
|
||||||
* Check the block restrictions
|
* Check the block restrictions
|
||||||
*
|
*
|
||||||
* @param player
|
|
||||||
* @param game
|
|
||||||
* @return false - if block restrictions were not complied
|
* @return false - if block restrictions were not complied
|
||||||
*/
|
*/
|
||||||
public boolean checkBlockRestrictions(Player player, Game game) {
|
public boolean checkBlockRestrictions(Player defender, Game game) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
boolean blockWasLegal = true;
|
boolean blockWasLegal = true;
|
||||||
for (CombatGroup group : groups) {
|
for (CombatGroup group : groups) {
|
||||||
count += group.getBlockers().size();
|
count += group.getBlockers().size();
|
||||||
}
|
}
|
||||||
for (CombatGroup group : groups) {
|
for (CombatGroup group : groups) {
|
||||||
blockWasLegal &= group.checkBlockRestrictions(game, count);
|
blockWasLegal &= group.checkBlockRestrictions(game, defender, count);
|
||||||
}
|
}
|
||||||
return blockWasLegal;
|
return blockWasLegal;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -773,11 +773,27 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkBlockRestrictions(Game game, int blockersCount) {
|
public boolean checkBlockRestrictions(Game game, Player defender, int blockersCount) {
|
||||||
boolean blockWasLegal = true;
|
boolean blockWasLegal = true;
|
||||||
if (attackers.isEmpty()) {
|
if (attackers.isEmpty()) {
|
||||||
return blockWasLegal;
|
return blockWasLegal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collect possible blockers
|
||||||
|
Map<UUID, Set<UUID>> possibleBlockers = new HashMap<>();
|
||||||
|
for (UUID attackerId : attackers) {
|
||||||
|
Permanent attacker = game.getPermanent(attackerId);
|
||||||
|
Set<UUID> goodBlockers = new HashSet<>();
|
||||||
|
for (Permanent blocker : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, defender.getId(), game)) {
|
||||||
|
if (blocker.canBlock(attackerId, game)) {
|
||||||
|
goodBlockers.add(blocker.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
possibleBlockers.put(attacker.getId(), goodBlockers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// effects: can't block alone
|
||||||
|
// too much blockers
|
||||||
if (blockersCount == 1) {
|
if (blockersCount == 1) {
|
||||||
List<UUID> toBeRemoved = new ArrayList<>();
|
List<UUID> toBeRemoved = new ArrayList<>();
|
||||||
for (UUID blockerId : getBlockers()) {
|
for (UUID blockerId : getBlockers()) {
|
||||||
|
|
@ -802,19 +818,27 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
for (UUID uuid : attackers) {
|
for (UUID uuid : attackers) {
|
||||||
Permanent attacker = game.getPermanent(uuid);
|
Permanent attacker = game.getPermanent(uuid);
|
||||||
if (attacker != null && this.blocked) {
|
if (attacker != null && this.blocked) {
|
||||||
// Check if there are enough blockers to have a legal block
|
// effects: can't be blocked except by xxx or more creatures
|
||||||
|
// too few blockers
|
||||||
if (attacker.getMinBlockedBy() > 1 && !blockers.isEmpty() && blockers.size() < attacker.getMinBlockedBy()) {
|
if (attacker.getMinBlockedBy() > 1 && !blockers.isEmpty() && blockers.size() < attacker.getMinBlockedBy()) {
|
||||||
for (UUID blockerId : new ArrayList<>(blockers)) {
|
for (UUID blockerId : new ArrayList<>(blockers)) {
|
||||||
game.getCombat().removeBlocker(blockerId, game);
|
game.getCombat().removeBlocker(blockerId, game); // !
|
||||||
}
|
}
|
||||||
blockers.clear();
|
blockers.clear();
|
||||||
blockerOrder.clear();
|
blockerOrder.clear();
|
||||||
if (!game.isSimulation()) {
|
if (!game.isSimulation()) {
|
||||||
game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded.");
|
game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if there aren't any possible blocker configuration then it's legal due mtg rules
|
||||||
|
// warning, it's affect AI related logic like other block auto-fixes does, see https://github.com/magefree/mage/pull/13182
|
||||||
|
if (attacker.getMinBlockedBy() <= possibleBlockers.getOrDefault(attacker.getId(), Collections.emptySet()).size()) {
|
||||||
blockWasLegal = false;
|
blockWasLegal = false;
|
||||||
}
|
}
|
||||||
// Check if there are too many blockers (maxBlockedBy = 0 means no restrictions)
|
}
|
||||||
|
|
||||||
|
// effects: can't be blocked by more than xxx creature
|
||||||
|
// too much blockers
|
||||||
if (attacker.getMaxBlockedBy() > 0 && attacker.getMaxBlockedBy() < blockers.size()) {
|
if (attacker.getMaxBlockedBy() > 0 && attacker.getMaxBlockedBy() < blockers.size()) {
|
||||||
for (UUID blockerId : new ArrayList<>(blockers)) {
|
for (UUID blockerId : new ArrayList<>(blockers)) {
|
||||||
game.getCombat().removeBlocker(blockerId, game);
|
game.getCombat().removeBlocker(blockerId, game);
|
||||||
|
|
@ -827,6 +851,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
.append(attacker.getMaxBlockedBy() == 1 ? " creature." : " creatures.")
|
.append(attacker.getMaxBlockedBy() == 1 ? " creature." : " creatures.")
|
||||||
.append(" Blockers discarded.").toString());
|
.append(" Blockers discarded.").toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
blockWasLegal = false;
|
blockWasLegal = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue