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:
Oleg Agafonov 2024-05-28 15:52:48 +04:00
parent e3de777bd5
commit add2d0473e
7 changed files with 281 additions and 163 deletions

View file

@ -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;

View file

@ -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());
}

View file

@ -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,
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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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(": ");

View file

@ -295,6 +295,9 @@ public class GameState implements Serializable, Copyable<GameState> {
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<GameState> {
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<GameState> {
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();