[Simulated AI] Smart decisions about whether to play spells before or after blocks.

For example, using a doom blade before blocks to attack for lethal is preferred vs using a pump spell after blocks to pump an unblocked creature for lethal damage. Adds relevant tests.
This commit is contained in:
Myrd
2017-02-26 21:22:10 +00:00
parent 35ad14aba4
commit 0527f4cb44
11 changed files with 347 additions and 119 deletions

View File

@@ -24,6 +24,7 @@ import forge.game.card.CardFactoryUtil;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.RegisteredPlayer; import forge.game.player.RegisteredPlayer;
import forge.game.spellability.AbilityActivated; import forge.game.spellability.AbilityActivated;
@@ -64,6 +65,9 @@ public class GameCopier {
} }
public Game makeCopy() { public Game makeCopy() {
return makeCopy(null);
}
public Game makeCopy(PhaseType advanceToPhase) {
List<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers(); List<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers();
List<RegisteredPlayer> newPlayers = new ArrayList<>(); List<RegisteredPlayer> newPlayers = new ArrayList<>();
for (RegisteredPlayer p : origPlayers) { for (RegisteredPlayer p : origPlayers) {
@@ -148,6 +152,10 @@ public class GameCopier {
newGame.getPhaseHandler().setCombat(new Combat(origPhaseHandler.getCombat(), gameObjectMap)); newGame.getPhaseHandler().setCombat(new Combat(origPhaseHandler.getCombat(), gameObjectMap));
} }
if (advanceToPhase != null) {
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase);
}
return newGame; return newGame;
} }

View File

@@ -12,6 +12,7 @@ import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.Game; import forge.game.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices; import forge.game.spellability.TargetChoices;
@@ -28,10 +29,10 @@ public class GameSimulator {
private Score origScore; private Score origScore;
private SpellAbilityChoicesIterator interceptor; private SpellAbilityChoicesIterator interceptor;
public GameSimulator(final SimulationController controller, final Game origGame, final Player origAiPlayer) { public GameSimulator(SimulationController controller, Game origGame, Player origAiPlayer, PhaseType advanceToPhase) {
this.controller = controller; this.controller = controller;
copier = new GameCopier(origGame); copier = new GameCopier(origGame);
simGame = copier.makeCopy(); simGame = copier.makeCopy(advanceToPhase);
aiPlayer = (Player) copier.find(origAiPlayer); aiPlayer = (Player) copier.find(origAiPlayer);
eval = new GameStateEvaluator(); eval = new GameStateEvaluator();
@@ -42,20 +43,9 @@ public class GameSimulator {
debugPrint = false; debugPrint = false;
origScore = eval.getScoreForGameState(origGame, origAiPlayer); origScore = eval.getScoreForGameState(origGame, origAiPlayer);
eval.setDebugging(true); if (advanceToPhase == null) {
List<String> simLines = new ArrayList<String>(); ensureGameCopyScoreMatches(origGame, origAiPlayer);
debugLines = simLines;
Score simScore = eval.getScoreForGameState(simGame, aiPlayer);
if (!simScore.equals(origScore)) {
// Re-eval orig with debug printing.
origLines = new ArrayList<String>();
debugLines = origLines;
eval.getScoreForGameState(origGame, origAiPlayer);
// Print debug info.
printDiff(origLines, simLines);
throw new RuntimeException("Game copy error. See diff output above for details.");
} }
eval.setDebugging(false);
// If the stack on the original game is not empty, resolve it // If the stack on the original game is not empty, resolve it
// first and get the updated eval score, since this is what we'll // first and get the updated eval score, since this is what we'll
@@ -73,6 +63,23 @@ public class GameSimulator {
debugLines = null; debugLines = null;
} }
private void ensureGameCopyScoreMatches(Game origGame, Player origAiPlayer) {
eval.setDebugging(true);
List<String> simLines = new ArrayList<String>();
debugLines = simLines;
Score simScore = eval.getScoreForGameState(simGame, aiPlayer);
if (!simScore.equals(origScore)) {
// Re-eval orig with debug printing.
origLines = new ArrayList<String>();
debugLines = origLines;
eval.getScoreForGameState(origGame, origAiPlayer);
// Print debug info.
printDiff(origLines, simLines);
throw new RuntimeException("Game copy error. See diff output above for details.");
}
eval.setDebugging(false);
}
public void setInterceptor(SpellAbilityChoicesIterator interceptor) { public void setInterceptor(SpellAbilityChoicesIterator interceptor) {
this.interceptor = interceptor; this.interceptor = interceptor;
((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor); ((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor);

View File

@@ -1,19 +1,16 @@
package forge.ai.simulation; package forge.ai.simulation;
import forge.ai.AiAttackController;
import forge.ai.CreatureEvaluator; import forge.ai.CreatureEvaluator;
import forge.game.Game; import forge.game.Game;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
public class GameStateEvaluator { public class GameStateEvaluator {
private boolean debugging = false; private boolean debugging = false;
private boolean ignoreTempBoosts = false;
private SimulationCreatureEvaluator eval = new SimulationCreatureEvaluator(); private SimulationCreatureEvaluator eval = new SimulationCreatureEvaluator();
public void setDebugging(boolean debugging) { public void setDebugging(boolean debugging) {
@@ -21,19 +18,25 @@ public class GameStateEvaluator {
} }
private static void debugPrint(String s) { private static void debugPrint(String s) {
//System.err.println(s);
GameSimulator.debugPrint(s); GameSimulator.debugPrint(s);
} }
private Combat simulateUpcomingCombatThisTurn(Game game) { private static class CombatSimResult {
PhaseHandler handler = game.getPhaseHandler(); public GameCopier copier;
if (handler.getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) { public Game gameCopy;
}
private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame) {
PhaseType phase = evalGame.getPhaseHandler().getPhase();
if (phase.isAfter(PhaseType.COMBAT_DAMAGE) || evalGame.isGameOver()) {
return null; return null;
} }
AiAttackController aiAtk = new AiAttackController(handler.getPlayerTurn()); GameCopier copier = new GameCopier(evalGame);
Combat combat = new Combat(handler.getPlayerTurn()); Game gameCopy = copier.makeCopy();
aiAtk.declareAttackers(combat); gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE);
return combat; CombatSimResult result = new CombatSimResult();
result.copier = copier;
result.gameCopy = gameCopy;
return result;
} }
private static String cardToString(Card c) { private static String cardToString(Card c) {
@@ -44,13 +47,27 @@ public class GameStateEvaluator {
return str; return str;
} }
private Score getScoreForGameOver(Game game, Player aiPlayer) {
return game.getOutcome().getWinningPlayer() == aiPlayer ? new Score(Integer.MAX_VALUE) : new Score(Integer.MIN_VALUE);
}
public Score getScoreForGameState(Game game, Player aiPlayer) { public Score getScoreForGameState(Game game, Player aiPlayer) {
if (game.isGameOver()) { if (game.isGameOver()) {
return game.getOutcome().getWinningPlayer() == aiPlayer ? new Score(Integer.MAX_VALUE) : new Score(Integer.MIN_VALUE); return getScoreForGameOver(game, aiPlayer);
} }
Combat combat = simulateUpcomingCombatThisTurn(game); CombatSimResult result = simulateUpcomingCombatThisTurn(game);
if (result != null) {
Player aiPlayerCopy = (Player) result.copier.find(aiPlayer);
if (result.gameCopy.isGameOver()) {
return getScoreForGameOver(result.gameCopy, aiPlayerCopy);
}
return getScoreForGameStateImpl(result.gameCopy, aiPlayerCopy);
}
return getScoreForGameStateImpl(game, aiPlayer);
}
private Score getScoreForGameStateImpl(Game game, Player aiPlayer) {
int score = 0; int score = 0;
// TODO: more than 2 players // TODO: more than 2 players
int myCards = 0; int myCards = 0;
@@ -85,7 +102,7 @@ public class GameStateEvaluator {
int summonSickScore = score; int summonSickScore = score;
PhaseType gamePhase = game.getPhaseHandler().getPhase(); PhaseType gamePhase = game.getPhaseHandler().getPhase();
for (Card c : game.getCardsIn(ZoneType.Battlefield)) { for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
int value = evalCard(game, aiPlayer, c, combat); int value = evalCard(game, aiPlayer, c);
int summonSickValue = value; int summonSickValue = value;
// To make the AI hold-off on playing creatures before MAIN2 if they give no other benefits, // To make the AI hold-off on playing creatures before MAIN2 if they give no other benefits,
// keep track of the score while treating summon sick creatures as having a value of 0. // keep track of the score while treating summon sick creatures as having a value of 0.
@@ -111,20 +128,10 @@ public class GameStateEvaluator {
return new Score(score, summonSickScore); return new Score(score, summonSickScore);
} }
public int evalCard(Game game, Player aiPlayer, Card c, Combat combat) { public int evalCard(Game game, Player aiPlayer, Card c) {
// TODO: These should be based on other considerations - e.g. in relation to opponents state. // TODO: These should be based on other considerations - e.g. in relation to opponents state.
if (c.isCreature()) { if (c.isCreature()) {
// Ignore temp boosts post combat, since it's a waste. return eval.evaluateCreature(c);
// TODO: Make this smarter. Right now, it only looks if the creature will attack - but
// does not consider things like blocks or the outcome of combat.
// Also, sometimes temp boosts post combat could be useful - e.g. if you then want to make your
// creature fight another, etc.
ignoreTempBoosts = true;
if (combat != null && combat.isAttacking(c)) {
ignoreTempBoosts = false;
}
int result = eval.evaluateCreature(c);
return result;
} else if (c.isLand()) { } else if (c.isLand()) {
return 100; return 100;
} else if (c.isEnchantingCard()) { } else if (c.isEnchantingCard()) {
@@ -153,19 +160,13 @@ public class GameStateEvaluator {
@Override @Override
protected int getEffectivePower(final Card c) { protected int getEffectivePower(final Card c) {
if (ignoreTempBoosts) { Card.StatBreakdown breakdown = c.getNetCombatDamageBreakdown();
Card.StatBreakdown breakdown = c.getNetCombatDamageBreakdown(); return breakdown.getTotal() - breakdown.tempBoost;
return breakdown.getTotal() - breakdown.tempBoost;
}
return c.getNetCombatDamage();
} }
@Override @Override
protected int getEffectiveToughness(final Card c) { protected int getEffectiveToughness(final Card c) {
if (ignoreTempBoosts) { Card.StatBreakdown breakdown = c.getNetToughnessBreakdown();
Card.StatBreakdown breakdown = c.getNetToughnessBreakdown(); return breakdown.getTotal() - breakdown.tempBoost;
return breakdown.getTotal() - breakdown.tempBoost;
}
return c.getNetToughness();
} }
} }

View File

@@ -7,16 +7,28 @@ import com.google.common.base.Joiner;
import forge.ai.simulation.GameStateEvaluator.Score; import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.phase.PhaseType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
public class Plan { public class Plan {
private List<Decision> decisions; private final List<Decision> decisions;
private final Score finalScore;
private int nextDecisionIndex; private int nextDecisionIndex;
private int nextChoice; private int nextChoice;
private Decision selectedDecision; private Decision selectedDecision;
private PhaseType startPhase;
public Plan(ArrayList<Decision> decisions) { public Plan(ArrayList<Decision> decisions, Score finalScore) {
this.decisions = decisions; this.decisions = decisions;
this.finalScore = finalScore;
}
public Score getFinalScore() {
return finalScore;
}
public PhaseType getStartPhase() {
return startPhase;
} }
public List<Decision> getDecisions() { public List<Decision> getDecisions() {

View File

@@ -125,7 +125,7 @@ public class SimulationController {
} }
} }
sequence.subList(writeIndex, sequence.size()).clear(); sequence.subList(writeIndex, sequence.size()).clear();
return new Plan(sequence); return new Plan(sequence, getBestScore());
} }
private Plan.Decision getLastMergedDecision() { private Plan.Decision getLastMergedDecision() {
@@ -221,7 +221,7 @@ public class SimulationController {
if (effect.hostCard == hostAndTarget[0] && effect.target == hostAndTarget[1] && effect.sa.equals(saString)) { if (effect.hostCard == hostAndTarget[0] && effect.target == hostAndTarget[1] && effect.sa.equals(saString)) {
GameStateEvaluator evaluator = new GameStateEvaluator(); GameStateEvaluator evaluator = new GameStateEvaluator();
Player player = sa.getActivatingPlayer(); Player player = sa.getActivatingPlayer();
int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null); int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2]);
if (cardScore == effect.targetScore) { if (cardScore == effect.targetScore) {
Score currentScore = getCurrentScore(); Score currentScore = getCurrentScore();
// TODO: summonSick score? // TODO: summonSick score?
@@ -249,7 +249,7 @@ public class SimulationController {
if (currentHostAndTarget != null) { if (currentHostAndTarget != null) {
GameStateEvaluator evaluator = new GameStateEvaluator(); GameStateEvaluator evaluator = new GameStateEvaluator();
Player player = sa.getActivatingPlayer(); Player player = sa.getActivatingPlayer();
int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null); int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2]);
effectCache.add(new CachedEffect(hostAndTarget[0], sa, hostAndTarget[1], cardScore, scoreDelta)); effectCache.add(new CachedEffect(hostAndTarget[0], sa, hostAndTarget[1], cardScore, scoreDelta));
cached = " (added to cache)"; cached = " (added to cache)";
} }

View File

@@ -1,5 +1,6 @@
package forge.ai.simulation; package forge.ai.simulation;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -57,7 +58,22 @@ public class SpellAbilityPicker {
print("---- choose ability (phase = " + phaseStr + ")"); print("---- choose ability (phase = " + phaseStr + ")");
} }
private List<SpellAbility> getCandidateSpellsAndAbilities(List<SpellAbility> all) { private List<SpellAbility> getCandidateSpellsAndAbilities() {
CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, player);
CardCollection landsToPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
if (landsToPlay != null) {
HashMap<String, Card> landsDeDupe = new HashMap<String, Card>();
for (Card land : landsToPlay) {
Card previousLand = landsDeDupe.get(land.getName());
// Skip identical lands.
if (previousLand != null && previousLand.getZone() == land.getZone() && previousLand.getOwner() == land.getOwner()) {
continue;
}
landsDeDupe.put(land.getName(), land);
all.add(new PlayLandAbility(land));
}
}
List<SpellAbility> candidateSAs = ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player); List<SpellAbility> candidateSAs = ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player);
int writeIndex = 0; int writeIndex = 0;
for (int i = 0; i < candidateSAs.size(); i++) { for (int i = 0; i < candidateSAs.size(); i++) {
@@ -89,28 +105,12 @@ public class SpellAbilityPicker {
return null; return null;
} }
CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, player);
CardCollection landsToPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
if (landsToPlay != null) {
HashMap<String, Card> landsDeDupe = new HashMap<String, Card>();
for (Card land : landsToPlay) {
Card previousLand = landsDeDupe.get(land.getName());
// Skip identical lands.
if (previousLand != null && previousLand.getZone() == land.getZone() && previousLand.getOwner() == land.getOwner()) {
continue;
}
landsDeDupe.put(land.getName(), land);
all.add(new PlayLandAbility(land));
}
}
Score origGameScore = new GameStateEvaluator().getScoreForGameState(game, player); Score origGameScore = new GameStateEvaluator().getScoreForGameState(game, player);
List<SpellAbility> candidateSAs = getCandidateSpellsAndAbilities(all); List<SpellAbility> candidateSAs = getCandidateSpellsAndAbilities();
if (controller != null) { if (controller != null) {
// This is a recursion during a higher-level simulation. Just return the head of the best // This is a recursion during a higher-level simulation. Just return the head of the best
// sequence directly, no need to create a Plan object. // sequence directly, no need to create a Plan object.
return chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore); return chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore, null);
} }
printPhaseInfo(); printPhaseInfo();
@@ -130,31 +130,81 @@ public class SpellAbilityPicker {
return sa; return sa;
} }
private void createNewPlan(Score origGameScore, List<SpellAbility> candidateSAs) { private Plan formulatePlanWithPhase(Score origGameScore, List<SpellAbility> candidateSAs, PhaseType phase) {
plan = null;
SimulationController controller = new SimulationController(origGameScore); SimulationController controller = new SimulationController(origGameScore);
SpellAbility sa = chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore); SpellAbility sa = chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore, phase);
if (sa == null) { if (sa != null) {
print("No good plan at this time"); return controller.getBestPlan();
return;
} }
return null;
}
plan = controller.getBestPlan(); private void printPlan(Plan plan, String intro) {
print("New plan with score " + controller.getBestScore() + ":"); if (plan == null) {
print(intro + ": no plan!");
}
print(intro +" plan with score " + plan.getFinalScore() + ":");
int i = 0; int i = 0;
for (Plan.Decision d : plan.getDecisions()) { for (Plan.Decision d : plan.getDecisions()) {
print(++i + ". " + d); print(++i + ". " + d);
} }
} }
private SpellAbility chooseSpellAbilityToPlayImpl(SimulationController controller, List<SpellAbility> candidateSAs, Score origGameScore) { private static boolean isSorcerySpeed(SpellAbility sa) {
// TODO: Can we use the actual rules engine for this instead of trying to do the logic ourselves?
if (sa instanceof PlayLandAbility) {
return false;
}
if (sa.isSpell()) {
return !sa.getHostCard().isInstant() && !sa.getHostCard().hasKeyword("Flash");
}
if (sa.getRestrictions().isPwAbility()) {
return !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed.");
}
return sa.isAbility() && sa.getRestrictions().isSorcerySpeed();
}
private void createNewPlan(Score origGameScore, List<SpellAbility> candidateSAs) {
plan = null;
Plan bestPlan = formulatePlanWithPhase(origGameScore, candidateSAs, null);
if (bestPlan == null) {
print("No good plan at this time");
return;
}
PhaseType currentPhase = game.getPhaseHandler().getPhase();
if (currentPhase.isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
List<SpellAbility> candidateSAs2 = new ArrayList<SpellAbility>();
for (SpellAbility sa : candidateSAs) {
if (!isSorcerySpeed(sa)) {
System.err.println("Not sorcery: " + sa);
candidateSAs2.add(sa);
}
}
if (!candidateSAs2.isEmpty()) {
System.err.println("Formula plan with phase bloom");
Plan afterBlockersPlan = formulatePlanWithPhase(origGameScore, candidateSAs2, PhaseType.COMBAT_DECLARE_BLOCKERS);
if (afterBlockersPlan != null && afterBlockersPlan.getFinalScore().value >= bestPlan.getFinalScore().value) {
printPlan(afterBlockersPlan, "After blockers");
print("Deciding to wait until after declare blockers.");
return;
}
}
}
printPlan(bestPlan, "Current phase (" + currentPhase + ")");
plan = bestPlan;
}
private SpellAbility chooseSpellAbilityToPlayImpl(SimulationController controller, List<SpellAbility> candidateSAs, Score origGameScore, PhaseType phase) {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
SpellAbility bestSa = null; SpellAbility bestSa = null;
Score bestSaValue = origGameScore; Score bestSaValue = origGameScore;
print("Evaluating... (orig score = " + origGameScore + ")"); print("Evaluating... (orig score = " + origGameScore + ")");
for (int i = 0; i < candidateSAs.size(); i++) { for (int i = 0; i < candidateSAs.size(); i++) {
Score value = evaluateSa(controller, candidateSAs, i); Score value = evaluateSa(controller, phase, candidateSAs, i);
if (value.value > bestSaValue.value) { if (value.value > bestSaValue.value) {
bestSaValue = value; bestSaValue = value;
bestSa = candidateSAs.get(i); bestSa = candidateSAs.get(i);
@@ -195,6 +245,11 @@ public class SpellAbilityPicker {
plan = null; plan = null;
return null; return null;
} }
PhaseType startPhase = plan.getStartPhase();
if (startPhase != null && game.getPhaseHandler().getPhase().isBefore(startPhase)) {
print("Waiting until phase " + startPhase + " to proceed with the plan.");
return null;
}
Plan.Decision decision = plan.selectNextDecision(); Plan.Decision decision = plan.selectNextDecision();
if (!decision.initialScore.equals(origGameScore)) { if (!decision.initialScore.equals(origGameScore)) {
printPlannedActionFailure(decision, "Unexpected game score (" + decision.initialScore + " vs. expected " + origGameScore + ")"); printPlannedActionFailure(decision, "Unexpected game score (" + decision.initialScore + " vs. expected " + origGameScore + ")");
@@ -308,7 +363,7 @@ public class SpellAbilityPicker {
return AiPlayDecision.WillPlay; return AiPlayDecision.WillPlay;
} }
private Score evaluateSa(final SimulationController controller, List<SpellAbility> saList, int saIndex) { private Score evaluateSa(final SimulationController controller, PhaseType phase, List<SpellAbility> saList, int saIndex) {
controller.evaluateSpellAbility(saList, saIndex); controller.evaluateSpellAbility(saList, saIndex);
SpellAbility sa = saList.get(saIndex); SpellAbility sa = saList.get(saIndex);
@@ -316,7 +371,7 @@ public class SpellAbilityPicker {
final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller); final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller);
Score lastScore = null; Score lastScore = null;
do { do {
GameSimulator simulator = new GameSimulator(controller, game, player); GameSimulator simulator = new GameSimulator(controller, game, player, phase);
simulator.setInterceptor(choicesIterator); simulator.setInterceptor(choicesIterator);
lastScore = simulator.simulateSpellAbility(sa); lastScore = simulator.simulateSpellAbility(sa);
if (lastScore.value > bestScore.value) { if (lastScore.value > bestScore.value) {

View File

@@ -19,6 +19,7 @@ package forge.game.combat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@@ -91,19 +92,23 @@ public class Combat {
attackableEntries.add(map.map(entry)); attackableEntries.add(map.map(entry));
} }
HashMap<AttackingBand, AttackingBand> bandsMap = new HashMap<>();
for (Entry<GameEntity, AttackingBand> entry : combat.attackedByBands.entries()) { for (Entry<GameEntity, AttackingBand> entry : combat.attackedByBands.entries()) {
AttackingBand origBand = entry.getValue();
ArrayList<Card> attackers = new ArrayList<Card>(); ArrayList<Card> attackers = new ArrayList<Card>();
for (Card c : entry.getValue().getAttackers()) { for (Card c : origBand.getAttackers()) {
attackers.add(map.map(c)); attackers.add(map.map(c));
} }
attackedByBands.put(map.map(entry.getKey()), new AttackingBand(attackers)); AttackingBand newBand = new AttackingBand(attackers);
Boolean blocked = entry.getValue().isBlocked();
if (blocked != null) {
newBand.setBlocked(blocked);
}
bandsMap.put(origBand, newBand);
attackedByBands.put(map.map(entry.getKey()), newBand);
} }
for (Entry<AttackingBand, Card> entry : combat.blockedBands.entries()) { for (Entry<AttackingBand, Card> entry : combat.blockedBands.entries()) {
ArrayList<Card> attackers = new ArrayList<Card>(); blockedBands.put(bandsMap.get(entry.getKey()), map.map(entry.getValue()));
for (Card c : entry.getKey().getAttackers()) {
attackers.add(map.map(c));
}
blockedBands.put(new AttackingBand(attackers), map.map(entry.getValue()));
} }
for (Entry<Card, Integer> entry : combat.defendingDamageMap.entrySet()) { for (Entry<Card, Integer> entry : combat.defendingDamageMap.entrySet()) {
defendingDamageMap.put(map.map(entry.getKey()), entry.getValue()); defendingDamageMap.put(map.map(entry.getKey()), entry.getValue());
@@ -123,6 +128,28 @@ public class Combat {
attackConstraints = new AttackConstraints(this); attackConstraints = new AttackConstraints(this);
} }
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (GameEntity defender : attackableEntries) {
CardCollection attackers = getAttackersOf(defender);
if (attackers.isEmpty()) {
continue;
}
sb.append(defender);
sb.append(" is being attacked by:\n");
for (Card attacker : attackers) {
sb.append(" ").append(attacker).append("\n");
for (Card blocker : getBlockers(attacker)) {
sb.append(" ... blocked by: ").append(blocker).append("\n");
}
}
}
if (sb.length() == 0) {
return "<no attacks>";
}
return sb.toString();
}
public void endCombat() { public void endCombat() {
//backup attackers and blockers //backup attackers and blockers

View File

@@ -889,8 +889,6 @@ public class PhaseHandler implements java.io.Serializable {
// don't even offer priority, because it's untap of 1st turn now // don't even offer priority, because it's untap of 1st turn now
givePriorityToPlayer = false; givePriorityToPlayer = false;
final Set<Card> allAffectedCards = new HashSet<Card>();
// MAIN GAME LOOP // MAIN GAME LOOP
while (!game.isGameOver()) { while (!game.isGameOver()) {
if (givePriorityToPlayer) { if (givePriorityToPlayer) {
@@ -903,17 +901,9 @@ public class PhaseHandler implements java.io.Serializable {
int loopCount = 0; int loopCount = 0;
do { do {
do { if (checkStateBasedEffects()) {
// Rule 704.3 Whenever a player would get priority, the game checks ... for state-based actions, // state-based effects check could lead to game over
game.getAction().checkStateEffects(false, allAffectedCards); return;
if (game.isGameOver()) {
return; // state-based effects check could lead to game over
}
} while (game.getStack().addAllTriggeredAbilitiesToStack()); //loop so long as something was added to stack
if (!allAffectedCards.isEmpty()) {
game.fireEvent(new GameEventCardStatsChanged(allAffectedCards));
allAffectedCards.clear();
} }
if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) { if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) {
@@ -996,9 +986,39 @@ public class PhaseHandler implements java.io.Serializable {
} }
} }
private boolean checkStateBasedEffects() {
final Set<Card> allAffectedCards = new HashSet<Card>();
do {
// Rule 704.3 Whenever a player would get priority, the game checks ... for state-based actions,
game.getAction().checkStateEffects(false, allAffectedCards);
if (game.isGameOver()) {
return true; // state-based effects check could lead to game over
}
} while (game.getStack().addAllTriggeredAbilitiesToStack()); //loop so long as something was added to stack
if (!allAffectedCards.isEmpty()) {
game.fireEvent(new GameEventCardStatsChanged(allAffectedCards));
allAffectedCards.clear();
}
return false;
}
public final boolean devAdvanceToPhase(PhaseType targetPhase) {
while (phase.isBefore(targetPhase)) {
if (checkStateBasedEffects()) {
return false;
}
onPhaseEnd();
advanceToNextPhase();
onPhaseBegin();
}
checkStateBasedEffects();
return true;
}
// this is a hack for the setup game state mode, do not use outside of devSetupGameState code // this is a hack for the setup game state mode, do not use outside of devSetupGameState code
// as it avoids calling any of the phase effects that may be necessary in a less enforced context // as it avoids calling any of the phase effects that may be necessary in a less enforced context
public final void devModeSet(final PhaseType phase0, final Player player0) { public final void devModeSet(final PhaseType phase0, final Player player0, boolean endCombat) {
if (phase0 != null) { if (phase0 != null) {
setPhase(phase0); setPhase(phase0);
} }
@@ -1007,7 +1027,12 @@ public class PhaseHandler implements java.io.Serializable {
} }
game.fireEvent(new GameEventTurnPhase(playerTurn, phase, "")); game.fireEvent(new GameEventTurnPhase(playerTurn, phase, ""));
endCombat(); // not-null can be created only when declare attackers phase begins if (endCombat) {
endCombat(); // not-null can be created only when declare attackers phase begins
}
}
public final void devModeSet(final PhaseType phase0, final Player player0) {
devModeSet(phase0, player0, true);
} }
public final void endTurnByEffect() { public final void endTurnByEffect() {

View File

@@ -179,7 +179,7 @@ public class GameSimulatorTest extends SimulationTestCase {
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
assertEquals(20, p.getOpponent().getLife()); assertEquals(20, game.getPlayers().get(0).getLife());
GameSimulator sim = createSimulator(game, p); GameSimulator sim = createSimulator(game, p);
Game simGame = sim.getSimulatedGameState(); Game simGame = sim.getSimulatedGameState();

View File

@@ -66,7 +66,7 @@ public class SimulationTestCase extends TestCase {
public boolean shouldRecurse() { public boolean shouldRecurse() {
return false; return false;
} }
}, game, p); }, game, p, null);
} }
protected Card findCardWithName(Game game, String name) { protected Card findCardWithName(Game game, String name) {

View File

@@ -5,6 +5,7 @@ import java.util.List;
import forge.game.Game; import forge.game.Game;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -22,7 +23,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
addCard("Runeclaw Bear", opponent); addCard("Runeclaw Bear", opponent);
opponent.setLife(2, null); opponent.setLife(2, null);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p); SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
@@ -43,7 +44,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
Card bearCard = addCard("Runeclaw Bear", opponent); Card bearCard = addCard("Runeclaw Bear", opponent);
opponent.setLife(20, null); opponent.setLife(20, null);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p); SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
@@ -91,7 +92,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
Player opponent = game.getPlayers().get(0); Player opponent = game.getPlayers().get(0);
addCard("Runeclaw Bear", opponent); addCard("Runeclaw Bear", opponent);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
// Expected: All creatures get -2/-2 to kill the bear. // Expected: All creatures get -2/-2 to kill the bear.
@@ -110,7 +111,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
addCard("Swamp", p); addCard("Swamp", p);
Card spell = addCardToZone("Dromar's Charm", p, ZoneType.Hand); Card spell = addCardToZone("Dromar's Charm", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
// Expected: Gain 5 life, since other modes aren't helpful. // Expected: Gain 5 life, since other modes aren't helpful.
@@ -134,7 +135,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
addCard("Runeclaw Bear", opponent); addCard("Runeclaw Bear", opponent);
opponent.setLife(20, null); opponent.setLife(20, null);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
// Expected: 2x 1 damage to each creature, 1x 2 damage to each opponent. // Expected: 2x 1 damage to each creature, 1x 2 damage to each opponent.
@@ -162,7 +163,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
addCard("Runeclaw Bear", opponent); addCard("Runeclaw Bear", opponent);
opponent.setLife(6, null); opponent.setLife(6, null);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
// Expected: 3x 2 damage to each opponent. // Expected: 3x 2 damage to each opponent.
@@ -188,7 +189,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
Card men = addCard("Flying Men", opponent); Card men = addCard("Flying Men", opponent);
opponent.setLife(20, null); opponent.setLife(20, null);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
game.getAction().checkStateEffects(true); game.getAction().checkStateEffects(true);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p); SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
@@ -292,4 +293,96 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
String saDesc = plan.getDecisions().get(1).saRef.toString(); String saDesc = plan.getDecisions().get(1).saRef.toString();
assertTrue(saDesc, saDesc.startsWith("Lightning Bolt deals 3 damage to target creature or player.")); assertTrue(saDesc, saDesc.startsWith("Lightning Bolt deals 3 damage to target creature or player."));
} }
public void testPlayingPumpSpellsAfterBlocks() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
Player opponent = game.getPlayers().get(0);
opponent.setLife(2, null);
Card blocker = addCard("Fugitive Wizard", opponent);
Card attacker1 = addCard("Dwarven Trader", p);
attacker1.setSickness(false);
Card attacker2 = addCard("Kird Ape", p);
attacker2.setSickness(false);
addCard("Mountain", p);
addCardToZone("Brute Force", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
game.getAction().checkStateEffects(true);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
assertNull(picker.chooseSpellAbilityToPlay(null));
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_BEGIN, p);
game.getAction().checkStateEffects(true);
assertNull(picker.chooseSpellAbilityToPlay(null));
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, p);
Combat combat = new Combat(p);
combat.addAttacker(attacker1, opponent);
combat.addAttacker(attacker2, opponent);
game.getPhaseHandler().setCombat(combat);
game.getAction().checkStateEffects(true);
assertNull(picker.chooseSpellAbilityToPlay(null));
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_BLOCKERS, p, false);
game.getAction().checkStateEffects(true);
combat.addBlocker(attacker1, blocker);
combat.getBandOfAttacker(attacker1).setBlocked(true);
combat.getBandOfAttacker(attacker2).setBlocked(false);
combat.orderBlockersForDamageAssignment();
combat.orderAttackersForDamageAssignment();
SpellAbility sa = picker.chooseSpellAbilityToPlay(null);
assertNotNull(sa);
assertEquals("Target creature gets +3/+3 until end of turn.", sa.toString());
assertEquals(attacker2, sa.getTargetCard());
}
public void testPlayingSorceryPumpSpellsBeforeBlocks() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
Player opponent = game.getPlayers().get(0);
opponent.setLife(2, null);
addCard("Fugitive Wizard", opponent);
Card attacker1 = addCard("Dwarven Trader", p);
attacker1.setSickness(false);
Card attacker2 = addCard("Kird Ape", p);
attacker2.setSickness(false);
addCard("Mountain", p);
Card furor = addCardToZone("Furor of the Bitten", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
game.getAction().checkStateEffects(true);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
SpellAbility sa = picker.chooseSpellAbilityToPlay(null);
assertNotNull(sa);
assertEquals(furor.getSpellAbilities().get(0), sa);
assertEquals(attacker1, sa.getTargetCard());
}
public void testPlayingRemovalBeforeBlocks() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
Player opponent = game.getPlayers().get(0);
opponent.setLife(2, null);
Card blocker = addCard("Fugitive Wizard", opponent);
Card attacker1 = addCard("Dwarven Trader", p);
attacker1.setSickness(false);
addCard("Swamp", p);
addCard("Swamp", p);
addCardToZone("Doom Blade", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
game.getAction().checkStateEffects(true);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
SpellAbility sa = picker.chooseSpellAbilityToPlay(null);
assertNotNull(sa);
assertEquals("Destroy target nonblack creature.", sa.toString());
assertEquals(blocker, sa.getTargetCard());
}
} }