Commit initial version of experimental AI simulation code.

It's disabled by default. Set SpellAbilityPicker.USE_SIMULATION = true to enable.

It's nowhere near complete and there are still tons of issues to work out, but it works a lot of the time.
This commit is contained in:
Myrd
2015-01-29 00:27:02 +00:00
parent f083b9055d
commit e22029463d
8 changed files with 610 additions and 1 deletions

5
.gitattributes vendored
View File

@@ -147,6 +147,11 @@ forge-ai/src/main/java/forge/ai/ability/UntapAi.java -text
forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java -text
forge-ai/src/main/java/forge/ai/ability/VoteAi.java -text
forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java -text
forge-ai/src/main/java/simulation/GameCopier.java -text
forge-ai/src/main/java/simulation/GameSimulator.java -text
forge-ai/src/main/java/simulation/GameStateEvaluator.java -text
forge-ai/src/main/java/simulation/PossibleTargetSelector.java -text
forge-ai/src/main/java/simulation/SpellAbilityPicker.java -text
forge-core/.classpath -text
forge-core/.project -text
forge-core/.settings/org.eclipse.core.resources.prefs -text

View File

@@ -28,6 +28,8 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import simulation.SpellAbilityPicker;
import com.esotericsoftware.minlog.Log;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
@@ -102,6 +104,7 @@ public class AiController {
private final Game game;
private final AiCardMemory memory;
public boolean bCheatShuffle;
private SpellAbilityPicker simPicker;
public boolean canCheatShuffle() {
return bCheatShuffle;
@@ -127,6 +130,7 @@ public class AiController {
player = computerPlayer;
game = game0;
memory = new AiCardMemory();
simPicker = new SpellAbilityPicker(game, player);
}
private CardCollection getAvailableCards() {
@@ -1206,7 +1210,11 @@ public class AiController {
private SpellAbility chooseSpellAbilityToPlay(final ArrayList<SpellAbility> all, boolean skipCounter) {
if (all == null || all.isEmpty())
return null;
SpellAbility simSa = simPicker.chooseSpellAbilityToPlay(getOriginalAndAltCostAbilities(all), skipCounter);
if (simSa != null)
return simSa;
Collections.sort(all, saComparator); // put best spells first
for (final SpellAbility sa : getOriginalAndAltCostAbilities(all)) {

View File

@@ -0,0 +1,195 @@
package simulation;
import java.util.*;
import java.util.Map.Entry;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.GameRules;
import forge.game.Match;
import forge.game.card.Card;
import forge.game.card.CardFactory;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.player.RegisteredPlayer;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
public class GameCopier {
private static final ZoneType[] ZONES = new ZoneType[] {
ZoneType.Battlefield,
ZoneType.Hand,
ZoneType.Graveyard,
ZoneType.Library,
ZoneType.Exile,
};
private Game origGame;
private BiMap<Player, Player> playerMap = HashBiMap.create();
private BiMap<Card, Card> cardMap = HashBiMap.create();
public GameCopier(Game origGame) {
this.origGame = origGame;
}
public Game makeCopy() {
List<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers();
List<RegisteredPlayer> newPlayers = new ArrayList<>();
for (RegisteredPlayer p : origPlayers) {
newPlayers.add(clonePlayer(p));
}
GameRules currentRules = origGame.getRules();
Match newMatch = new Match(currentRules, newPlayers);
Game newGame = new Game(newPlayers, currentRules, newMatch);
for (int i = 0; i < origGame.getPlayers().size(); i++) {
Player origPlayer = origGame.getPlayers().get(i);
Player newPlayer = newGame.getPlayers().get(i);
newPlayer.setLife(origPlayer.getLife(), null);
newPlayer.setActivateLoyaltyAbilityThisTurn(origPlayer.getActivateLoyaltyAbilityThisTurn());
newPlayer.setPoisonCounters(origPlayer.getPoisonCounters(), null);
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
newPlayer.setPreventNextDamage(origPlayer.getPreventNextDamage());
playerMap.put(origPlayer, newPlayer);
}
Player newPlayerTurn = playerMap.get(origGame.getPhaseHandler().getPlayerTurn());
newGame.getPhaseHandler().devModeSet(origGame.getPhaseHandler().getPhase(), newPlayerTurn);
newGame.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
copyGameState(newGame);
newGame.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
newGame.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
return newGame;
}
private RegisteredPlayer clonePlayer(RegisteredPlayer p) {
RegisteredPlayer clone = new RegisteredPlayer(p.getDeck());
clone.setPlayer(p.getPlayer());
return clone;
}
private void copyGameState(Game newGame) {
for (ZoneType zone : ZONES) {
for (Card card : origGame.getCardsIn(zone)) {
addCard(newGame, zone, card);
}
}
for (Card card : origGame.getCardsIn(ZoneType.Battlefield)) {
Card otherCard = cardMap.get(card);
if (card.isEnchanting()) {
otherCard.setEnchanting(cardMap.get(card.getEnchanting()));
}
if (card.isEquipping()) {
otherCard.setEquipping(cardMap.get(card.getEquipping()));
}
if (card.isFortifying()) {
otherCard.setFortifying(cardMap.get(card.getFortifying()));
}
if (card.getCloneOrigin() != null) {
otherCard.setCloneOrigin(cardMap.get(card.getCloneOrigin()));
}
if (card.getHaunting() != null) {
otherCard.setHaunting(cardMap.get(card.getHaunting()));
}
if (card.getEffectSource() != null) {
otherCard.setEffectSource(cardMap.get(card.getEffectSource()));
}
if (card.isPaired()) {
otherCard.setPairedWith(cardMap.get(card.getPairedWith()));
}
otherCard.setCommander(card.isCommander());
// TODO: Verify that the above relationships are preserved bi-directionally or not.
}
}
@SuppressWarnings("unchecked")
private void addCard(Game newGame, ZoneType zone, Card c) {
Player owner = playerMap.get(c.getOwner());
Card newCard = null;
if (c.isToken()) {
String tokenStr = new CardFactory.TokenInfo(c).toString();
// TODO: Use a version of the API that doesn't return a list (i.e. these shouldn't be affected
// by doubling season, etc).
newCard = CardFactory.makeToken(CardFactory.TokenInfo.fromString(tokenStr), owner).get(0);
} else {
newCard = Card.fromPaperCard(c.getPaperCard(), owner);
}
cardMap.put(c, newCard);
Player zoneOwner = owner;
if (zone == ZoneType.Battlefield) {
// TODO: Controllers' list with timestamps should be copied.
zoneOwner = playerMap.get(c.getController());
newCard.setController(zoneOwner, 0);
newCard.addTempPowerBoost(c.getTempPowerBoost());
newCard.addTempToughnessBoost(c.getTempToughnessBoost());
newCard.setChangedCardTypes(c.getChangedCardTypes());
newCard.setChangedCardKeywords(c.getChangedCardKeywords());
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such?
for (String kw : c.getHiddenExtrinsicKeywords())
newCard.addHiddenExtrinsicKeyword(kw);
newCard.setExtrinsicKeyword((ArrayList<String>) c.getExtrinsicKeyword().clone());
if (c.isTapped()) {
newCard.setTapped(true);
}
if (c.isSick()) {
newCard.setSickness(true);
}
if (c.isFaceDown()) {
newCard.setState(CardStateName.FaceDown, true);
if (c.isManifested()) {
newCard.setManifested(true);
// TODO: Should be able to copy other abilities...
newCard.addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
}
}
Map<CounterType, Integer> counters = c.getCounters();
if (!counters.isEmpty()) {
for(Entry<CounterType, Integer> kv : counters.entrySet()) {
String str = kv.getKey().toString();
int count = kv.getValue();
newCard.addCounter(CounterType.valueOf(str), count, false);
}
}
// TODO: Other chosen things...
if (c.getChosenPlayer() != null) {
newCard.setChosenPlayer(playerMap.get(c.getChosenPlayer()));
}
// TODO: FIXME
if (c.hasRemembered()) {
for (Object o : c.getRemembered()) {
System.out.println("Remembered: " + o + o.getClass());
//newCard.addRemembered(o);
}
}
}
zoneOwner.getZone(zone).add(newCard);
}
public GameObject find(GameObject o) {
GameObject result = cardMap.get(o);
if (result != null)
return result;
// TODO: Have only one GameObject map?
return playerMap.get(o);
}
public GameObject reverseFind(GameObject o) {
GameObject result = cardMap.inverse().get(o);
if (result != null)
return result;
// TODO: Have only one GameObject map?
return playerMap.inverse().get(o);
}
}

View File

@@ -0,0 +1,151 @@
package simulation;
import java.util.HashSet;
import java.util.Set;
import forge.ai.ComputerUtil;
import forge.ai.PlayerControllerAi;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.Ability;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType;
public class GameSimulator {
private Game origGame;
private GameCopier copier;
private Game simGame;
private Player aiPlayer;
private Player opponent;
private GameStateEvaluator eval;
public GameSimulator(final Game origGame) {
this.origGame = origGame;
copier = new GameCopier(origGame);
simGame = copier.makeCopy();
// TODO:
aiPlayer = simGame.getPlayers().get(1);
opponent = simGame.getPlayers().get(0);
eval = new GameStateEvaluator();
int simScore = eval.getScoreForGameState(simGame, aiPlayer, opponent);
int origScore = getScoreForOrigGame();
debugPrint = true;
if (simScore != origScore) {
// Print debug info.
eval.getScoreForGameState(simGame, aiPlayer, opponent);
getScoreForOrigGame();
throw new RuntimeException("Game copy error");
}
}
public static boolean debugPrint;
public static void debugPrint(String str) {
if (debugPrint) {
System.out.println(str);
}
}
private SpellAbility findSaInSimGame(SpellAbility sa) {
Card origHostCard = sa.getHostCard();
ZoneType zone = origHostCard.getZone().getZoneType();
for (Card c : simGame.getCardsIn(zone)) {
if (!c.getOwner().getController().isAI()) {
continue;
}
debugPrint(c.getName()+"->");
if (c.getName().equals(origHostCard.getName())) {
for (SpellAbility cSa : c.getSpellAbilities()) {
debugPrint(" "+cSa);
if (cSa.getDescription().equals(sa.getDescription())) {
return cSa;
}
}
}
}
return null;
}
public int simulateSpellAbility(SpellAbility origSa) {
// TODO: optimize: prune identical SA (e.g. two of the same card in hand)
boolean found = false;
SpellAbility sa = findSaInSimGame(origSa);
if (sa != null) {
found = true;
} else {
System.err.println("SA not found! " + sa);
return Integer.MIN_VALUE;
}
Player origActivatingPlayer = sa.getActivatingPlayer();
sa.setActivatingPlayer(aiPlayer);
if (origSa.usesTargeting()) {
for (GameObject o : origSa.getTargets().getTargets()) {
debugPrint("Copying over target " +o);
debugPrint(" found: "+copier.find(o));
sa.getTargets().add(copier.find(o));
}
}
debugPrint("Simulating playing sa: " + sa + " found="+found);
if (sa == Ability.PLAY_LAND_SURROGATE) {
aiPlayer.playLand(sa.getHostCard(), false);
}
else {
// TODO: should simulate all possible targets...
if (!sa.getAllTargetChoices().isEmpty()) {
debugPrint("Targets: ");
for (TargetChoices target : sa.getAllTargetChoices()) {
System.out.print(target.getTargetedString());
}
System.out.println();
}
ComputerUtil.handlePlayingSpellAbility(aiPlayer, sa, simGame);
}
if (simGame.getStack().isEmpty()) {
System.err.println("Stack empty: " + sa);
return Integer.MIN_VALUE;
}
opponent.runWithController(new Runnable() {
@Override
public void run() {
final Set<Card> allAffectedCards = new HashSet<Card>();
do {
// Resolve the top effect on the stack.
simGame.getStack().resolveStack();
// Evaluate state based effects as a result of resolving stack.
// Note: Needs to happen after resolve stack rather than at the
// top of the loop to ensure state effects are evaluated after the
// last resolved effect
simGame.getAction().checkStateEffects(false, allAffectedCards);
// Add any triggers as a result of resolving the effect.
simGame.getStack().addAllTriggeredAbilitiesToStack();
// Continue until stack is empty.
} while (!simGame.getStack().isEmpty() && !simGame.isGameOver());
}
}, new PlayerControllerAi(simGame, opponent, opponent.getLobbyPlayer()));
// TODO: If this is during combat, before blockers are declared,
// we should simulate how combat will resolve and evaluate that
// state instead!
debugPrint("SimGame:");
int score = eval.getScoreForGameState(simGame, aiPlayer, opponent);
sa.setActivatingPlayer(origActivatingPlayer);
return score;
}
public int getScoreForOrigGame() {
// TODO: Make this logic more bulletproof.
Player origAiPlayer = origGame.getPlayers().get(1);
Player origOpponent = origGame.getPlayers().get(0);
return eval.getScoreForGameState(origGame, origAiPlayer, origOpponent);
}
}

View File

@@ -0,0 +1,73 @@
package simulation;
import forge.ai.ComputerUtilCard;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
public class GameStateEvaluator {
public int getScoreForGameState(Game game, Player aiPlayer, Player opponent) {
if (game.isGameOver()) {
return game.getOutcome().getWinningPlayer() == aiPlayer ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
int score = 0;
// TODO: more than 2 players
int myCards = 0;
int theirCards = 0;
for (Card c : game.getCardsIn(ZoneType.Hand)) {
if (c.getController() == aiPlayer) {
myCards++;
} else {
theirCards++;
}
}
GameSimulator.debugPrint("My cards in hand: " + myCards);
GameSimulator.debugPrint("Their cards in hand: " + theirCards);
score += 3 * myCards - 3 * theirCards;
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
int value = evalCard(c);
String str = c.toString();
if (c.isCreature()) {
str += " " + c.getNetPower() + "/" + c.getNetToughness();
}
if (c.getController() == aiPlayer) {
GameSimulator.debugPrint(" Battlefield: " + str + " = " + value);
score += value;
} else {
GameSimulator.debugPrint(" Battlefield: " + str + " = -" + value);
score -= value;
}
String nonAbilityText = c.getNonAbilityText();
if (!nonAbilityText.isEmpty()) {
GameSimulator.debugPrint(" "+nonAbilityText.replaceAll("CARDNAME", c.getName()));
}
}
GameSimulator.debugPrint(" My life: " + aiPlayer.getLife());
score += aiPlayer.getLife();
GameSimulator.debugPrint(" Opponent life: -" + opponent.getLife());
score -= opponent.getLife();
GameSimulator.debugPrint("Score = " + score);
return score;
}
private static int evalCard(Card c) {
// TODO: These should be based on other considerations - e.g. in relation to opponents state.
if (c.isCreature()) {
return ComputerUtilCard.evaluateCreature(c);
} else if (c.isLand()) {
return 100;
} else if (c.isEnchantingCard()) {
// TODO: Should provide value in whatever it's enchanting?
// Else the computer would think that casting a Lifelink enchantment
// on something that already has lifelink is a net win.
return 0;
} else {
// e.g. a 5 CMC permanent results in 200, whereas a 5/5 creature is ~225
return 50 + 30 * c.getCMC();
}
}
}

View File

@@ -0,0 +1,50 @@
package simulation;
import java.util.ArrayList;
import java.util.List;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.card.CardUtil;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
public class PossibleTargetSelector {
private SpellAbility sa;
private TargetRestrictions tgt;
private int targetIndex;
private List<GameObject> validTargets;
public PossibleTargetSelector(Game game, Player self, SpellAbility sa) {
this.sa = sa;
this.tgt = sa.getTargetRestrictions();
this.targetIndex = 0;
this.validTargets = new ArrayList<GameObject>();
if (tgt.canTgtPermanent() || tgt.canTgtCreature()) {
// TODO: What about things that target enchantments and such?
validTargets.addAll(CardUtil.getValidCardsToTarget(tgt, sa));
}
if (tgt.canTgtPlayer()) {
for (Player p : game.getPlayers()) {
if (p != self || !tgt.canOnlyTgtOpponent()) {
validTargets.add(p);
}
}
}
}
public boolean selectNextTargets() {
if (targetIndex >= validTargets.size()) {
return false;
}
sa.resetTargets();
int index = targetIndex;
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
sa.getTargets().add(validTargets.get(index++));
}
// TODO: smarter about multiple targets, identical targets, etc...
targetIndex++;
return true;
}
}

View File

@@ -0,0 +1,109 @@
package simulation;
import java.util.ArrayList;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCost;
import forge.game.Game;
import forge.game.ability.ApiType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
public class SpellAbilityPicker {
private static boolean USE_SIMULATION = false;
private Game game;
private Player player;
public SpellAbilityPicker(Game game, Player player) {
this.game = game;
this.player = player;
}
public SpellAbility chooseSpellAbilityToPlay(final ArrayList<SpellAbility> originalAndAltCostAbilities, boolean skipCounter) {
if (!USE_SIMULATION)
return null;
System.out.println("----\nchooseSpellAbilityToPlay game " + game.toString());
System.out.println("---- (phase = " + game.getPhaseHandler().getPhase() + ")");
ArrayList<SpellAbility> candidateSAs = new ArrayList<>();
for (final SpellAbility sa : originalAndAltCostAbilities) {
// Don't add Counterspells to the "normal" playcard lookups
if (skipCounter && sa.getApi() == ApiType.Counter) {
continue;
}
sa.setActivatingPlayer(player);
AiPlayDecision opinion = canPlayAndPayForSim(sa);
System.out.println(" " + opinion + ": " + sa);
// PhaseHandler ph = game.getPhaseHandler();
// System.out.printf("Ai thinks '%s' of %s -> %s @ %s %s >>> \n", opinion, sa.getHostCard(), sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase());
if (opinion != AiPlayDecision.WillPlay)
continue;
candidateSAs.add(sa);
}
if (candidateSAs.isEmpty()) {
return null;
}
SpellAbility bestSa = null;
System.out.println("Evaluating...");
GameSimulator simulator = new GameSimulator(game);
// FIXME: This is wasteful, we should re-use the same simulator...
int bestSaValue = simulator.getScoreForOrigGame();
for (final SpellAbility sa : candidateSAs) {
int value = evaluateSa(sa);
if (value > bestSaValue) {
bestSaValue = value;
bestSa = sa;
}
}
System.out.println("BEST: " + bestSa + " SCORE: " + bestSaValue);
return bestSa;
}
private AiPlayDecision canPlayAndPayForSim(final SpellAbility sa) {
if (!sa.canPlay()) {
return AiPlayDecision.CantPlaySa;
}
if (sa.getConditions() != null && !sa.getConditions().areMet(sa)) {
return AiPlayDecision.CantPlaySa;
}
/*
AiPlayDecision op = canPlaySa(sa);
if (op != AiPlayDecision.WillPlay) {
return op;
}
*/
return ComputerUtilCost.canPayCost(sa, player) ? AiPlayDecision.WillPlay : AiPlayDecision.CantAfford;
}
private int evaluateSa(SpellAbility sa) {
System.out.println("Evaluate SA: " + sa);
if (!sa.usesTargeting()) {
GameSimulator simulator = new GameSimulator(game);
return simulator.simulateSpellAbility(sa);
}
PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa);
int bestScore = Integer.MIN_VALUE;
TargetChoices tgt = null;
while (selector.selectNextTargets()) {
System.out.println("Trying targets: " + sa.getTargets().getTargetedString());
GameSimulator simulator = new GameSimulator(game);
int score = simulator.simulateSpellAbility(sa);
if (score > bestScore) {
bestScore = score;
tgt = sa.getTargets();
sa.resetTargets();
}
}
if (tgt != null) {
sa.setTargets(tgt);
}
return bestScore;
}
}

View File

@@ -2396,6 +2396,10 @@ public class Card extends GameEntity implements Comparable<Card> {
public Map<Long, CardChangedType> getChangedCardTypes() {
return Collections.unmodifiableMap(changedCardTypes);
}
public Map<Long, KeywordsChange> getChangedCardKeywords() {
return changedCardKeywords;
}
public final void addChangedCardTypes(final CardType addType, final CardType removeType,
final boolean removeSuperTypes, final boolean removeCardTypes, final boolean removeSubTypes,
@@ -6443,4 +6447,18 @@ public class Card extends GameEntity implements Comparable<Card> {
return keywords;
}
}
public void setChangedCardTypes(Map<Long, CardChangedType> changedCardTypes) {
this.changedCardTypes.clear();
for (Entry<Long, CardChangedType> entry : changedCardTypes.entrySet()) {
this.changedCardTypes.put(entry.getKey(), entry.getValue());
}
}
public void setChangedCardKeywords(Map<Long, KeywordsChange> changedCardKeywords) {
this.changedCardKeywords.clear();
for (Entry<Long, KeywordsChange> entry : changedCardKeywords.entrySet()) {
this.changedCardKeywords.put(entry.getKey(), entry.getValue());
}
}
}