Simulated AI: Evaluate different fetch targets.

This lets the AI figure out such sequences as:

Game state:

In play: Urborg, Thespian's stage & fetch land
In hand: Crop rotation

Sequence:
  1. Fetch for a forest.
  2. Crop rotate forest into Dark Depths.
  3. Activate Thespian's stage targeting Dark Depths.
  4. Profit!
This commit is contained in:
Myrd
2016-12-23 17:50:26 +00:00
parent bd1e99bc2e
commit c272cde429
5 changed files with 106 additions and 8 deletions

View File

@@ -33,6 +33,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName;
import forge.card.MagicColor;
@@ -115,6 +116,10 @@ public class AiController {
this.useSimulation = value;
}
public SpellAbilityPicker getSimulationPicker() {
return simPicker;
}
public Game getGame() {
return game;
}
@@ -1556,5 +1561,12 @@ public class AiController {
return MyRandom.getRandom().nextBoolean();
}
public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa,
CardCollection fetchList, Player player2, Player decider) {
if (useSimulation) {
return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
}
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
}
}

View File

@@ -16,7 +16,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import forge.LobbyPlayer;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.ProtectAi;
import forge.card.ColorSet;
import forge.card.ICardFace;
@@ -864,11 +863,10 @@ public class PlayerControllerAi extends PlayerController {
public Card chooseSingleCardForZoneChange(ZoneType destination,
List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, DelayedReveal delayedReveal,
String selectPrompt, boolean isOptional, Player decider) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player, decider);
return brains.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player, decider);
}
@Override

View File

@@ -10,6 +10,7 @@ import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.PlayerControllerAi;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.ai.simulation.SpellAbilityPicker.Interceptor;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.card.Card;
@@ -74,6 +75,10 @@ public class GameSimulator {
debugLines = null;
}
public void setInterceptor(Interceptor interceptor) {
((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor);
}
private void printDiff(List<String> lines1, List<String> lines2) {
int i = 0;
int j = 0;

View File

@@ -167,6 +167,7 @@ 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;
@@ -183,5 +184,9 @@ public class GameStateEvaluator {
return false;
return value == other.value && summonSickValue == other.summonSickValue;
}
public String toString() {
return value + (summonSickValue != value ? " (ss " + summonSickValue + ")" :"");
}
}
}

View File

@@ -1,29 +1,39 @@
package forge.ai.simulation;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType;
public class SpellAbilityPicker {
private Game game;
private Player player;
private Score bestScore;
private boolean printOutput;
private Interceptor interceptor;
public SpellAbilityPicker(Game game, Player player) {
this.game = game;
this.player = player;
}
public void setInterceptor(Interceptor in) {
this.interceptor = in;
}
private void print(String str) {
if (printOutput) {
@@ -65,11 +75,11 @@ public class SpellAbilityPicker {
return null;
}
SpellAbility bestSa = null;
print("Evaluating...");
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);
@@ -153,13 +163,60 @@ public class SpellAbilityPicker {
private Score evaluateSa(SimulationController controller, SpellAbility sa) {
GameSimulator.debugPrint("Evaluate SA: " + sa);
Score bestScore = new Score(Integer.MIN_VALUE);
if (!sa.usesTargeting()) {
GameSimulator simulator = new GameSimulator(controller, game, player);
return simulator.simulateSpellAbility(sa);
// TODO: Refactor this into a general decision tree.
Interceptor interceptor = new Interceptor() {
private int numChoices = -1;
private int nextChoice = 0;
private Card choice;
@Override
public Card chooseCard(CardCollection fetchList) {
choice = null;
// Prune duplicates.
HashSet<String> uniqueCards = new HashSet<String>();
for (int i = 0; i < fetchList.size(); i++) {
Card card = fetchList.get(i);
if (uniqueCards.add(card.getName()) && uniqueCards.size() == nextChoice + 1) {
choice = card;
}
}
numChoices = uniqueCards.size();
nextChoice++;
GameSimulator.debugPrint("Trying out choice " + choice);
return choice;
}
@Override
public Card getLastChoice() {
return choice;
}
@Override
public boolean hasMoreChoices() {
return nextChoice < numChoices;
}
};
do {
GameSimulator simulator = new GameSimulator(controller, game, player);
simulator.setInterceptor(interceptor);
Score score = simulator.simulateSpellAbility(sa);
if (score.value > bestScore.value) {
bestScore = score;
Card choice = interceptor.getLastChoice();
if (choice != null) {
bestScore.choice = choice.getName();
}
}
} while (interceptor.hasMoreChoices());
return bestScore;
}
GameSimulator.debugPrint("Checking out targets");
PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa);
Score bestScore = new Score(Integer.MIN_VALUE);
TargetChoices tgt = null;
while (selector.selectNextTargets()) {
GameSimulator.debugPrint("Trying targets: " + sa.getTargets().getTargetedString());
@@ -177,4 +234,25 @@ public class SpellAbilityPicker {
return bestScore;
}
public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa,
CardCollection fetchList, Player player2, Player decider) {
if (interceptor != null) {
return interceptor.chooseCard(fetchList);
}
// TODO: Make the below more robust?
if (bestScore != null && bestScore.choice != null) {
for (Card c : fetchList) {
if (c.getName().equals(bestScore.choice)) {
return c;
}
}
}
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
}
public interface Interceptor {
public Card chooseCard(CardCollection fetchList);
public Card getLastChoice();
public boolean hasMoreChoices();
}
}