diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index b2d6069bba0..ad08028f2ab 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -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 actions = new LinkedList<>(); protected List targets = new ArrayList<>(); protected List choices = new ArrayList<>(); @@ -78,7 +78,7 @@ public class ComputerPlayer6 extends ComputerPlayer { protected Set actionCache; private static final List 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 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 (%s)", + // show final calculated score and best actions chain from it + List 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 (Lightning Bolt [aa5]: Cast Lightning Bolt -> Balduvian Bears [c49]) + // total + logger.info(String.format("Sim Prio [%d] #%d ", 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 ", + + // 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] (%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] (%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] (%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: "); 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 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 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; diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java index 018eab010ff..f78cceee8f1 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java @@ -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()); } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java index e756af96fbc..a1bd3db6dc0 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java @@ -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; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 071455a5a82..858d2c2e0d2 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -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 allActions; + private transient ConcurrentLinkedQueue 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 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 iterator = options.iterator(); boolean bad = true; diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulationNode2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulationNode2.java index d4298f3486e..3d11365f85d 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulationNode2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulationNode2.java @@ -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 abilities; protected int depth; protected List children = new ArrayList<>(); protected SimulationNode2 parent; - protected List targets = new ArrayList<>(); - protected List choices = new ArrayList<>(); + protected List targets = new ArrayList<>(); // TODO: looks like it un-used by bugs (research and implement possible targets simulation for choices?) + protected List choices = new ArrayList<>(); // TODO: un-used at all, maybe same history as targets above protected UUID playerId; protected Combat combat; diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index c49289269c3..ba5a89bec46 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -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(hand.getCards(game))); - } - } - protected void logList(String message, List list) { StringBuilder sb = new StringBuilder(); sb.append(message).append(": "); diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 43a841b790a..53a9a29392d 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -295,6 +295,9 @@ public class GameState implements Serializable, Copyable { playerList.add(player.getId()); } + /** + * AI related: monitor changes in game state (if it changed then AI must re-calculate current actions chain) + */ public String getValue(boolean useHidden) { StringBuilder sb = threadLocalBuilder.get(); @@ -333,6 +336,9 @@ public class GameState implements Serializable, Copyable { return sb.toString(); } + /** + * AI related: monitor changes in game state (if it changed then AI must re-calculate current actions chain) + */ public String getValue(boolean useHidden, Game game) { StringBuilder sb = threadLocalBuilder.get(); @@ -386,6 +392,9 @@ public class GameState implements Serializable, Copyable { return sb.toString(); } + /** + * AI related: monitor changes in game state (if it changed then AI must re-calculate current actions chain) + */ public String getValue(Game game, UUID playerId) { StringBuilder sb = threadLocalBuilder.get();