mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
dev, AI: improved AI logs:
* added object and targets info for possible actions; * added detailed score for each action in possible action chains; * added stack info; * improved output for easy read and analyse;
This commit is contained in:
parent
e3de777bd5
commit
add2d0473e
7 changed files with 281 additions and 163 deletions
|
|
@ -9,7 +9,6 @@ import mage.abilities.common.PassAbility;
|
|||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.SearchEffect;
|
||||
import mage.abilities.keyword.*;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.Cards;
|
||||
import mage.choices.Choice;
|
||||
import mage.constants.Outcome;
|
||||
|
|
@ -30,7 +29,7 @@ import mage.players.Player;
|
|||
import mage.target.Target;
|
||||
import mage.target.TargetAmount;
|
||||
import mage.target.TargetCard;
|
||||
import mage.target.Targets;
|
||||
import mage.util.CardUtil;
|
||||
import mage.util.RandomUtil;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
|
|
@ -47,9 +46,10 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(ComputerPlayer6.class);
|
||||
|
||||
// TODO: add and research maxNodes logs, is it good to increase to 50000 for better results?
|
||||
// TODO: increase maxNodes due AI skill level?
|
||||
// TODO: add and research maxNodes logs, is it good to increase from 5000 to 50000 for better results?
|
||||
// TODO: increase maxNodes due AI skill level like max depth?
|
||||
private static final int MAX_SIMULATED_NODES_PER_CALC = 5000;
|
||||
private static final int MAX_SIMULATED_NODES_PER_ERROR = 5100; // TODO: debug only, set low value to find big calculations
|
||||
|
||||
// same params as Executors.newFixedThreadPool
|
||||
// no needs errors check in afterExecute here cause that pool used for FutureTask with result check already
|
||||
|
|
@ -66,7 +66,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
});
|
||||
protected int maxDepth;
|
||||
protected int maxNodes;
|
||||
protected int maxThink;
|
||||
protected int maxThinkTimeSecs;
|
||||
protected LinkedList<Ability> actions = new LinkedList<>();
|
||||
protected List<UUID> targets = new ArrayList<>();
|
||||
protected List<String> choices = new ArrayList<>();
|
||||
|
|
@ -78,7 +78,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
|
||||
protected Set<String> actionCache;
|
||||
private static final List<TreeOptimizer> optimizers = new ArrayList<>();
|
||||
protected int lastLoggedTurn = 0;
|
||||
protected int lastLoggedTurn = 0; // for debug logs: mark start of the turn
|
||||
protected static final String BLANKS = "...............................................";
|
||||
|
||||
static {
|
||||
|
|
@ -92,11 +92,11 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
public ComputerPlayer6(String name, RangeOfInfluence range, int skill) {
|
||||
super(name, range);
|
||||
if (skill < 4) {
|
||||
maxDepth = 4; // wtf
|
||||
maxDepth = 4; // TODO: can be increased to support better calculations? (example = 8, skill * 2)
|
||||
} else {
|
||||
maxDepth = skill;
|
||||
}
|
||||
maxThink = skill * 3;
|
||||
maxThinkTimeSecs = skill * 3;
|
||||
maxNodes = MAX_SIMULATED_NODES_PER_CALC;
|
||||
this.actionCache = new HashSet<>();
|
||||
}
|
||||
|
|
@ -119,21 +119,20 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
return new ComputerPlayer6(this);
|
||||
}
|
||||
|
||||
protected void printOutState(Game game) {
|
||||
protected void printBattlefieldScore(Game game, String info) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
printOutState(game, playerId);
|
||||
logger.info("");
|
||||
logger.info("=================== " + info + ", turn " + game.getTurnNum() + ", " + game.getPlayer(game.getPriorityPlayerId()).getName() + " ===================");
|
||||
logger.info("[Stack]: " + game.getStack());
|
||||
printBattlefieldScore(game, playerId);
|
||||
for (UUID opponentId : game.getOpponents(playerId)) {
|
||||
printOutState(game, opponentId);
|
||||
printBattlefieldScore(game, opponentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void printOutState(Game game, UUID playerId) {
|
||||
if (lastLoggedTurn != game.getTurnNum()) {
|
||||
lastLoggedTurn = game.getTurnNum();
|
||||
logger.info(new StringBuilder("------------------------ ").append("Turn: ").append(game.getTurnNum()).append("] --------------------------------------------------------------").toString());
|
||||
}
|
||||
|
||||
protected void printBattlefieldScore(Game game, UUID playerId) {
|
||||
// hand
|
||||
Player player = game.getPlayer(playerId);
|
||||
GameStateEvaluator2.PlayerEvaluateScore score = GameStateEvaluator2.evaluate(playerId, game);
|
||||
logger.info(new StringBuilder("[").append(game.getPlayer(playerId).getName()).append("]")
|
||||
|
|
@ -141,27 +140,26 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
.append(", score = ").append(score.getTotalScore())
|
||||
.append(" (").append(score.getPlayerInfoFull()).append(")")
|
||||
.toString());
|
||||
StringBuilder sb = new StringBuilder("-> Hand: [");
|
||||
for (Card card : player.getHand().getCards(game)) {
|
||||
sb.append(card.getName()).append(';');
|
||||
}
|
||||
logger.info(sb.append(']').toString());
|
||||
String cardsInfo = player.getHand().getCards(game).stream()
|
||||
.map(card -> card.getName() + ":" + GameStateEvaluator2.HAND_CARD_SCORE) // TODO: add card score here after implement
|
||||
.collect(Collectors.joining("; "));
|
||||
StringBuilder sb = new StringBuilder("-> Hand: [")
|
||||
.append(cardsInfo)
|
||||
.append("]");
|
||||
logger.info(sb.toString());
|
||||
|
||||
// battlefield
|
||||
sb.setLength(0);
|
||||
sb.append("-> Permanents: [");
|
||||
for (Permanent permanent : game.getBattlefield().getAllPermanents()) {
|
||||
if (permanent.isOwnedBy(player.getId())) {
|
||||
sb.append(permanent.getName());
|
||||
if (permanent.isTapped()) {
|
||||
sb.append("(tapped)");
|
||||
}
|
||||
if (permanent.isAttacking()) {
|
||||
sb.append("(attacking)");
|
||||
}
|
||||
sb.append(':' + String.valueOf(GameStateEvaluator2.evaluatePermanent(permanent, game)));
|
||||
sb.append(';');
|
||||
}
|
||||
}
|
||||
logger.info(sb.append(']').toString());
|
||||
String ownPermanentsInfo = game.getBattlefield().getAllPermanents().stream()
|
||||
.filter(p -> p.isOwnedBy(player.getId()))
|
||||
.map(p -> p.getName()
|
||||
+ (p.isTapped() ? ",tapped" : "")
|
||||
+ (p.isAttacking() ? ",attacking" : "")
|
||||
+ (p.getBlocking() > 0 ? ",blocking" : "")
|
||||
+ ":" + GameStateEvaluator2.evaluatePermanent(p, game))
|
||||
.collect(Collectors.joining("; "));
|
||||
sb.append("-> Permanents: [").append(ownPermanentsInfo).append("]");
|
||||
logger.info(sb.toString());
|
||||
}
|
||||
|
||||
protected void act(Game game) {
|
||||
|
|
@ -175,8 +173,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
// example: ===> SELECTED ACTION for PlayerA: Play Swamp
|
||||
logger.info(String.format("===> SELECTED ACTION for %s: %s",
|
||||
getName(),
|
||||
ability.toString()
|
||||
+ listTargets(game, ability.getTargets(), " (targeting %s)", "")
|
||||
getAbilityAndSourceInfo(game, ability, true)
|
||||
));
|
||||
if (!ability.getTargets().isEmpty()) {
|
||||
for (Target target : ability.getTargets()) {
|
||||
|
|
@ -187,10 +184,6 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
}
|
||||
}
|
||||
}
|
||||
Player player = game.getPlayer(ability.getFirstTarget());
|
||||
if (player != null) {
|
||||
logger.info("targets = " + player.getName());
|
||||
}
|
||||
}
|
||||
this.activateAbility((ActivatedAbility) ability, game);
|
||||
if (ability.isUsesStack()) {
|
||||
|
|
@ -220,6 +213,9 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
return GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
|
||||
}
|
||||
// Condition to stop deeper simulation
|
||||
if (SimulationNode2.nodeCount > MAX_SIMULATED_NODES_PER_ERROR) {
|
||||
throw new IllegalStateException("AI ERROR: too much nodes (possible actions)");
|
||||
}
|
||||
if (depth <= 0
|
||||
|| SimulationNode2.nodeCount > maxNodes
|
||||
|| game.checkIfGameIsOver()) {
|
||||
|
|
@ -309,11 +305,21 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
if (root.playerId.equals(playerId)
|
||||
&& root.abilities != null
|
||||
&& game.getState().getValue(true).hashCode() == test.gameValue) {
|
||||
logger.info("simulating -- continuing previous action chain");
|
||||
logger.info("simulating -- continuing previous actions chain");
|
||||
actions = new LinkedList<>(root.abilities);
|
||||
combat = root.combat;
|
||||
return true;
|
||||
} else {
|
||||
if (root.abilities == null || root.abilities.isEmpty()) {
|
||||
logger.info("simulating -- need re-calculation (no more actions)");
|
||||
} else if (game.getState().getValue(true).hashCode() != test.gameValue) {
|
||||
logger.info("simulating -- need re-calculation (game state changed between actions)");
|
||||
} else if (!root.playerId.equals(playerId)) {
|
||||
// TODO: need research, why need playerId and why it taken from stack objects as controller
|
||||
logger.info("simulating -- need re-calculation (active controller changed)");
|
||||
} else {
|
||||
logger.info("simulating -- need re-calculation (unknown reason)");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -329,6 +335,9 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
if (alpha >= beta) {
|
||||
break;
|
||||
}
|
||||
if (SimulationNode2.nodeCount > MAX_SIMULATED_NODES_PER_ERROR) {
|
||||
throw new IllegalStateException("AI ERROR: too much nodes (possible actions)");
|
||||
}
|
||||
if (SimulationNode2.nodeCount > maxNodes) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -426,7 +435,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
FutureTask<Integer> task = new FutureTask<>(() -> addActions(root, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE));
|
||||
threadPoolSimulations.execute(task);
|
||||
try {
|
||||
int maxSeconds = maxThink;
|
||||
int maxSeconds = maxThinkTimeSecs;
|
||||
if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS) {
|
||||
maxSeconds = 3600;
|
||||
}
|
||||
|
|
@ -473,14 +482,16 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
if (logger.isInfoEnabled()
|
||||
&& !allActions.isEmpty()
|
||||
&& depth == maxDepth) {
|
||||
logger.info(String.format("POSSIBLE ACTIONS for %s (%d, started score: %d)%s",
|
||||
logger.info(String.format("POSSIBLE ACTION CHAINS for %s (%d, started score: %d)%s",
|
||||
getName(),
|
||||
allActions.size(),
|
||||
startedScore,
|
||||
(actions.isEmpty() ? "" : ":")
|
||||
));
|
||||
for (int i = 0; i < allActions.size(); i++) {
|
||||
logger.info(String.format("-> #%d (%s)", i + 1, allActions.get(i)));
|
||||
// print possible actions with detailed targets
|
||||
Ability possibleAbility = allActions.get(i);
|
||||
logger.info(String.format("-> #%d (%s)", i + 1, getAbilityAndSourceInfo(game, possibleAbility, true)));
|
||||
}
|
||||
}
|
||||
int actionNumber = 0;
|
||||
|
|
@ -512,15 +523,15 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
}
|
||||
SimulationNode2 newNode = new SimulationNode2(node, sim, action, depth, currentPlayer.getId());
|
||||
sim.checkStateAndTriggered();
|
||||
int actionScore;
|
||||
int finalScore;
|
||||
if (action instanceof PassAbility && sim.getStack().isEmpty()) {
|
||||
// no more next actions, it's a final score
|
||||
actionScore = GameStateEvaluator2.evaluate(this.getId(), sim).getTotalScore();
|
||||
finalScore = GameStateEvaluator2.evaluate(this.getId(), sim).getTotalScore();
|
||||
} else {
|
||||
// resolve current action and calc all next actions to find best score (return max possible score)
|
||||
actionScore = addActions(newNode, depth - 1, alpha, beta);
|
||||
finalScore = addActions(newNode, depth - 1, alpha, beta);
|
||||
}
|
||||
logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + actionNumber + " <" + actionScore + "> - (" + action + ") ");
|
||||
logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + actionNumber + " <" + finalScore + "> - (" + action + ") ");
|
||||
|
||||
// Hints on data:
|
||||
// * node - started game with executed command (pay and put on stack)
|
||||
|
|
@ -529,67 +540,114 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
// * node.score - rewrites to store max score (e.g. contains only final data)
|
||||
if (logger.isInfoEnabled()
|
||||
&& depth >= maxDepth) {
|
||||
// show calculated actions and score
|
||||
// example: Sim Prio [6] #1 <605> (Play Swamp)
|
||||
int currentActionScore = GameStateEvaluator2.evaluate(this.getId(), newNode.getGame()).getTotalScore();
|
||||
int diffCurrentAction = currentActionScore - startedScore;
|
||||
int diffNextActions = actionScore - startedScore - diffCurrentAction;
|
||||
logger.info(String.format("Sim Prio [%d] #%d <diff %s, %s> (%s)",
|
||||
// show final calculated score and best actions chain from it
|
||||
List<SimulationNode2> fullChain = new ArrayList<>();
|
||||
fullChain.add(newNode);
|
||||
SimulationNode2 finalNode = newNode;
|
||||
while (!finalNode.getChildren().isEmpty()) {
|
||||
finalNode = finalNode.getChildren().get(0);
|
||||
fullChain.add(finalNode);
|
||||
}
|
||||
|
||||
// example: Sim Prio [6] #1 <diff -19, +4444> (Lightning Bolt [aa5]: Cast Lightning Bolt -> Balduvian Bears [c49])
|
||||
// total
|
||||
logger.info(String.format("Sim Prio [%d] #%d <total score diff %s (from %s to %s)>",
|
||||
depth,
|
||||
actionNumber,
|
||||
printDiffScore(diffCurrentAction),
|
||||
printDiffScore(diffNextActions),
|
||||
action
|
||||
+ (action.isModal() ? " Mode = " + action.getModes().getMode().toString() : "")
|
||||
+ listTargets(game, action.getTargets(), " (targeting %s)", "")
|
||||
+ (logger.isTraceEnabled() ? " #" + newNode.hashCode() : "")
|
||||
printDiffScore(finalScore - startedScore),
|
||||
printDiffScore(startedScore),
|
||||
printDiffScore(finalScore)
|
||||
));
|
||||
// collect childs info (next actions chain)
|
||||
SimulationNode2 logNode = newNode;
|
||||
while (logNode.getChildren() != null
|
||||
&& !logNode.getChildren().isEmpty()) {
|
||||
logNode = logNode.getChildren().get(0);
|
||||
if (logNode.getAbilities() != null
|
||||
&& !logNode.getAbilities().isEmpty()) {
|
||||
int logCurrentScore = GameStateEvaluator2.evaluate(this.getId(), logNode.getGame()).getTotalScore();
|
||||
int logPrevScore = GameStateEvaluator2.evaluate(this.getId(), logNode.getParent().getGame()).getTotalScore();
|
||||
logger.info(String.format("Sim Prio [%d] -> next action: [%d]%s <diff %s, %s>",
|
||||
|
||||
// details
|
||||
for (int chainIndex = 0; chainIndex < fullChain.size(); chainIndex++) {
|
||||
SimulationNode2 currentNode = fullChain.get(chainIndex);
|
||||
SimulationNode2 prevNode;
|
||||
if (chainIndex == 0) {
|
||||
prevNode = node;
|
||||
} else {
|
||||
prevNode = fullChain.get(chainIndex - 1);
|
||||
}
|
||||
|
||||
int currentScore = GameStateEvaluator2.evaluate(this.getId(), currentNode.getGame()).getTotalScore();
|
||||
int prevScore = GameStateEvaluator2.evaluate(this.getId(), prevNode.getGame()).getTotalScore();
|
||||
|
||||
if (currentNode.getAbilities() != null) {
|
||||
// ON PRIORITY
|
||||
|
||||
// runtime check
|
||||
if (currentNode.getAbilities().size() != 1) {
|
||||
throw new IllegalStateException("AI's simulated game must contains only one selected action, but found: " + currentNode.getAbilities());
|
||||
}
|
||||
if (!currentNode.getTargets().isEmpty() || !currentNode.getChoices().isEmpty()) {
|
||||
throw new IllegalStateException("WTF, simulated abilities with targets/choices");
|
||||
}
|
||||
logger.info(String.format("Sim Prio [%d] -> next action: [%d]<diff %s> (%s)",
|
||||
depth,
|
||||
logNode.getDepth(),
|
||||
logNode.getAbilities().toString(),
|
||||
printDiffScore(logCurrentScore - logPrevScore),
|
||||
printDiffScore(actionScore - logCurrentScore)
|
||||
currentNode.getDepth(),
|
||||
printDiffScore(currentScore - prevScore),
|
||||
getAbilityAndSourceInfo(currentNode.getGame(), currentNode.getAbilities().get(0), true)
|
||||
));
|
||||
} else if (!currentNode.getTargets().isEmpty()) {
|
||||
// ON TARGETS
|
||||
String targetsInfo = currentNode.getTargets()
|
||||
.stream()
|
||||
.map(id -> {
|
||||
Player player = game.getPlayer(id);
|
||||
if (player != null) {
|
||||
return player.getName();
|
||||
}
|
||||
MageObject object = game.getObject(id);
|
||||
if (object != null) {
|
||||
return object.getIdName();
|
||||
}
|
||||
return "unknown";
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
logger.info(String.format("Sim Prio [%d] -> with choices (TODO): [%d]<diff %s> (%s)",
|
||||
depth,
|
||||
currentNode.getDepth(),
|
||||
printDiffScore(currentScore - prevScore),
|
||||
targetsInfo)
|
||||
);
|
||||
} else if (!currentNode.getChoices().isEmpty()) {
|
||||
// ON CHOICES
|
||||
String choicesInfo = String.join(", ", currentNode.getChoices());
|
||||
logger.info(String.format("Sim Prio [%d] -> with choices (TODO): [%d]<diff %s> (%s)",
|
||||
depth,
|
||||
currentNode.getDepth(),
|
||||
printDiffScore(currentScore - prevScore),
|
||||
choicesInfo)
|
||||
);
|
||||
} else {
|
||||
throw new IllegalStateException("AI CALC ERROR: unknown calculation result (no abilities, no targets, no choices)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPlayer.getId().equals(playerId)) {
|
||||
if (actionScore > bestValSubNodes) {
|
||||
bestValSubNodes = actionScore;
|
||||
if (finalScore > bestValSubNodes) {
|
||||
bestValSubNodes = finalScore;
|
||||
}
|
||||
if (depth == maxDepth
|
||||
&& action instanceof PassAbility) {
|
||||
actionScore = actionScore - PASSIVITY_PENALTY; // passivity penalty
|
||||
finalScore = finalScore - PASSIVITY_PENALTY; // passivity penalty
|
||||
}
|
||||
if (actionScore > alpha
|
||||
if (finalScore > alpha
|
||||
|| (depth == maxDepth
|
||||
&& actionScore == alpha
|
||||
&& finalScore == alpha
|
||||
&& RandomUtil.nextBoolean())) { // Adding random for equal value to get change sometimes
|
||||
alpha = actionScore;
|
||||
alpha = finalScore;
|
||||
bestNode = newNode;
|
||||
bestNode.setScore(actionScore);
|
||||
bestNode.setScore(finalScore);
|
||||
if (!newNode.getChildren().isEmpty()) {
|
||||
// TODO: wtf, must review all code to remove shared objects
|
||||
bestNode.setCombat(newNode.getChildren().get(0).getCombat());
|
||||
}
|
||||
|
||||
// keep only best node
|
||||
if (depth == maxDepth) {
|
||||
GameStateEvaluator2.PlayerEvaluateScore score = GameStateEvaluator2.evaluate(this.getId(), bestNode.game);
|
||||
String scoreInfo = " [" + score.getPlayerInfoShort() + "-" + score.getOpponentInfoShort() + "]";
|
||||
String abilitiesInfo = bestNode.getAbilities()
|
||||
.stream()
|
||||
.map(a -> a.toString() + listTargets(game, a.getTargets(), " (targeting %s)", ""))
|
||||
.collect(Collectors.joining("; "));
|
||||
logger.info("Sim Prio [" + depth + "] >> BEST action chain found <" + bestNode.getScore() + scoreInfo + "> " + abilitiesInfo);
|
||||
logger.info("Sim Prio [" + depth + "] -* BEST actions chain so far: <final score " + bestNode.getScore() + ">");
|
||||
node.children.clear();
|
||||
node.children.add(bestNode);
|
||||
node.setScore(bestNode.getScore());
|
||||
|
|
@ -597,22 +655,22 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
}
|
||||
|
||||
// no need to check other actions
|
||||
if (actionScore == GameStateEvaluator2.WIN_GAME_SCORE) {
|
||||
if (finalScore == GameStateEvaluator2.WIN_GAME_SCORE) {
|
||||
logger.debug("Sim Prio -- win - break");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (actionScore < beta) {
|
||||
beta = actionScore;
|
||||
if (finalScore < beta) {
|
||||
beta = finalScore;
|
||||
bestNode = newNode;
|
||||
bestNode.setScore(actionScore);
|
||||
bestNode.setScore(finalScore);
|
||||
if (!newNode.getChildren().isEmpty()) {
|
||||
bestNode.setCombat(newNode.getChildren().get(0).getCombat());
|
||||
}
|
||||
}
|
||||
|
||||
// no need to check other actions
|
||||
if (actionScore == GameStateEvaluator2.LOSE_GAME_SCORE) {
|
||||
if (finalScore == GameStateEvaluator2.LOSE_GAME_SCORE) {
|
||||
logger.debug("Sim Prio -- lose - break");
|
||||
break;
|
||||
}
|
||||
|
|
@ -620,6 +678,9 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
if (alpha >= beta) {
|
||||
break;
|
||||
}
|
||||
if (SimulationNode2.nodeCount > MAX_SIMULATED_NODES_PER_ERROR) {
|
||||
throw new IllegalStateException("AI ERROR: too many nodes (possible actions)");
|
||||
}
|
||||
if (SimulationNode2.nodeCount > maxNodes) {
|
||||
logger.debug("Sim Prio -- reached end-state");
|
||||
break;
|
||||
|
|
@ -628,7 +689,8 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
} // end of for (allActions)
|
||||
|
||||
if (depth == maxDepth) {
|
||||
logger.info("Sim Prio [" + depth + "] -- End for Max Depth -- Nodes calculated: " + SimulationNode2.nodeCount);
|
||||
// TODO: buggy? Why it ended with depth limit 6 on one Pass action?!
|
||||
logger.info("Sim Prio [" + depth + "] ## Ended due max actions chain depth limit (" + maxDepth + ") -- Nodes calculated: " + SimulationNode2.nodeCount);
|
||||
}
|
||||
if (bestNode != null) {
|
||||
node.children.clear();
|
||||
|
|
@ -647,6 +709,49 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
protected String getAbilityAndSourceInfo(Game game, Ability ability, boolean showTargets) {
|
||||
// ability
|
||||
// TODO: add modal info
|
||||
// + (action.isModal() ? " Mode = " + action.getModes().getMode().toString() : "")
|
||||
if (ability.isModal()) {
|
||||
throw new IllegalStateException("TODO: need implement");
|
||||
}
|
||||
MageObject sourceObject = ability.getSourceObject(game);
|
||||
String abilityInfo = (sourceObject == null ? "" : sourceObject.getIdName() + ": ") + CardUtil.substring(ability.toString(), 30, "...");
|
||||
// targets
|
||||
String targetsInfo = "";
|
||||
if (showTargets) {
|
||||
List<String> allTargetsInfo = new ArrayList<>();
|
||||
ability.getAllSelectedTargets().forEach(target -> {
|
||||
target.getTargets().forEach(selectedId -> {
|
||||
String xInfo = "";
|
||||
if (target instanceof TargetAmount) {
|
||||
xInfo = "x" + target.getTargetAmount(selectedId) + " ";
|
||||
}
|
||||
|
||||
String targetInfo;
|
||||
|
||||
Player player = game.getPlayer(selectedId);
|
||||
MageObject object = game.getObject(selectedId);
|
||||
mage.game.stack.Spell spell = game.getSpellOrLKIStack(selectedId);
|
||||
|
||||
if (player != null) {
|
||||
targetInfo = player.getName();
|
||||
} else if (object != null) {
|
||||
targetInfo = object.getIdName();
|
||||
} else if (spell != null) {
|
||||
targetInfo = "spell - " + CardUtil.substring(spell.toString(), 20, "...");
|
||||
} else {
|
||||
targetInfo = "unknown";
|
||||
}
|
||||
allTargetsInfo.add(xInfo + targetInfo);
|
||||
});
|
||||
});
|
||||
targetsInfo = String.join(" + ", allTargetsInfo);
|
||||
}
|
||||
return abilityInfo + (targetsInfo.isEmpty() ? "" : " -> " + targetsInfo);
|
||||
}
|
||||
|
||||
private String printDiffScore(int score) {
|
||||
if (score >= 0) {
|
||||
return "+" + score;
|
||||
|
|
@ -1084,38 +1189,6 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return info about targets list (targeting objects)
|
||||
*
|
||||
* @param game
|
||||
* @param targets
|
||||
* @param format example: my %s in data
|
||||
* @param emptyText default text for empty targets list
|
||||
* @return
|
||||
*/
|
||||
protected String listTargets(Game game, Targets targets, String format, String emptyText) {
|
||||
List<String> res = new ArrayList<>();
|
||||
for (Target target : targets) {
|
||||
for (UUID id : target.getTargets()) {
|
||||
MageObject object = game.getObject(id);
|
||||
if (object != null) {
|
||||
String prefix = "";
|
||||
if (target instanceof TargetAmount) {
|
||||
prefix = " " + target.getTargetAmount(id) + "x ";
|
||||
}
|
||||
res.add(prefix + object.getIdName());
|
||||
}
|
||||
}
|
||||
}
|
||||
String info = String.join("; ", res);
|
||||
|
||||
if (info.isEmpty()) {
|
||||
return emptyText;
|
||||
} else {
|
||||
return String.format(format, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanUpOnMatchEnd() {
|
||||
root = null;
|
||||
|
|
|
|||
|
|
@ -42,12 +42,6 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
|
|||
}
|
||||
|
||||
private boolean priorityPlay(Game game) {
|
||||
if (lastLoggedTurn != game.getTurnNum()) {
|
||||
lastLoggedTurn = game.getTurnNum();
|
||||
logger.info("======================= Turn: " + game.getState().toString() + " [" + game.getPlayer(game.getActivePlayerId()).getName() + "] =========================================");
|
||||
}
|
||||
logState(game);
|
||||
logger.debug("Priority -- Step: " + (game.getTurnStepType() + " ").substring(0, 25) + " ActivePlayer-" + game.getPlayer(game.getActivePlayerId()).getName() + " PriorityPlayer-" + name);
|
||||
game.getState().setPriorityPlayerId(playerId);
|
||||
game.firePriorityEvent(playerId);
|
||||
switch (game.getTurnStepType()) {
|
||||
|
|
@ -59,10 +53,12 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
|
|||
// 09.03.2020:
|
||||
// in old version it passes opponent's pre-combat step (game.isActivePlayer(playerId) -> pass(game))
|
||||
// why?!
|
||||
printOutState(game);
|
||||
printBattlefieldScore(game, "Sim PRIORITY on MAIN 1");
|
||||
if (actions.isEmpty()) {
|
||||
logger.info("Sim Calculate pre combat main actions ----------------------------------------------------- ");
|
||||
calculateActions(game);
|
||||
} else {
|
||||
// TODO: is it possible non empty actions without calculation?!
|
||||
throw new IllegalStateException("wtf");
|
||||
}
|
||||
act(game);
|
||||
return true;
|
||||
|
|
@ -70,17 +66,22 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
|
|||
pass(game);
|
||||
return false;
|
||||
case DECLARE_ATTACKERS:
|
||||
printOutState(game);
|
||||
printBattlefieldScore(game, "Sim PRIORITY on DECLARE ATTACKERS");
|
||||
if (actions.isEmpty()) {
|
||||
logger.info("Sim Calculate declare attackers actions ----------------------------------------------------- ");
|
||||
calculateActions(game);
|
||||
} else {
|
||||
// TODO: is it possible non empty actions without calculation?!
|
||||
throw new IllegalStateException("wtf");
|
||||
}
|
||||
act(game);
|
||||
return true;
|
||||
case DECLARE_BLOCKERS:
|
||||
printOutState(game);
|
||||
printBattlefieldScore(game, "Sim PRIORITY on DECLARE BLOCKERS");
|
||||
if (actions.isEmpty()) {
|
||||
calculateActions(game);
|
||||
} else {
|
||||
// TODO: is it possible non empty actions without calculation?!
|
||||
throw new IllegalStateException("wtf");
|
||||
}
|
||||
act(game);
|
||||
return true;
|
||||
|
|
@ -90,9 +91,12 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
|
|||
pass(game);
|
||||
return false;
|
||||
case POSTCOMBAT_MAIN:
|
||||
printOutState(game);
|
||||
printBattlefieldScore(game, "Sim PRIORITY on MAIN 2");
|
||||
if (actions.isEmpty()) {
|
||||
calculateActions(game);
|
||||
} else {
|
||||
// TODO: is it possible non empty actions without calculation?!
|
||||
throw new IllegalStateException("wtf");
|
||||
}
|
||||
act(game);
|
||||
return true;
|
||||
|
|
@ -107,6 +111,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
|
|||
|
||||
protected void calculateActions(Game game) {
|
||||
if (!getNextAction(game)) {
|
||||
//logger.info("--- calculating possible actions for " + this.getName() + " on " + game.toString());
|
||||
Date startTime = new Date();
|
||||
currentScore = GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
|
||||
Game sim = createSimulation(game);
|
||||
|
|
@ -116,6 +121,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
|
|||
if (root != null && root.children != null && !root.children.isEmpty()) {
|
||||
logger.trace("After add actions timed: root.children.size = " + root.children.size());
|
||||
root = root.children.get(0);
|
||||
|
||||
// prevent repeating always the same action with no cost
|
||||
boolean doThis = true;
|
||||
if (root.abilities.size() == 1) {
|
||||
|
|
@ -128,9 +134,10 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doThis) {
|
||||
actions = new LinkedList<>(root.abilities);
|
||||
combat = root.combat;
|
||||
combat = root.combat; // TODO: must use copy?!
|
||||
for (Ability ability : actions) {
|
||||
actionCache.add(ability.getRule() + '_' + ability.getSourceId());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,21 +23,24 @@ public final class GameStateEvaluator2 {
|
|||
public static final int WIN_GAME_SCORE = 100000000;
|
||||
public static final int LOSE_GAME_SCORE = -WIN_GAME_SCORE;
|
||||
|
||||
public static final int HAND_CARD_SCORE = 5;
|
||||
|
||||
public static PlayerEvaluateScore evaluate(UUID playerId, Game game) {
|
||||
// TODO: add multi opponents support, so AI can take better actions
|
||||
Player player = game.getPlayer(playerId);
|
||||
Player opponent = game.getPlayer(game.getOpponents(playerId).stream().findFirst().orElse(null)); // TODO: add multi opponent support?
|
||||
Player opponent = game.getPlayer(game.getOpponents(playerId).stream().findFirst().orElse(null));
|
||||
if (opponent == null) {
|
||||
return new PlayerEvaluateScore(WIN_GAME_SCORE);
|
||||
return new PlayerEvaluateScore(playerId, WIN_GAME_SCORE);
|
||||
}
|
||||
|
||||
if (game.checkIfGameIsOver()) {
|
||||
if (player.hasLost()
|
||||
|| opponent.hasWon()) {
|
||||
return new PlayerEvaluateScore(LOSE_GAME_SCORE);
|
||||
return new PlayerEvaluateScore(playerId, LOSE_GAME_SCORE);
|
||||
}
|
||||
if (opponent.hasLost()
|
||||
|| player.hasWon()) {
|
||||
return new PlayerEvaluateScore(WIN_GAME_SCORE);
|
||||
return new PlayerEvaluateScore(playerId, WIN_GAME_SCORE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,8 +91,22 @@ public final class GameStateEvaluator2 {
|
|||
} catch (Throwable t) {
|
||||
}
|
||||
|
||||
int playerHandScore = player.getHand().size() * 5;
|
||||
int opponentHandScore = opponent.getHand().size() * 5;
|
||||
// TODO: add card evaluator like permanent evaluator
|
||||
// - same card on battlefield must score x2 compared to hand, so AI will want to play it;
|
||||
// - other zones must score cards same way, example: battlefield = x, hand = x * 0.1, graveyard = x * 0.5, exile = x * 0.3
|
||||
// - possible bug in wrong score: instant and sorcery on hand will be more valuable compared to other zones,
|
||||
// so AI will keep it in hand. Possible fix: look at card type and apply zones multipliers due special
|
||||
// table like:
|
||||
// * battlefield needs in creatures and enchantments/auras;
|
||||
// * hand needs in instants and sorceries
|
||||
// * graveyard needs in anything after battlefield and hand;
|
||||
// * exile needs in nothing;
|
||||
// * commander zone needs in nothing;
|
||||
// - additional improve: use revealed data to score opponent's hand:
|
||||
// * known card by card evaluator;
|
||||
// * unknown card by max value (so AI will use reveal to make opponent's total score lower -- is it helps???)
|
||||
int playerHandScore = player.getHand().size() * HAND_CARD_SCORE;
|
||||
int opponentHandScore = opponent.getHand().size() * HAND_CARD_SCORE;
|
||||
|
||||
int score = (playerLifeScore - opponentLifeScore)
|
||||
+ (playerPermanentsScore - opponentPermanentsScore)
|
||||
|
|
@ -99,6 +116,7 @@ public final class GameStateEvaluator2 {
|
|||
+ " permanents:" + (playerPermanentsScore - opponentPermanentsScore)
|
||||
+ " hand:" + (playerHandScore - opponentHandScore) + ')');
|
||||
return new PlayerEvaluateScore(
|
||||
playerId,
|
||||
playerLifeScore, playerHandScore, playerPermanentsScore,
|
||||
opponentLifeScore, opponentHandScore, opponentPermanentsScore);
|
||||
}
|
||||
|
|
@ -132,6 +150,7 @@ public final class GameStateEvaluator2 {
|
|||
|
||||
public static class PlayerEvaluateScore {
|
||||
|
||||
private UUID playerId;
|
||||
private int playerLifeScore = 0;
|
||||
private int playerHandScore = 0;
|
||||
private int playerPermanentsScore = 0;
|
||||
|
|
@ -140,14 +159,17 @@ public final class GameStateEvaluator2 {
|
|||
private int opponentHandScore = 0;
|
||||
private int opponentPermanentsScore = 0;
|
||||
|
||||
private int specialScore = 0; // special score (ignore all other)
|
||||
private int specialScore = 0; // special score (ignore all others, e.g. for win/lose game states)
|
||||
|
||||
public PlayerEvaluateScore(int specialScore) {
|
||||
public PlayerEvaluateScore(UUID playerId, int specialScore) {
|
||||
this.playerId = playerId;
|
||||
this.specialScore = specialScore;
|
||||
}
|
||||
|
||||
public PlayerEvaluateScore(int playerLifeScore, int playerHandScore, int playerPermanentsScore,
|
||||
int opponentLifeScore, int opponentHandScore, int opponentPermanentsScore) {
|
||||
public PlayerEvaluateScore(UUID playerId,
|
||||
int playerLifeScore, int playerHandScore, int playerPermanentsScore,
|
||||
int opponentLifeScore, int opponentHandScore, int opponentPermanentsScore) {
|
||||
this.playerId = playerId;
|
||||
this.playerLifeScore = playerLifeScore;
|
||||
this.playerHandScore = playerHandScore;
|
||||
this.playerPermanentsScore = playerPermanentsScore;
|
||||
|
|
@ -156,6 +178,10 @@ public final class GameStateEvaluator2 {
|
|||
this.opponentPermanentsScore = opponentPermanentsScore;
|
||||
}
|
||||
|
||||
public UUID getPlayerId() {
|
||||
return this.playerId;
|
||||
}
|
||||
|
||||
public int getPlayerScore() {
|
||||
return playerLifeScore + playerHandScore + playerPermanentsScore;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(SimulatedPlayer2.class);
|
||||
|
||||
private static final boolean AI_SIMULATE_ALL_BAD_AND_GOOD_TARGETS = false; // TODO: enable and do performance test (it's increase calculations by x2, but is it useful?)
|
||||
|
||||
private final boolean isSimulatedPlayer;
|
||||
private transient ConcurrentLinkedQueue<Ability> allActions;
|
||||
private transient ConcurrentLinkedQueue<Ability> allActions; // all possible abilities to play (copies with already selected targets)
|
||||
private final Player originalPlayer; // copy of the original player, source of choices/results in tests
|
||||
|
||||
public SimulatedPlayer2(Player originalPlayer, boolean isSimulatedPlayer) {
|
||||
|
|
@ -57,6 +59,9 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
|||
return new SimulatedPlayer2(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all playable abilities with all possible targets (targets already selected in ability)
|
||||
*/
|
||||
public List<Ability> simulatePriority(Game game) {
|
||||
allActions = new ConcurrentLinkedQueue<>();
|
||||
Game sim = game.createSimulationForAI();
|
||||
|
|
@ -164,6 +169,10 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
|||
return options;
|
||||
}
|
||||
|
||||
if (AI_SIMULATE_ALL_BAD_AND_GOOD_TARGETS) {
|
||||
return options;
|
||||
}
|
||||
|
||||
// determine if all effects are bad or good
|
||||
Iterator<Ability> iterator = options.iterator();
|
||||
boolean bad = true;
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ public class SimulationNode2 implements Serializable {
|
|||
protected static int nodeCount;
|
||||
|
||||
protected Game game;
|
||||
protected int gameValue;
|
||||
protected int gameValue; // game state hash to monitor changes
|
||||
protected int score;
|
||||
protected List<Ability> abilities;
|
||||
protected int depth;
|
||||
protected List<SimulationNode2> children = new ArrayList<>();
|
||||
protected SimulationNode2 parent;
|
||||
protected List<UUID> targets = new ArrayList<>();
|
||||
protected List<String> choices = new ArrayList<>();
|
||||
protected List<UUID> targets = new ArrayList<>(); // TODO: looks like it un-used by bugs (research and implement possible targets simulation for choices?)
|
||||
protected List<String> choices = new ArrayList<>(); // TODO: un-used at all, maybe same history as targets above
|
||||
protected UUID playerId;
|
||||
protected Combat combat;
|
||||
|
||||
|
|
|
|||
|
|
@ -1315,10 +1315,10 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
switch (game.getTurnStepType()) {
|
||||
case UPKEEP:
|
||||
// TODO: is it needs here? Need research (e.g. for better choose in upkeep triggers)?
|
||||
findPlayables(game);
|
||||
break;
|
||||
case DRAW:
|
||||
logState(game);
|
||||
break;
|
||||
case PRECOMBAT_MAIN:
|
||||
findPlayables(game);
|
||||
|
|
@ -2873,12 +2873,6 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
return threats;
|
||||
}
|
||||
|
||||
protected void logState(Game game) {
|
||||
if (log.isTraceEnabled()) {
|
||||
logList("Computer player " + name + " hand: ", new ArrayList<MageObject>(hand.getCards(game)));
|
||||
}
|
||||
}
|
||||
|
||||
protected void logList(String message, List<MageObject> list) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(message).append(": ");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue