mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 12:18:00 +00:00
[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:
@@ -24,6 +24,7 @@ import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.RegisteredPlayer;
|
||||
import forge.game.spellability.AbilityActivated;
|
||||
@@ -64,6 +65,9 @@ public class GameCopier {
|
||||
}
|
||||
|
||||
public Game makeCopy() {
|
||||
return makeCopy(null);
|
||||
}
|
||||
public Game makeCopy(PhaseType advanceToPhase) {
|
||||
List<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers();
|
||||
List<RegisteredPlayer> newPlayers = new ArrayList<>();
|
||||
for (RegisteredPlayer p : origPlayers) {
|
||||
@@ -148,6 +152,10 @@ public class GameCopier {
|
||||
newGame.getPhaseHandler().setCombat(new Combat(origPhaseHandler.getCombat(), gameObjectMap));
|
||||
}
|
||||
|
||||
if (advanceToPhase != null) {
|
||||
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase);
|
||||
}
|
||||
|
||||
return newGame;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import forge.ai.simulation.GameStateEvaluator.Score;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetChoices;
|
||||
@@ -28,10 +29,10 @@ public class GameSimulator {
|
||||
private Score origScore;
|
||||
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;
|
||||
copier = new GameCopier(origGame);
|
||||
simGame = copier.makeCopy();
|
||||
simGame = copier.makeCopy(advanceToPhase);
|
||||
|
||||
aiPlayer = (Player) copier.find(origAiPlayer);
|
||||
eval = new GameStateEvaluator();
|
||||
@@ -42,20 +43,9 @@ public class GameSimulator {
|
||||
debugPrint = false;
|
||||
origScore = eval.getScoreForGameState(origGame, 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.");
|
||||
if (advanceToPhase == null) {
|
||||
ensureGameCopyScoreMatches(origGame, origAiPlayer);
|
||||
}
|
||||
eval.setDebugging(false);
|
||||
|
||||
// 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
|
||||
@@ -73,6 +63,23 @@ public class GameSimulator {
|
||||
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) {
|
||||
this.interceptor = interceptor;
|
||||
((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor);
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
package forge.ai.simulation;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.CreatureEvaluator;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class GameStateEvaluator {
|
||||
private boolean debugging = false;
|
||||
private boolean ignoreTempBoosts = false;
|
||||
private SimulationCreatureEvaluator eval = new SimulationCreatureEvaluator();
|
||||
|
||||
public void setDebugging(boolean debugging) {
|
||||
@@ -21,19 +18,25 @@ public class GameStateEvaluator {
|
||||
}
|
||||
|
||||
private static void debugPrint(String s) {
|
||||
//System.err.println(s);
|
||||
GameSimulator.debugPrint(s);
|
||||
}
|
||||
|
||||
private Combat simulateUpcomingCombatThisTurn(Game game) {
|
||||
PhaseHandler handler = game.getPhaseHandler();
|
||||
if (handler.getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
|
||||
private static class CombatSimResult {
|
||||
public GameCopier copier;
|
||||
public Game gameCopy;
|
||||
}
|
||||
private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame) {
|
||||
PhaseType phase = evalGame.getPhaseHandler().getPhase();
|
||||
if (phase.isAfter(PhaseType.COMBAT_DAMAGE) || evalGame.isGameOver()) {
|
||||
return null;
|
||||
}
|
||||
AiAttackController aiAtk = new AiAttackController(handler.getPlayerTurn());
|
||||
Combat combat = new Combat(handler.getPlayerTurn());
|
||||
aiAtk.declareAttackers(combat);
|
||||
return combat;
|
||||
GameCopier copier = new GameCopier(evalGame);
|
||||
Game gameCopy = copier.makeCopy();
|
||||
gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE);
|
||||
CombatSimResult result = new CombatSimResult();
|
||||
result.copier = copier;
|
||||
result.gameCopy = gameCopy;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String cardToString(Card c) {
|
||||
@@ -44,13 +47,27 @@ public class GameStateEvaluator {
|
||||
return str;
|
||||
}
|
||||
|
||||
public Score getScoreForGameState(Game game, Player aiPlayer) {
|
||||
if (game.isGameOver()) {
|
||||
private Score getScoreForGameOver(Game game, Player aiPlayer) {
|
||||
return game.getOutcome().getWinningPlayer() == aiPlayer ? new Score(Integer.MAX_VALUE) : new Score(Integer.MIN_VALUE);
|
||||
}
|
||||
|
||||
Combat combat = simulateUpcomingCombatThisTurn(game);
|
||||
public Score getScoreForGameState(Game game, Player aiPlayer) {
|
||||
if (game.isGameOver()) {
|
||||
return getScoreForGameOver(game, aiPlayer);
|
||||
}
|
||||
|
||||
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;
|
||||
// TODO: more than 2 players
|
||||
int myCards = 0;
|
||||
@@ -85,7 +102,7 @@ public class GameStateEvaluator {
|
||||
int summonSickScore = score;
|
||||
PhaseType gamePhase = game.getPhaseHandler().getPhase();
|
||||
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
int value = evalCard(game, aiPlayer, c, combat);
|
||||
int value = evalCard(game, aiPlayer, c);
|
||||
int summonSickValue = value;
|
||||
// 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.
|
||||
@@ -111,20 +128,10 @@ public class GameStateEvaluator {
|
||||
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.
|
||||
if (c.isCreature()) {
|
||||
// Ignore temp boosts post combat, since it's a waste.
|
||||
// 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;
|
||||
return eval.evaluateCreature(c);
|
||||
} else if (c.isLand()) {
|
||||
return 100;
|
||||
} else if (c.isEnchantingCard()) {
|
||||
@@ -153,20 +160,14 @@ public class GameStateEvaluator {
|
||||
|
||||
@Override
|
||||
protected int getEffectivePower(final Card c) {
|
||||
if (ignoreTempBoosts) {
|
||||
Card.StatBreakdown breakdown = c.getNetCombatDamageBreakdown();
|
||||
return breakdown.getTotal() - breakdown.tempBoost;
|
||||
}
|
||||
return c.getNetCombatDamage();
|
||||
}
|
||||
@Override
|
||||
protected int getEffectiveToughness(final Card c) {
|
||||
if (ignoreTempBoosts) {
|
||||
Card.StatBreakdown breakdown = c.getNetToughnessBreakdown();
|
||||
return breakdown.getTotal() - breakdown.tempBoost;
|
||||
}
|
||||
return c.getNetToughness();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Score {
|
||||
|
||||
@@ -7,16 +7,28 @@ import com.google.common.base.Joiner;
|
||||
|
||||
import forge.ai.simulation.GameStateEvaluator.Score;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class Plan {
|
||||
private List<Decision> decisions;
|
||||
private final List<Decision> decisions;
|
||||
private final Score finalScore;
|
||||
private int nextDecisionIndex;
|
||||
private int nextChoice;
|
||||
private Decision selectedDecision;
|
||||
private PhaseType startPhase;
|
||||
|
||||
public Plan(ArrayList<Decision> decisions) {
|
||||
public Plan(ArrayList<Decision> decisions, Score finalScore) {
|
||||
this.decisions = decisions;
|
||||
this.finalScore = finalScore;
|
||||
}
|
||||
|
||||
public Score getFinalScore() {
|
||||
return finalScore;
|
||||
}
|
||||
|
||||
public PhaseType getStartPhase() {
|
||||
return startPhase;
|
||||
}
|
||||
|
||||
public List<Decision> getDecisions() {
|
||||
|
||||
@@ -125,7 +125,7 @@ public class SimulationController {
|
||||
}
|
||||
}
|
||||
sequence.subList(writeIndex, sequence.size()).clear();
|
||||
return new Plan(sequence);
|
||||
return new Plan(sequence, getBestScore());
|
||||
}
|
||||
|
||||
private Plan.Decision getLastMergedDecision() {
|
||||
@@ -221,7 +221,7 @@ public class SimulationController {
|
||||
if (effect.hostCard == hostAndTarget[0] && effect.target == hostAndTarget[1] && effect.sa.equals(saString)) {
|
||||
GameStateEvaluator evaluator = new GameStateEvaluator();
|
||||
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) {
|
||||
Score currentScore = getCurrentScore();
|
||||
// TODO: summonSick score?
|
||||
@@ -249,7 +249,7 @@ public class SimulationController {
|
||||
if (currentHostAndTarget != null) {
|
||||
GameStateEvaluator evaluator = new GameStateEvaluator();
|
||||
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));
|
||||
cached = " (added to cache)";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.simulation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@@ -57,7 +58,22 @@ public class SpellAbilityPicker {
|
||||
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);
|
||||
int writeIndex = 0;
|
||||
for (int i = 0; i < candidateSAs.size(); i++) {
|
||||
@@ -89,28 +105,12 @@ public class SpellAbilityPicker {
|
||||
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);
|
||||
List<SpellAbility> candidateSAs = getCandidateSpellsAndAbilities(all);
|
||||
List<SpellAbility> candidateSAs = getCandidateSpellsAndAbilities();
|
||||
if (controller != null) {
|
||||
// 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.
|
||||
return chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore);
|
||||
return chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore, null);
|
||||
}
|
||||
|
||||
printPhaseInfo();
|
||||
@@ -130,31 +130,81 @@ public class SpellAbilityPicker {
|
||||
return sa;
|
||||
}
|
||||
|
||||
private void createNewPlan(Score origGameScore, List<SpellAbility> candidateSAs) {
|
||||
plan = null;
|
||||
private Plan formulatePlanWithPhase(Score origGameScore, List<SpellAbility> candidateSAs, PhaseType phase) {
|
||||
SimulationController controller = new SimulationController(origGameScore);
|
||||
SpellAbility sa = chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore);
|
||||
if (sa == null) {
|
||||
print("No good plan at this time");
|
||||
return;
|
||||
SpellAbility sa = chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore, phase);
|
||||
if (sa != null) {
|
||||
return controller.getBestPlan();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
plan = controller.getBestPlan();
|
||||
print("New plan with score " + controller.getBestScore() + ":");
|
||||
private void printPlan(Plan plan, String intro) {
|
||||
if (plan == null) {
|
||||
print(intro + ": no plan!");
|
||||
}
|
||||
print(intro +" plan with score " + plan.getFinalScore() + ":");
|
||||
int i = 0;
|
||||
for (Plan.Decision d : plan.getDecisions()) {
|
||||
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();
|
||||
|
||||
SpellAbility bestSa = null;
|
||||
Score bestSaValue = origGameScore;
|
||||
print("Evaluating... (orig score = " + origGameScore + ")");
|
||||
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) {
|
||||
bestSaValue = value;
|
||||
bestSa = candidateSAs.get(i);
|
||||
@@ -195,6 +245,11 @@ public class SpellAbilityPicker {
|
||||
plan = 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();
|
||||
if (!decision.initialScore.equals(origGameScore)) {
|
||||
printPlannedActionFailure(decision, "Unexpected game score (" + decision.initialScore + " vs. expected " + origGameScore + ")");
|
||||
@@ -308,7 +363,7 @@ public class SpellAbilityPicker {
|
||||
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);
|
||||
SpellAbility sa = saList.get(saIndex);
|
||||
|
||||
@@ -316,7 +371,7 @@ public class SpellAbilityPicker {
|
||||
final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller);
|
||||
Score lastScore = null;
|
||||
do {
|
||||
GameSimulator simulator = new GameSimulator(controller, game, player);
|
||||
GameSimulator simulator = new GameSimulator(controller, game, player, phase);
|
||||
simulator.setInterceptor(choicesIterator);
|
||||
lastScore = simulator.simulateSpellAbility(sa);
|
||||
if (lastScore.value > bestScore.value) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package forge.game.combat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
@@ -91,19 +92,23 @@ public class Combat {
|
||||
attackableEntries.add(map.map(entry));
|
||||
}
|
||||
|
||||
HashMap<AttackingBand, AttackingBand> bandsMap = new HashMap<>();
|
||||
for (Entry<GameEntity, AttackingBand> entry : combat.attackedByBands.entries()) {
|
||||
AttackingBand origBand = entry.getValue();
|
||||
ArrayList<Card> attackers = new ArrayList<Card>();
|
||||
for (Card c : entry.getValue().getAttackers()) {
|
||||
for (Card c : origBand.getAttackers()) {
|
||||
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()) {
|
||||
ArrayList<Card> attackers = new ArrayList<Card>();
|
||||
for (Card c : entry.getKey().getAttackers()) {
|
||||
attackers.add(map.map(c));
|
||||
}
|
||||
blockedBands.put(new AttackingBand(attackers), map.map(entry.getValue()));
|
||||
blockedBands.put(bandsMap.get(entry.getKey()), map.map(entry.getValue()));
|
||||
}
|
||||
for (Entry<Card, Integer> entry : combat.defendingDamageMap.entrySet()) {
|
||||
defendingDamageMap.put(map.map(entry.getKey()), entry.getValue());
|
||||
@@ -123,6 +128,28 @@ public class Combat {
|
||||
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() {
|
||||
//backup attackers and blockers
|
||||
|
||||
@@ -889,8 +889,6 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
// don't even offer priority, because it's untap of 1st turn now
|
||||
givePriorityToPlayer = false;
|
||||
|
||||
final Set<Card> allAffectedCards = new HashSet<Card>();
|
||||
|
||||
// MAIN GAME LOOP
|
||||
while (!game.isGameOver()) {
|
||||
if (givePriorityToPlayer) {
|
||||
@@ -903,17 +901,9 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
|
||||
int loopCount = 0;
|
||||
do {
|
||||
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; // 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 (checkStateBasedEffects()) {
|
||||
// state-based effects check could lead to game over
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
// 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) {
|
||||
setPhase(phase0);
|
||||
}
|
||||
@@ -1007,8 +1027,13 @@ public class PhaseHandler implements java.io.Serializable {
|
||||
}
|
||||
|
||||
game.fireEvent(new GameEventTurnPhase(playerTurn, phase, ""));
|
||||
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() {
|
||||
endCombat();
|
||||
|
||||
@@ -179,7 +179,7 @@ public class GameSimulatorTest extends SimulationTestCase {
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
assertEquals(20, p.getOpponent().getLife());
|
||||
assertEquals(20, game.getPlayers().get(0).getLife());
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
Game simGame = sim.getSimulatedGameState();
|
||||
|
||||
@@ -66,7 +66,7 @@ public class SimulationTestCase extends TestCase {
|
||||
public boolean shouldRecurse() {
|
||||
return false;
|
||||
}
|
||||
}, game, p);
|
||||
}, game, p, null);
|
||||
}
|
||||
|
||||
protected Card findCardWithName(Game game, String name) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -22,7 +23,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
|
||||
addCard("Runeclaw Bear", opponent);
|
||||
opponent.setLife(2, null);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
|
||||
@@ -43,7 +44,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
|
||||
Card bearCard = addCard("Runeclaw Bear", opponent);
|
||||
opponent.setLife(20, null);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
|
||||
@@ -91,7 +92,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
|
||||
Player opponent = game.getPlayers().get(0);
|
||||
addCard("Runeclaw Bear", opponent);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
// Expected: All creatures get -2/-2 to kill the bear.
|
||||
@@ -110,7 +111,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
|
||||
addCard("Swamp", p);
|
||||
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);
|
||||
|
||||
// Expected: Gain 5 life, since other modes aren't helpful.
|
||||
@@ -134,7 +135,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
|
||||
addCard("Runeclaw Bear", opponent);
|
||||
opponent.setLife(20, null);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
// 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);
|
||||
opponent.setLife(6, null);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
// Expected: 3x 2 damage to each opponent.
|
||||
@@ -188,7 +189,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
|
||||
Card men = addCard("Flying Men", opponent);
|
||||
opponent.setLife(20, null);
|
||||
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
game.getAction().checkStateEffects(true);
|
||||
|
||||
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
|
||||
@@ -292,4 +293,96 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
|
||||
String saDesc = plan.getDecisions().get(1).saRef.toString();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user