[Simulated AI] Refactor code to create a Plan object.

This allows coming up with a multi-step planning and caching it, so it doesn't need to be re-computed at subsequent steps if nothing meaningful changed.
This commit is contained in:
Myrd
2016-12-25 05:05:18 +00:00
parent 5ed07ed4a0
commit 328922029a
8 changed files with 321 additions and 48 deletions

1
.gitattributes vendored
View File

@@ -156,6 +156,7 @@ forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java -text
forge-ai/src/main/java/forge/ai/simulation/GameCopier.java -text forge-ai/src/main/java/forge/ai/simulation/GameCopier.java -text
forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java -text forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java -text
forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java -text forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java -text
forge-ai/src/main/java/forge/ai/simulation/Plan.java -text
forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java -text forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java -text
forge-ai/src/main/java/forge/ai/simulation/SimulationController.java -text forge-ai/src/main/java/forge/ai/simulation/SimulationController.java -text
forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java -text forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java -text

View File

@@ -192,7 +192,7 @@ public class GameSimulator {
} }
controller.printState(score, origSa); controller.printState(score, origSa);
if (controller.shouldRecurse() && !simGame.isGameOver()) { if (controller.shouldRecurse() && !simGame.isGameOver()) {
controller.push(sa); controller.push(sa, score);
SpellAbilityPicker sim = new SpellAbilityPicker(simGame, aiPlayer); SpellAbilityPicker sim = new SpellAbilityPicker(simGame, aiPlayer);
CardCollection cards = ComputerUtilAbility.getAvailableCards(simGame, aiPlayer); CardCollection cards = ComputerUtilAbility.getAvailableCards(simGame, aiPlayer);
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, aiPlayer); List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, aiPlayer);

View File

@@ -79,9 +79,9 @@ public class GameStateEvaluator {
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, combat);
int summonSickValue = value; int summonSickValue = value;
// To make the AI hold-off on playing creatures in MAIN1 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.
if (gamePhase == PhaseType.MAIN1 && c.isSick() && c.getController() == aiPlayer) { if (gamePhase.isBefore(PhaseType.MAIN2) && c.isSick() && c.getController() == aiPlayer) {
summonSickValue = 0; summonSickValue = 0;
} }
String str = c.getName(); String str = c.getName();
@@ -167,7 +167,6 @@ public class GameStateEvaluator {
public static class Score { public static class Score {
public final int value; public final int value;
public final int summonSickValue; public final int summonSickValue;
public String choice;
public Score(int value) { public Score(int value) {
this.value = value; this.value = value;

View File

@@ -0,0 +1,78 @@
package forge.ai.simulation;
import java.util.ArrayList;
import java.util.List;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
public class Plan {
private List<Decision> decisions;
private int nextDecisionIndex;
private Decision selectedDecision;
public Plan(ArrayList<Decision> decisions) {
this.decisions = decisions;
}
public List<Decision> getDecisions() {
return decisions;
}
public boolean hasNextDecision() {
return nextDecisionIndex < decisions.size();
}
public Decision selectNextDecision() {
selectedDecision = decisions.get(nextDecisionIndex);
nextDecisionIndex++;
return selectedDecision;
}
public Decision getSelectedDecision() {
return selectedDecision;
}
public int getNextDecisionIndex() {
return nextDecisionIndex;
}
public static class Decision {
final Decision prevDecision;
final Score initialScore;
final String sa;
PossibleTargetSelector.Targets targets;
String choice;
public Decision(Score initialScore, Decision prevDecision, SpellAbility sa) {
this.initialScore = initialScore;
this.prevDecision = prevDecision;
this.sa = sa.toString();
this.targets = null;
this.choice = null;
}
public Decision(Score initialScore, Decision prevDecision, PossibleTargetSelector.Targets targets) {
this.initialScore = initialScore;
this.prevDecision = prevDecision;
this.sa = null;
this.targets = targets;
this.choice = null;
}
public Decision(Score initialScore, Decision prevDecision, Card choice) {
this.initialScore = initialScore;
this.prevDecision = prevDecision;
this.sa = null;
this.targets = null;
this.choice = choice.getName();
}
@Override
public String toString() {
return "[initScore=" + initialScore + " " + sa + " " + targets + " " + choice + "]";
}
}
}

View File

@@ -16,23 +16,43 @@ public class PossibleTargetSelector {
private int targetIndex; private int targetIndex;
private List<GameObject> validTargets; private List<GameObject> validTargets;
public static class Targets {
final int originalTargetCount;
final int targetIndex;
final String description;
private Targets(int originalTargetCount, int targetIndex, String description) {
this.originalTargetCount = originalTargetCount;
this.targetIndex = targetIndex;
this.description = description;
if (targetIndex < 0 || targetIndex >= originalTargetCount) {
throw new IllegalArgumentException("Invalid targetIndex=" + targetIndex);
}
}
@Override
public String toString() {
return description;
}
}
public PossibleTargetSelector(Game game, Player self, SpellAbility sa) { public PossibleTargetSelector(Game game, Player self, SpellAbility sa) {
this.sa = sa; this.sa = sa;
this.tgt = sa.getTargetRestrictions(); this.tgt = sa.getTargetRestrictions();
this.targetIndex = 0; this.targetIndex = 0;
this.validTargets = new ArrayList<GameObject>(); this.validTargets = new ArrayList<GameObject>();
sa.resetTargets();
sa.setActivatingPlayer(self); sa.setActivatingPlayer(self);
for (GameObject o : tgt.getAllCandidates(sa, true)) { for (GameObject o : tgt.getAllCandidates(sa, true)) {
validTargets.add(o); validTargets.add(o);
} }
} }
public boolean selectNextTargets() { private void selectTargetsByIndex(int index) {
if (targetIndex >= validTargets.size()) {
return false;
}
sa.resetTargets(); sa.resetTargets();
int index = targetIndex;
// TODO: smarter about multiple targets, identical targets, etc...
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa) && index < validTargets.size()) { while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa) && index < validTargets.size()) {
sa.getTargets().add(validTargets.get(index++)); sa.getTargets().add(validTargets.get(index++));
} }
@@ -53,8 +73,25 @@ public class PossibleTargetSelector {
} }
} }
} }
}
// TODO: smarter about multiple targets, identical targets, etc... public Targets getLastSelectedTargets() {
return new Targets(validTargets.size(), targetIndex - 1, sa.getTargets().getTargetedString());
}
public boolean selectTargets(Targets targets) {
if (targets.originalTargetCount != validTargets.size()) {
return false;
}
selectTargetsByIndex(targets.targetIndex);
return true;
}
public boolean selectNextTargets() {
if (targetIndex >= validTargets.size()) {
return false;
}
selectTargetsByIndex(targetIndex);
targetIndex++; targetIndex++;
return true; return true;
} }

View File

@@ -1,6 +1,11 @@
package forge.ai.simulation; package forge.ai.simulation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import forge.ai.simulation.GameStateEvaluator.Score; import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
public class SimulationController { public class SimulationController {
@@ -8,21 +13,93 @@ public class SimulationController {
private int recursionDepth; private int recursionDepth;
public SimulationController() { private List<Plan.Decision> currentStack;
private List<Score> scoreStack;
private Plan.Decision bestSequence; // last action of sequence
private Score bestScore;
public SimulationController(Score score) {
bestScore = score;
scoreStack = new ArrayList<Score>();
scoreStack.add(score);
currentStack = new ArrayList<Plan.Decision>();
} }
public boolean shouldRecurse() { public boolean shouldRecurse() {
return recursionDepth < MAX_DEPTH; return recursionDepth < MAX_DEPTH;
} }
public void push(SpellAbility sa) { private Plan.Decision getLastDecision() {
if (currentStack.isEmpty()) {
return null;
}
return currentStack.get(currentStack.size() - 1);
}
private Score getCurrentScore() {
return scoreStack.get(scoreStack.size() - 1);
}
public void evaluateSpellAbility(SpellAbility sa) {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), sa));
}
public void evaluateCardChoice(Card choice) {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), choice));
}
public void evaluateTargetChoices(PossibleTargetSelector.Targets targets) {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), targets));
}
public void doneEvaluating(Score score) {
if (score.value > bestScore.value) {
bestScore = score;
bestSequence = currentStack.get(currentStack.size() - 1);
}
currentStack.remove(currentStack.size() - 1);
}
public Score getBestScore() {
return bestScore;
}
public Plan getBestPlan() {
ArrayList<Plan.Decision> sequence = new ArrayList<Plan.Decision>();
Plan.Decision current = bestSequence;
while (current != null) {
sequence.add(current);
current = current.prevDecision;
}
Collections.reverse(sequence);
// Merge targets & choices into their parents.
int writeIndex = 0;
for (int i = 0; i < sequence.size(); i++) {
Plan.Decision d = sequence.get(i);
System.out.println("SeqInput: " + d);
if (d.sa != null) {
sequence.set(writeIndex, d);
writeIndex++;
} else if (d.targets != null) {
sequence.get(writeIndex - 1).targets = d.targets;
} else if (d.choice != null) {
sequence.get(writeIndex - 1).choice = d.choice;
}
}
sequence.subList(writeIndex, sequence.size()).clear();
return new Plan(sequence);
}
public void push(SpellAbility sa, Score score) {
GameSimulator.debugPrint("Recursing DEPTH=" + recursionDepth); GameSimulator.debugPrint("Recursing DEPTH=" + recursionDepth);
GameSimulator.debugPrint(" With: " + sa); GameSimulator.debugPrint(" With: " + sa);
recursionDepth++; recursionDepth++;
scoreStack.add(score);
} }
public void pop(Score score, SpellAbility nextSa) { public void pop(Score score, SpellAbility nextSa) {
recursionDepth--; recursionDepth--;
scoreStack.remove(scoreStack.size() - 1);
GameSimulator.debugPrint("DEPTH"+recursionDepth+" best score " + score + " " + nextSa); GameSimulator.debugPrint("DEPTH"+recursionDepth+" best score " + score + " " + nextSa);
} }

View File

@@ -1,6 +1,5 @@
package forge.ai.simulation; package forge.ai.simulation;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -26,6 +25,8 @@ public class SpellAbilityPicker {
private boolean printOutput; private boolean printOutput;
private Interceptor interceptor; private Interceptor interceptor;
private Plan plan;
public SpellAbilityPicker(Game game, Player player) { public SpellAbilityPicker(Game game, Player player) {
this.game = game; this.game = game;
this.player = player; this.player = player;
@@ -41,21 +42,19 @@ public class SpellAbilityPicker {
} }
} }
public SpellAbility chooseSpellAbilityToPlay(SimulationController controller, final List<SpellAbility> all, boolean skipCounter) { private void printPhaseInfo() {
printOutput = false;
if (controller == null) {
controller = new SimulationController();
printOutput = true;
}
String phaseStr = game.getPhaseHandler().getPhase().toString(); String phaseStr = game.getPhaseHandler().getPhase().toString();
if (game.getPhaseHandler().getPlayerTurn() != player) { if (game.getPhaseHandler().getPlayerTurn() != player) {
phaseStr = "opponent " + phaseStr; phaseStr = "opponent " + phaseStr;
} }
print("---- choose ability (phase = " + phaseStr + ")"); print("---- choose ability (phase = " + phaseStr + ")");
}
long startTime = System.currentTimeMillis(); private List<SpellAbility> getCandidateSpellsAndAbilities(List<SpellAbility> all) {
List<SpellAbility> candidateSAs = new ArrayList<>(); List<SpellAbility> candidateSAs = ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) { int writeIndex = 0;
for (int i = 0; i < candidateSAs.size(); i++) {
SpellAbility sa = candidateSAs.get(i);
if (sa.isManaAbility()) { if (sa.isManaAbility()) {
continue; continue;
} }
@@ -68,20 +67,59 @@ public class SpellAbilityPicker {
if (opinion != AiPlayDecision.WillPlay) if (opinion != AiPlayDecision.WillPlay)
continue; continue;
candidateSAs.add(sa); candidateSAs.set(writeIndex, sa);
writeIndex++;
}
candidateSAs.subList(writeIndex, candidateSAs.size()).clear();
return candidateSAs;
} }
if (candidateSAs.isEmpty()) { public SpellAbility chooseSpellAbilityToPlay(SimulationController controller, List<SpellAbility> all, boolean skipCounter) {
return null; printOutput = (controller == null);
}
SpellAbility bestSa = null;
GameSimulator simulator = new GameSimulator(controller, game, player);
// FIXME: This is wasteful, we should re-use the same simulator... // FIXME: This is wasteful, we should re-use the same simulator...
GameSimulator simulator = new GameSimulator(controller, game, player);
Score origGameScore = simulator.getScoreForOrigGame(); Score origGameScore = simulator.getScoreForOrigGame();
List<SpellAbility> candidateSAs = getCandidateSpellsAndAbilities(all);
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);
}
printPhaseInfo();
SpellAbility sa = getPlannedSpellAbility(origGameScore, candidateSAs);
if (sa != null) {
return sa;
}
createNewPlan(origGameScore, candidateSAs);
return getPlannedSpellAbility(origGameScore, candidateSAs);
}
private void createNewPlan(Score origGameScore, List<SpellAbility> candidateSAs) {
plan = null;
SimulationController controller = new SimulationController(origGameScore);
SpellAbility sa = chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore);
if (sa == null) {
print("No good plan at this time");
return;
}
plan = controller.getBestPlan();
print("New plan with score " + controller.getBestScore() + ":");
int i = 0;
for (Plan.Decision d : plan.getDecisions()) {
print(++i + ". " + d);
}
}
private SpellAbility chooseSpellAbilityToPlayImpl(SimulationController controller, List<SpellAbility> candidateSAs, Score origGameScore) {
long startTime = System.currentTimeMillis();
SpellAbility bestSa = null;
Score bestSaValue = origGameScore; Score bestSaValue = origGameScore;
print("Evaluating... (orig score = " + origGameScore + ")"); print("Evaluating... (orig score = " + origGameScore + ")");
for (final SpellAbility sa : candidateSAs) { for (final SpellAbility sa : candidateSAs) {
print(abilityToString(sa));;
Score value = evaluateSa(controller, sa); Score value = evaluateSa(controller, sa);
if (value.value > bestSaValue.value) { if (value.value > bestSaValue.value) {
bestSaValue = value; bestSaValue = value;
@@ -104,6 +142,41 @@ public class SpellAbilityPicker {
return bestSa; return bestSa;
} }
private SpellAbility getPlannedSpellAbility(Score origGameScore, List<SpellAbility> availableSAs) {
if (plan != null && plan.hasNextDecision()) {
boolean badTargets = false;
boolean saNotFound = false;
Plan.Decision decision = plan.selectNextDecision();
if (decision.initialScore.equals(origGameScore)) {
// TODO: Other safeguards like list of SAs and maybe the index and such?
for (final SpellAbility sa : availableSAs) {
if (sa.toString().equals(decision.sa)) {
if (decision.targets != null) {
PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa);
if (!selector.selectTargets(decision.targets)) {
badTargets = true;
break;
}
}
print("Planned decision " + plan.getNextDecisionIndex() + ": " + abilityToString(sa) + " " + decision.choice);
return sa;
}
}
saNotFound = true;
}
print("Failed to continue planned action (" + decision.sa + "). Cause:");
if (badTargets) {
print(" Bad targets!");
} else if (saNotFound) {
print(" Couldn't find spell/ability!");
} else {
print(" Unexpected game score (" + decision.initialScore + " vs. expected " + origGameScore + ")!");
}
plan = null;
}
return null;
}
public Score getScoreForChosenAbility() { public Score getScoreForChosenAbility() {
return bestScore; return bestScore;
} }
@@ -161,12 +234,11 @@ public class SpellAbilityPicker {
return AiPlayDecision.WillPlay; return AiPlayDecision.WillPlay;
} }
private Score evaluateSa(SimulationController controller, SpellAbility sa) { private Score evaluateSa(final SimulationController controller, SpellAbility sa) {
GameSimulator.debugPrint("Evaluate SA: " + sa); controller.evaluateSpellAbility(sa);
Score bestScore = new Score(Integer.MIN_VALUE); Score bestScore = new Score(Integer.MIN_VALUE);
if (!sa.usesTargeting()) { if (!sa.usesTargeting()) {
// TODO: Refactor this into a general decision tree.
Interceptor interceptor = new Interceptor() { Interceptor interceptor = new Interceptor() {
private int numChoices = -1; private int numChoices = -1;
private int nextChoice = 0; private int nextChoice = 0;
@@ -185,7 +257,9 @@ public class SpellAbilityPicker {
} }
numChoices = uniqueCards.size(); numChoices = uniqueCards.size();
nextChoice++; nextChoice++;
GameSimulator.debugPrint("Trying out choice " + choice); if (choice != null) {
controller.evaluateCardChoice(choice);
}
return choice; return choice;
} }
@@ -204,30 +278,33 @@ public class SpellAbilityPicker {
GameSimulator simulator = new GameSimulator(controller, game, player); GameSimulator simulator = new GameSimulator(controller, game, player);
simulator.setInterceptor(interceptor); simulator.setInterceptor(interceptor);
Score score = simulator.simulateSpellAbility(sa); Score score = simulator.simulateSpellAbility(sa);
if (interceptor.getLastChoice() != null) {
controller.doneEvaluating(score);
}
if (score.value > bestScore.value) { if (score.value > bestScore.value) {
bestScore = score; bestScore = score;
Card choice = interceptor.getLastChoice();
if (choice != null) {
bestScore.choice = choice.getName();
}
} }
} while (interceptor.hasMoreChoices()); } while (interceptor.hasMoreChoices());
controller.doneEvaluating(bestScore);
return bestScore; return bestScore;
} }
GameSimulator.debugPrint("Checking out targets");
PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa); PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa);
TargetChoices tgt = null; TargetChoices tgt = null;
while (selector.selectNextTargets()) { while (selector.selectNextTargets()) {
GameSimulator.debugPrint("Trying targets: " + sa.getTargets().getTargetedString()); controller.evaluateTargetChoices(selector.getLastSelectedTargets());
GameSimulator simulator = new GameSimulator(controller, game, player); GameSimulator simulator = new GameSimulator(controller, game, player);
Score score = simulator.simulateSpellAbility(sa); Score score = simulator.simulateSpellAbility(sa);
controller.doneEvaluating(score);
// TODO: Get rid of the below when no longer needed.
if (score.value > bestScore.value) { if (score.value > bestScore.value) {
bestScore = score; bestScore = score;
tgt = sa.getTargets(); tgt = sa.getTargets();
sa.resetTargets(); sa.resetTargets();
} }
} }
controller.doneEvaluating(bestScore);
if (tgt != null) { if (tgt != null) {
sa.setTargets(tgt); sa.setTargets(tgt);
} }
@@ -240,12 +317,15 @@ public class SpellAbilityPicker {
return interceptor.chooseCard(fetchList); return interceptor.chooseCard(fetchList);
} }
// TODO: Make the below more robust? // TODO: Make the below more robust?
if (bestScore != null && bestScore.choice != null) { if (plan != null && plan.getSelectedDecision() != null) {
String choice = plan.getSelectedDecision().choice;
for (Card c : fetchList) { for (Card c : fetchList) {
if (c.getName().equals(bestScore.choice)) { if (c.getName().equals(choice)) {
print(" Planned choice: " + c);
return c; return c;
} }
} }
print("Failed to use planned choice (" + choice + "). Not found!");
} }
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
} }

View File

@@ -10,6 +10,7 @@ import forge.GuiBase;
import forge.GuiDesktop; import forge.GuiDesktop;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.LobbyPlayerAi; import forge.ai.LobbyPlayerAi;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.deck.Deck; import forge.deck.Deck;
import forge.game.Game; import forge.game.Game;
@@ -51,7 +52,7 @@ public class GameSimulatorTest extends TestCase {
} }
private GameSimulator createSimulator(Game game, Player p) { private GameSimulator createSimulator(Game game, Player p) {
return new GameSimulator(new SimulationController(), game, p); return new GameSimulator(new SimulationController(new Score(0)), game, p);
} }
private Card findCardWithName(Game game, String name) { private Card findCardWithName(Game game, String name) {