[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/GameSimulator.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/SimulationController.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);
if (controller.shouldRecurse() && !simGame.isGameOver()) {
controller.push(sa);
controller.push(sa, score);
SpellAbilityPicker sim = new SpellAbilityPicker(simGame, aiPlayer);
CardCollection cards = ComputerUtilAbility.getAvailableCards(simGame, aiPlayer);
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, aiPlayer);

View File

@@ -79,9 +79,9 @@ public class GameStateEvaluator {
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
int value = evalCard(game, aiPlayer, c, combat);
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.
if (gamePhase == PhaseType.MAIN1 && c.isSick() && c.getController() == aiPlayer) {
if (gamePhase.isBefore(PhaseType.MAIN2) && c.isSick() && c.getController() == aiPlayer) {
summonSickValue = 0;
}
String str = c.getName();
@@ -167,7 +167,6 @@ public class GameStateEvaluator {
public static class Score {
public final int value;
public final int summonSickValue;
public String choice;
public Score(int 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 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) {
this.sa = sa;
this.tgt = sa.getTargetRestrictions();
this.targetIndex = 0;
this.validTargets = new ArrayList<GameObject>();
sa.resetTargets();
sa.setActivatingPlayer(self);
for (GameObject o : tgt.getAllCandidates(sa, true)) {
validTargets.add(o);
}
}
public boolean selectNextTargets() {
if (targetIndex >= validTargets.size()) {
return false;
}
private void selectTargetsByIndex(int index) {
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()) {
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++;
return true;
}

View File

@@ -1,6 +1,11 @@
package forge.ai.simulation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
public class SimulationController {
@@ -8,21 +13,93 @@ public class SimulationController {
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() {
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(" With: " + sa);
recursionDepth++;
scoreStack.add(score);
}
public void pop(Score score, SpellAbility nextSa) {
recursionDepth--;
scoreStack.remove(scoreStack.size() - 1);
GameSimulator.debugPrint("DEPTH"+recursionDepth+" best score " + score + " " + nextSa);
}

View File

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

View File

@@ -10,6 +10,7 @@ import forge.GuiBase;
import forge.GuiDesktop;
import forge.ai.ComputerUtilAbility;
import forge.ai.LobbyPlayerAi;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.card.CardStateName;
import forge.deck.Deck;
import forge.game.Game;
@@ -51,7 +52,7 @@ public class GameSimulatorTest extends TestCase {
}
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) {