mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 20:58:03 +00:00
[Simulated AI] Refactor SpellAbilityChoicesIterator to its own class.
This commit is contained in:
@@ -9,7 +9,6 @@ import java.util.Set;
|
||||
import forge.ai.ComputerUtil;
|
||||
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;
|
||||
@@ -27,8 +26,8 @@ public class GameSimulator {
|
||||
private GameStateEvaluator eval;
|
||||
private List<String> origLines;
|
||||
private Score origScore;
|
||||
private Interceptor interceptor;
|
||||
|
||||
private SpellAbilityChoicesIterator interceptor;
|
||||
|
||||
public GameSimulator(final SimulationController controller, final Game origGame, final Player origAiPlayer) {
|
||||
this.controller = controller;
|
||||
copier = new GameCopier(origGame);
|
||||
@@ -74,7 +73,7 @@ public class GameSimulator {
|
||||
debugLines = null;
|
||||
}
|
||||
|
||||
public void setInterceptor(Interceptor interceptor) {
|
||||
public void setInterceptor(SpellAbilityChoicesIterator interceptor) {
|
||||
this.interceptor = interceptor;
|
||||
((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package forge.ai.simulation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.apache.commons.math3.util.CombinatoricsUtils;
|
||||
|
||||
import forge.ai.simulation.GameStateEvaluator.Score;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class SpellAbilityChoicesIterator {
|
||||
private SimulationController controller;
|
||||
|
||||
private Iterator<int[]> modeIterator;
|
||||
private int[] selectedModes;
|
||||
private Score bestScoreForMode = new Score(Integer.MIN_VALUE);
|
||||
private boolean advancedToNextMode;
|
||||
|
||||
private Score[] cachedTargetScores;
|
||||
private int nextTarget = 0;
|
||||
private Score bestScoreForTarget = new Score(Integer.MIN_VALUE);
|
||||
|
||||
private int numChoices = -1;
|
||||
private int nextChoice = 0;
|
||||
private Card selectedChoice;
|
||||
private Score bestScoreForChoice = new Score(Integer.MIN_VALUE);
|
||||
|
||||
public SpellAbilityChoicesIterator(SimulationController controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public List<AbilitySub> chooseModesForAbility(List<AbilitySub> choices, final int min, final int num, boolean allowRepeat) {
|
||||
if (modeIterator == null) {
|
||||
// TODO: Need to skip modes that are invalid (e.g. targets don't exist)!
|
||||
// TODO: Do we need to do something special to support cards that have extra costs
|
||||
// when choosing more modes, like Blessed Alliance?
|
||||
if (!allowRepeat) {
|
||||
modeIterator = CombinatoricsUtils.combinationsIterator(choices.size(), num);;
|
||||
} else {
|
||||
// Note: When allowRepeat is true, it does result in many possibilities being tried.
|
||||
// We should ideally prune some of those at a higher level.
|
||||
final int numChoices = choices.size();
|
||||
modeIterator = new Iterator<int[]>() {
|
||||
int[] indexes = new int[min];
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return indexes != null;
|
||||
}
|
||||
|
||||
// Note: This returns a new int[] array and doesn't modify indexes in place,
|
||||
// since that gets returned to the caller.
|
||||
private int[] getNextIndexes() {
|
||||
for (int i = indexes.length - 1; i >= 0; i--) {
|
||||
if (indexes[i] < numChoices - 1) {
|
||||
int[] nextIndexes = new int[indexes.length];
|
||||
System.arraycopy(indexes, 0, nextIndexes, 0, i);
|
||||
nextIndexes[i] = indexes[i] + 1;
|
||||
return nextIndexes;
|
||||
}
|
||||
}
|
||||
if (indexes.length < num) {
|
||||
return new int[indexes.length + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] next() {
|
||||
if (indexes == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
int[] result = indexes;
|
||||
indexes = getNextIndexes();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
selectedModes = modeIterator.next();
|
||||
advancedToNextMode = true;
|
||||
}
|
||||
// Note: If modeIterator already existed, selectedModes would have been updated in advance().
|
||||
List<AbilitySub> result = getModeCombination(choices, selectedModes);
|
||||
if (advancedToNextMode) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (AbilitySub sub : result) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(" ");
|
||||
} else {
|
||||
sb.append(sub.getHostCard()).append(" -> ");
|
||||
}
|
||||
sb.append(sub);
|
||||
}
|
||||
controller.evaluateChosenModes(selectedModes, sb.toString());
|
||||
advancedToNextMode = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Card chooseCard(CardCollection fetchList) {
|
||||
// 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) {
|
||||
selectedChoice = card;
|
||||
}
|
||||
}
|
||||
numChoices = uniqueCards.size();
|
||||
if (selectedChoice != null) {
|
||||
controller.evaluateCardChoice(selectedChoice);
|
||||
}
|
||||
return selectedChoice;
|
||||
}
|
||||
|
||||
public void chooseTargets(SpellAbility sa, GameSimulator simulator) {
|
||||
// Note: Can't just keep a TargetSelector object cached because it's
|
||||
// responsible for setting state on a SA and the SA object changes each
|
||||
// time since it's a different simulation.
|
||||
PossibleTargetSelector selector = new PossibleTargetSelector(sa);
|
||||
if (selector.hasPossibleTargets()) {
|
||||
if (cachedTargetScores == null) {
|
||||
cachedTargetScores = new Score[selector.getValidTargetsSize()];
|
||||
nextTarget = -1;
|
||||
for (int i = 0; i < cachedTargetScores.length; i++) {
|
||||
selector.selectTargetsByIndex(i);
|
||||
cachedTargetScores[i] = controller.shouldSkipTarget(sa, selector.getLastSelectedTargets(), simulator);
|
||||
if (cachedTargetScores[i] != null) {
|
||||
controller.printState(cachedTargetScores[i], sa, " - via estimate (skipped)", false);
|
||||
} else if (nextTarget == -1) {
|
||||
nextTarget = i;
|
||||
}
|
||||
}
|
||||
// If all targets were cached, we unfortunately have to evaluate the first target again
|
||||
// because at this point we're already running the simulation code and there's no turning
|
||||
// back. This used to be not possible when the PossibleTargetSelector was controlling the
|
||||
// flow. :(
|
||||
if (nextTarget == -1) { nextTarget = 0; }
|
||||
}
|
||||
selector.selectTargetsByIndex(nextTarget);
|
||||
controller.setHostAndTarget(sa, simulator);
|
||||
// The hierarchy is modes -> targets -> choices. In the presence of multiple choices, we want to call
|
||||
// evaluate just once at the top level. We can do this by only calling when numChoices is -1.
|
||||
if (numChoices == -1) {
|
||||
controller.evaluateTargetChoices(sa, selector.getLastSelectedTargets());
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public Card getSelectedChoice() {
|
||||
return selectedChoice;
|
||||
}
|
||||
|
||||
public int[] getSelectModes() {
|
||||
return selectedModes;
|
||||
}
|
||||
|
||||
public boolean advance(Score lastScore) {
|
||||
if (lastScore.value > bestScoreForChoice.value) {
|
||||
bestScoreForChoice = lastScore;
|
||||
}
|
||||
if (lastScore.value > bestScoreForTarget.value) {
|
||||
bestScoreForTarget = lastScore;
|
||||
}
|
||||
if (lastScore.value > bestScoreForMode.value) {
|
||||
bestScoreForMode = lastScore;
|
||||
}
|
||||
|
||||
if (numChoices != -1) {
|
||||
if (selectedChoice != null) {
|
||||
controller.doneEvaluating(bestScoreForChoice);
|
||||
}
|
||||
bestScoreForChoice = new Score(Integer.MIN_VALUE);
|
||||
selectedChoice = null;
|
||||
if (nextChoice + 1 < numChoices) {
|
||||
nextChoice++;
|
||||
return true;
|
||||
}
|
||||
nextChoice = 0;
|
||||
numChoices = -1;
|
||||
}
|
||||
if (cachedTargetScores != null) {
|
||||
controller.doneEvaluating(bestScoreForTarget);
|
||||
bestScoreForTarget = new Score(Integer.MIN_VALUE);
|
||||
while (nextTarget + 1 < cachedTargetScores.length) {
|
||||
nextTarget++;
|
||||
if (cachedTargetScores[nextTarget] == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
nextTarget = -1;
|
||||
cachedTargetScores = null;
|
||||
}
|
||||
if (modeIterator != null) {
|
||||
controller.doneEvaluating(bestScoreForMode);
|
||||
bestScoreForMode = new Score(Integer.MIN_VALUE);
|
||||
if (modeIterator.hasNext()) {
|
||||
selectedModes = modeIterator.next();
|
||||
advancedToNextMode = true;
|
||||
return true;
|
||||
}
|
||||
modeIterator = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static List<AbilitySub> getModeCombination(List<AbilitySub> choices, int[] modeIndexes) {
|
||||
ArrayList<AbilitySub> modes = new ArrayList<AbilitySub>();
|
||||
for (int modeIndex : modeIndexes) {
|
||||
modes.add(choices.get(modeIndex));
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
package forge.ai.simulation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.apache.commons.math3.util.CombinatoricsUtils;
|
||||
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
@@ -32,7 +26,7 @@ public class SpellAbilityPicker {
|
||||
private Player player;
|
||||
private Score bestScore;
|
||||
private boolean printOutput;
|
||||
private Interceptor interceptor;
|
||||
private SpellAbilityChoicesIterator interceptor;
|
||||
|
||||
private Plan plan;
|
||||
|
||||
@@ -41,7 +35,7 @@ public class SpellAbilityPicker {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public void setInterceptor(Interceptor in) {
|
||||
public void setInterceptor(SpellAbilityChoicesIterator in) {
|
||||
this.interceptor = in;
|
||||
}
|
||||
|
||||
@@ -301,225 +295,21 @@ public class SpellAbilityPicker {
|
||||
return AiPlayDecision.WillPlay;
|
||||
}
|
||||
|
||||
private static List<AbilitySub> getModeCombination(List<AbilitySub> choices, int[] modeIndexes) {
|
||||
ArrayList<AbilitySub> modes = new ArrayList<AbilitySub>();
|
||||
for (int modeIndex : modeIndexes) {
|
||||
modes.add(choices.get(modeIndex));
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
private Score evaluateSa(final SimulationController controller, List<SpellAbility> saList, int saIndex) {
|
||||
controller.evaluateSpellAbility(saList, saIndex);
|
||||
SpellAbility sa = saList.get(saIndex);
|
||||
|
||||
Score bestScore = new Score(Integer.MIN_VALUE);
|
||||
Interceptor interceptor = new Interceptor() {
|
||||
private Iterator<int[]> modeIterator;
|
||||
private int[] selectedModes;
|
||||
private Score bestScoreForMode = new Score(Integer.MIN_VALUE);
|
||||
private boolean advancedToNextMode;
|
||||
|
||||
private Score[] cachedTargetScores;
|
||||
private int nextTarget = 0;
|
||||
private Score bestScoreForTarget = new Score(Integer.MIN_VALUE);
|
||||
|
||||
private int numChoices = -1;
|
||||
private int nextChoice = 0;
|
||||
private Card selectedChoice;
|
||||
private Score bestScoreForChoice = new Score(Integer.MIN_VALUE);
|
||||
|
||||
public List<AbilitySub> chooseModesForAbility(List<AbilitySub> choices, final int min, final int num, boolean allowRepeat) {
|
||||
if (modeIterator == null) {
|
||||
// TODO: Need to skip modes that are invalid (e.g. targets don't exist)!
|
||||
// TODO: Do we need to do something special to support cards that have extra costs
|
||||
// when choosing more modes, like Blessed Alliance?
|
||||
if (!allowRepeat) {
|
||||
modeIterator = CombinatoricsUtils.combinationsIterator(choices.size(), num);;
|
||||
} else {
|
||||
// Note: When allowRepeat is true, it does result in many possibilities being tried.
|
||||
// We should ideally prune some of those at a higher level.
|
||||
final int numChoices = choices.size();
|
||||
modeIterator = new Iterator<int[]>() {
|
||||
int[] indexes = new int[min];
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return indexes != null;
|
||||
}
|
||||
|
||||
// Note: This returns a new int[] array and doesn't modify indexes in place,
|
||||
// since that gets returned to the caller.
|
||||
private int[] getNextIndexes() {
|
||||
for (int i = indexes.length - 1; i >= 0; i--) {
|
||||
if (indexes[i] < numChoices - 1) {
|
||||
int[] nextIndexes = new int[indexes.length];
|
||||
System.arraycopy(indexes, 0, nextIndexes, 0, i);
|
||||
nextIndexes[i] = indexes[i] + 1;
|
||||
return nextIndexes;
|
||||
}
|
||||
}
|
||||
if (indexes.length < num) {
|
||||
return new int[indexes.length + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] next() {
|
||||
if (indexes == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
int[] result = indexes;
|
||||
indexes = getNextIndexes();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
selectedModes = modeIterator.next();
|
||||
advancedToNextMode = true;
|
||||
}
|
||||
// Note: If modeIterator already existed, selectedModes would have been updated in advance().
|
||||
List<AbilitySub> result = getModeCombination(choices, selectedModes);
|
||||
if (advancedToNextMode) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (AbilitySub sub : result) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(" ");
|
||||
} else {
|
||||
sb.append(sub.getHostCard()).append(" -> ");
|
||||
}
|
||||
sb.append(sub);
|
||||
}
|
||||
controller.evaluateChosenModes(selectedModes, sb.toString());
|
||||
advancedToNextMode = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Card chooseCard(CardCollection fetchList) {
|
||||
// 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) {
|
||||
selectedChoice = card;
|
||||
}
|
||||
}
|
||||
numChoices = uniqueCards.size();
|
||||
if (selectedChoice != null) {
|
||||
controller.evaluateCardChoice(selectedChoice);
|
||||
}
|
||||
return selectedChoice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chooseTargets(SpellAbility sa, GameSimulator simulator) {
|
||||
// Note: Can't just keep a TargetSelector object cached because it's
|
||||
// responsible for setting state on a SA and the SA object changes each
|
||||
// time since it's a different simulation.
|
||||
PossibleTargetSelector selector = new PossibleTargetSelector(sa);
|
||||
if (selector.hasPossibleTargets()) {
|
||||
if (cachedTargetScores == null) {
|
||||
cachedTargetScores = new Score[selector.getValidTargetsSize()];
|
||||
nextTarget = -1;
|
||||
for (int i = 0; i < cachedTargetScores.length; i++) {
|
||||
selector.selectTargetsByIndex(i);
|
||||
cachedTargetScores[i] = controller.shouldSkipTarget(sa, selector.getLastSelectedTargets(), simulator);
|
||||
if (cachedTargetScores[i] != null) {
|
||||
controller.printState(cachedTargetScores[i], sa, " - via estimate (skipped)", false);
|
||||
} else if (nextTarget == -1) {
|
||||
nextTarget = i;
|
||||
}
|
||||
}
|
||||
// If all targets were cached, we unfortunately have to evaluate the first target again
|
||||
// because at this point we're already running the simulation code and there's no turning
|
||||
// back. This used to be not possible when the PossibleTargetSelector was controlling the
|
||||
// flow. :(
|
||||
if (nextTarget == -1) { nextTarget = 0; }
|
||||
}
|
||||
selector.selectTargetsByIndex(nextTarget);
|
||||
controller.setHostAndTarget(sa, simulator);
|
||||
// The hierarchy is modes -> targets -> choices. In the presence of multiple choices, we want to call
|
||||
// evaluate just once at the top level. We can do this by only calling when numChoices is -1.
|
||||
if (numChoices == -1) {
|
||||
controller.evaluateTargetChoices(sa, selector.getLastSelectedTargets());
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Card getSelectedChoice() {
|
||||
return selectedChoice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSelectModes() {
|
||||
return selectedModes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean advance(Score lastScore) {
|
||||
if (lastScore.value > bestScoreForChoice.value) {
|
||||
bestScoreForChoice = lastScore;
|
||||
}
|
||||
if (lastScore.value > bestScoreForTarget.value) {
|
||||
bestScoreForTarget = lastScore;
|
||||
}
|
||||
if (lastScore.value > bestScoreForMode.value) {
|
||||
bestScoreForMode = lastScore;
|
||||
}
|
||||
|
||||
if (numChoices != -1) {
|
||||
if (selectedChoice != null) {
|
||||
controller.doneEvaluating(bestScoreForChoice);
|
||||
}
|
||||
bestScoreForChoice = new Score(Integer.MIN_VALUE);
|
||||
selectedChoice = null;
|
||||
if (nextChoice + 1 < numChoices) {
|
||||
nextChoice++;
|
||||
return true;
|
||||
}
|
||||
nextChoice = 0;
|
||||
numChoices = -1;
|
||||
}
|
||||
if (cachedTargetScores != null) {
|
||||
controller.doneEvaluating(bestScoreForTarget);
|
||||
bestScoreForTarget = new Score(Integer.MIN_VALUE);
|
||||
while (nextTarget + 1 < cachedTargetScores.length) {
|
||||
nextTarget++;
|
||||
if (cachedTargetScores[nextTarget] == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
nextTarget = -1;
|
||||
cachedTargetScores = null;
|
||||
}
|
||||
if (modeIterator != null) {
|
||||
controller.doneEvaluating(bestScoreForMode);
|
||||
bestScoreForMode = new Score(Integer.MIN_VALUE);
|
||||
if (modeIterator.hasNext()) {
|
||||
selectedModes = modeIterator.next();
|
||||
advancedToNextMode = true;
|
||||
return true;
|
||||
}
|
||||
modeIterator = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller);
|
||||
Score lastScore = null;
|
||||
do {
|
||||
GameSimulator simulator = new GameSimulator(controller, game, player);
|
||||
simulator.setInterceptor(interceptor);
|
||||
simulator.setInterceptor(choicesIterator);
|
||||
lastScore = simulator.simulateSpellAbility(sa);
|
||||
if (lastScore.value > bestScore.value) {
|
||||
bestScore = lastScore;
|
||||
}
|
||||
} while (interceptor.advance(lastScore));
|
||||
} while (choicesIterator.advance(lastScore));
|
||||
controller.doneEvaluating(bestScore);
|
||||
return bestScore;
|
||||
}
|
||||
@@ -533,7 +323,7 @@ public class SpellAbilityPicker {
|
||||
Plan.Decision decision = plan.getSelectedDecision();
|
||||
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
|
||||
// TODO: Validate that there's no discrepancies between choices and modes?
|
||||
List<AbilitySub> plannedModes = getModeCombination(choices, decision.modes);
|
||||
List<AbilitySub> plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes);
|
||||
if (plan.getSelectedDecision().targets != null) {
|
||||
PossibleTargetSelector selector = new PossibleTargetSelector(sa, plannedModes);
|
||||
if (!selector.selectTargets(decision.targets)) {
|
||||
@@ -582,13 +372,4 @@ public class SpellAbilityPicker {
|
||||
@Override
|
||||
public String toUnsuppressedString() { return "Play land " + (getHostCard() != null ? getHostCard().getName() : ""); }
|
||||
}
|
||||
|
||||
public interface Interceptor {
|
||||
public List<AbilitySub> chooseModesForAbility(List<AbilitySub> choices, int min, int num, boolean allowRepeat);
|
||||
public Card chooseCard(CardCollection fetchList);
|
||||
public void chooseTargets(SpellAbility sa, GameSimulator simulator);
|
||||
public Card getSelectedChoice();
|
||||
public int[] getSelectModes();
|
||||
public boolean advance(Score lastScore);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user