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/UntapAllAi.java -text
forge-ai/src/main/java/forge/ai/ability/VoteAi.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/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/.classpath -text
forge-core/.project -text forge-core/.project -text
forge-core/.settings/org.eclipse.core.resources.prefs -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;
import java.util.Map.Entry; import java.util.Map.Entry;
import simulation.SpellAbilityPicker;
import com.esotericsoftware.minlog.Log; import com.esotericsoftware.minlog.Log;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
@@ -102,6 +104,7 @@ public class AiController {
private final Game game; private final Game game;
private final AiCardMemory memory; private final AiCardMemory memory;
public boolean bCheatShuffle; public boolean bCheatShuffle;
private SpellAbilityPicker simPicker;
public boolean canCheatShuffle() { public boolean canCheatShuffle() {
return bCheatShuffle; return bCheatShuffle;
@@ -127,6 +130,7 @@ public class AiController {
player = computerPlayer; player = computerPlayer;
game = game0; game = game0;
memory = new AiCardMemory(); memory = new AiCardMemory();
simPicker = new SpellAbilityPicker(game, player);
} }
private CardCollection getAvailableCards() { private CardCollection getAvailableCards() {
@@ -1206,7 +1210,11 @@ public class AiController {
private SpellAbility chooseSpellAbilityToPlay(final ArrayList<SpellAbility> all, boolean skipCounter) { private SpellAbility chooseSpellAbilityToPlay(final ArrayList<SpellAbility> all, boolean skipCounter) {
if (all == null || all.isEmpty()) if (all == null || all.isEmpty())
return null; return null;
SpellAbility simSa = simPicker.chooseSpellAbilityToPlay(getOriginalAndAltCostAbilities(all), skipCounter);
if (simSa != null)
return simSa;
Collections.sort(all, saComparator); // put best spells first Collections.sort(all, saComparator); // put best spells first
for (final SpellAbility sa : getOriginalAndAltCostAbilities(all)) { 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() { public Map<Long, CardChangedType> getChangedCardTypes() {
return Collections.unmodifiableMap(changedCardTypes); return Collections.unmodifiableMap(changedCardTypes);
} }
public Map<Long, KeywordsChange> getChangedCardKeywords() {
return changedCardKeywords;
}
public final void addChangedCardTypes(final CardType addType, final CardType removeType, public final void addChangedCardTypes(final CardType addType, final CardType removeType,
final boolean removeSuperTypes, final boolean removeCardTypes, final boolean removeSubTypes, final boolean removeSuperTypes, final boolean removeCardTypes, final boolean removeSubTypes,
@@ -6443,4 +6447,18 @@ public class Card extends GameEntity implements Comparable<Card> {
return keywords; 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());
}
}
} }