forked from External/mage
AI: improved performance and fixed crashes on use cases with too much target options like "deals 5 damage divided as you choose" (related to #11285):
* added DebugUtil.AI_ENABLE_DEBUG_MODE for better IDE's debugging AI code; * it's a target amount optimizations; * it's use a grouping of possible targets due same static and dynamic stats (name, abilities, rules, damage, etc); * instead of going through all possible combinations, AI uses only meaningful targets from particular groups;
This commit is contained in:
parent
b4fa6ace66
commit
f17cbbe72b
9 changed files with 349 additions and 50 deletions
|
|
@ -346,7 +346,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
logger.debug("simulating -- node #:" + SimulationNode2.getCount() + " triggered ability option");
|
logger.debug("simulating -- node #:" + SimulationNode2.getCount() + " triggered ability option");
|
||||||
for (Target target : ability.getTargets()) {
|
for (Target target : ability.getTargets()) {
|
||||||
for (UUID targetId : target.getTargets()) {
|
for (UUID targetId : target.getTargets()) {
|
||||||
newNode.getTargets().add(targetId);
|
newNode.getTargets().add(targetId); // save for info only (real targets in newNode.ability already)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parent.children.add(newNode);
|
parent.children.add(newNode);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ public class ComputerPlayer extends PlayerImpl {
|
||||||
protected int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
|
protected int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
|
||||||
|
|
||||||
// debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout)
|
// debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout)
|
||||||
protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false;
|
protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = DebugUtil.AI_ENABLE_DEBUG_MODE;
|
||||||
|
|
||||||
// AI agents uses game simulation thread for all calcs and it's high CPU consumption
|
// AI agents uses game simulation thread for all calcs and it's high CPU consumption
|
||||||
// More AI threads - more parallel AI games can be calculate
|
// More AI threads - more parallel AI games can be calculate
|
||||||
|
|
@ -82,7 +82,7 @@ public class ComputerPlayer extends PlayerImpl {
|
||||||
// * use your's CPU cores for best performance
|
// * use your's CPU cores for best performance
|
||||||
// TODO: add server config to control max AI threads (with CPU cores by default)
|
// TODO: add server config to control max AI threads (with CPU cores by default)
|
||||||
// TODO: rework AI implementation to use multiple sims calculation instead one by one
|
// TODO: rework AI implementation to use multiple sims calculation instead one by one
|
||||||
final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 5;
|
final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5;
|
||||||
|
|
||||||
private final transient Map<Mana, Card> unplayable = new TreeMap<>();
|
private final transient Map<Mana, Card> unplayable = new TreeMap<>();
|
||||||
private final transient List<Card> playableNonInstant = new ArrayList<>();
|
private final transient List<Card> playableNonInstant = new ArrayList<>();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package org.mage.test.AI.basic;
|
package org.mage.test.AI.basic;
|
||||||
|
|
||||||
|
import mage.abilities.Ability;
|
||||||
|
import mage.abilities.common.SimpleStaticAbility;
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
|
import mage.game.permanent.Permanent;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -11,6 +14,13 @@ import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Possible problems:
|
||||||
|
* - big memory consumption on sims prepare (memory overflow)
|
||||||
|
* - too many sims to calculate (AI fail on time out and do nothing)
|
||||||
|
* <p>
|
||||||
|
* TODO: add tests and implement best choice selection on timeout
|
||||||
|
* (AI must make any good/bad choice on timeout with game log - not a skip)
|
||||||
|
*
|
||||||
* @author JayDi85
|
* @author JayDi85
|
||||||
*/
|
*/
|
||||||
public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
|
|
@ -21,7 +31,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_AIvsAI_Simple() {
|
public void test_Simple_ShortGame() {
|
||||||
// both must kill x2 bears by x2 bolts
|
// both must kill x2 bears by x2 bolts
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2);
|
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2);
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 2);
|
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 2);
|
||||||
|
|
@ -39,7 +49,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_AIvsAI_LongGame() {
|
public void test_Simple_LongGame() {
|
||||||
// many bears and bolts must help to end game fast
|
// many bears and bolts must help to end game fast
|
||||||
int maxTurn = 50;
|
int maxTurn = 50;
|
||||||
removeAllCardsFromLibrary(playerA);
|
removeAllCardsFromLibrary(playerA);
|
||||||
|
|
@ -63,13 +73,27 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
Assert.assertTrue("One of player must won a game before turn " + maxTurn + ", but it ends on " + currentGame, currentGame.hasEnded());
|
Assert.assertTrue("One of player must won a game before turn " + maxTurn + ", but it ends on " + currentGame, currentGame.hasEnded());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runManyTargetOptionsTest(String info, int totalCreatures, int needDiedCreatures, int needPlayerLife) {
|
private void runManyTargetOptionsInTrigger(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) {
|
||||||
// When Bogardan Hellkite enters, it deals 5 damage divided as you choose among any number of targets.
|
// When Bogardan Hellkite enters, it deals 5 damage divided as you choose among any number of targets.
|
||||||
addCard(Zone.HAND, playerA, "Bogardan Hellkite", 1); // {6}{R}{R}
|
addCard(Zone.HAND, playerA, "Bogardan Hellkite", 1); // {6}{R}{R}
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
|
||||||
//
|
//
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures);
|
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures);
|
||||||
|
|
||||||
|
if (isDamageRandomCreature) {
|
||||||
|
runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> {
|
||||||
|
Permanent creature = game.getBattlefield().getAllPermanents().stream()
|
||||||
|
.filter(p -> p.getName().equals("Balduvian Bears"))
|
||||||
|
.findAny()
|
||||||
|
.orElse(null);
|
||||||
|
Assert.assertNotNull(creature);
|
||||||
|
Ability fakeAbility = new SimpleStaticAbility(null);
|
||||||
|
fakeAbility.setControllerId(player.getId());
|
||||||
|
fakeAbility.setSourceId(creature.getId());
|
||||||
|
creature.damage(1, fakeAbility, game);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setStrictChooseMode(true);
|
setStrictChooseMode(true);
|
||||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
execute();
|
execute();
|
||||||
|
|
@ -80,34 +104,107 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_AIvsAI_ManyTargetOptions_Simple() {
|
public void test_ManyTargetOptions_Triggered_Single() {
|
||||||
// 2 damage to bear and 3 damage to player B
|
// 2 damage to bear and 3 damage to player B
|
||||||
runManyTargetOptionsTest("1 target creature", 1, 1, 20 - 3);
|
runManyTargetOptionsInTrigger("1 target creature", 1, 1, false, 20 - 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_AIvsAI_ManyTargetOptions_Few() {
|
public void test_ManyTargetOptions_Triggered_Few() {
|
||||||
// 4 damage to x2 bears and 1 damage to player B
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
runManyTargetOptionsTest("2 target creatures", 2, 2, 20 - 1);
|
runManyTargetOptionsInTrigger("2 target creatures", 2, 2, false, 20 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_AIvsAI_ManyTargetOptions_Many() {
|
public void test_ManyTargetOptions_Triggered_Many() {
|
||||||
// 4 damage to x2 bears and 1 damage to player B
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
runManyTargetOptionsTest("5 target creatures", 2, 2, 20 - 1);
|
runManyTargetOptionsInTrigger("5 target creatures", 5, 2, false, 20 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // AI code must be improved
|
public void test_ManyTargetOptions_Triggered_TooMuch() {
|
||||||
// TODO: need memory optimization
|
// warning, can be slow
|
||||||
// TODO: need sims/options amount optimization (example: target name + score as unique param to reduce possible options)
|
|
||||||
// TODO: need best choice selection on timeout (AI must make any good/bad choice on timeout with game log - not a skip)
|
// make sure targets optimization works
|
||||||
public void test_AIvsAI_ManyTargetOptions_TooMuch() {
|
// (must ignore same targets for faster calc)
|
||||||
// possible problems:
|
|
||||||
// - big memory consumption on sims prepare (memory overflow)
|
|
||||||
// - too many sims to calculate (AI fail on time out and do nothing)
|
|
||||||
|
|
||||||
// 4 damage to x2 bears and 1 damage to player B
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
runManyTargetOptionsTest("50 target creatures", 50, 2, 20 - 1);
|
runManyTargetOptionsInTrigger("50 target creatures", 50, 2, false, 20 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore // TODO: AI do not support game simulations for target options in triggers
|
||||||
|
public void test_ManyTargetOptions_Triggered_TargetGroups() {
|
||||||
|
// make sure targets optimization can find unique creatures, e.g. damaged
|
||||||
|
|
||||||
|
// 4 damage to x2 bears and 1 damage to damaged bear
|
||||||
|
runManyTargetOptionsInTrigger("5 target creatures with one damaged", 5, 3, true, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runManyTargetOptionsInActivate(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) {
|
||||||
|
// Boulderfall deals 5 damage divided as you choose among any number of targets.
|
||||||
|
addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R}
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
|
||||||
|
//
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures);
|
||||||
|
|
||||||
|
if (isDamageRandomCreature) {
|
||||||
|
runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> {
|
||||||
|
Permanent creature = game.getBattlefield().getAllPermanents().stream()
|
||||||
|
.filter(p -> p.getName().equals("Balduvian Bears"))
|
||||||
|
.findAny()
|
||||||
|
.orElse(null);
|
||||||
|
Assert.assertNotNull(creature);
|
||||||
|
Ability fakeAbility = new SimpleStaticAbility(null);
|
||||||
|
fakeAbility.setControllerId(player.getId());
|
||||||
|
fakeAbility.setSourceId(creature.getId());
|
||||||
|
creature.damage(1, fakeAbility, game);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, "Boulderfall", 1); // if fail then AI stops before all sims ends
|
||||||
|
assertGraveyardCount(playerB, "Balduvian Bears", needDiedCreatures);
|
||||||
|
assertLife(playerB, needPlayerLife);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_ManyTargetOptions_Activated_Single() {
|
||||||
|
// 2 damage to bear and 3 damage to player B
|
||||||
|
runManyTargetOptionsInActivate("1 target creature", 1, 1, false, 20 - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_ManyTargetOptions_Activated_Few() {
|
||||||
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
|
runManyTargetOptionsInActivate("2 target creatures", 2, 2, false, 20 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_ManyTargetOptions_Activated_Many() {
|
||||||
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
|
runManyTargetOptionsInActivate("5 target creatures", 5, 2, false, 20 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_ManyTargetOptions_Activated_TooMuch() {
|
||||||
|
// warning, can be slow
|
||||||
|
|
||||||
|
// make sure targets optimization works
|
||||||
|
// (must ignore same targets for faster calc)
|
||||||
|
|
||||||
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
|
runManyTargetOptionsInActivate("50 target creatures", 50, 2, false, 20 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_ManyTargetOptions_Activated_TargetGroups() {
|
||||||
|
// make sure targets optimization can find unique creatures, e.g. damaged
|
||||||
|
|
||||||
|
// 4 damage to x2 bears and 1 damage to damaged bear
|
||||||
|
runManyTargetOptionsInActivate("5 target creatures with one damaged", 5, 3, true, 20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ public class TargetAmountAITest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_AI_SimulateTargets() {
|
public void test_AI_SimulateTargets() {
|
||||||
|
// warning, test depends on targets list optimization by AI
|
||||||
|
|
||||||
// Distribute four +1/+1 counters among any number of target creatures.
|
// Distribute four +1/+1 counters among any number of target creatures.
|
||||||
addCard(Zone.HAND, playerA, "Blessings of Nature", 1); // {4}{G}
|
addCard(Zone.HAND, playerA, "Blessings of Nature", 1); // {4}{G}
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
|
||||||
|
|
|
||||||
|
|
@ -4457,11 +4457,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only used for AIs
|
* AI related code
|
||||||
*
|
|
||||||
* @param ability
|
|
||||||
* @param game
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<Ability> getPlayableOptions(Ability ability, Game game) {
|
public List<Ability> getPlayableOptions(Ability ability, Game game) {
|
||||||
|
|
@ -4482,6 +4478,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI related code
|
||||||
|
*/
|
||||||
private void addModeOptions(List<Ability> options, Ability option, Game game) {
|
private void addModeOptions(List<Ability> options, Ability option, Game game) {
|
||||||
// TODO: support modal spells with more than one selectable mode (also must use max modes filter)
|
// TODO: support modal spells with more than one selectable mode (also must use max modes filter)
|
||||||
for (Mode mode : option.getModes().values()) {
|
for (Mode mode : option.getModes().values()) {
|
||||||
|
|
@ -4504,11 +4503,18 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI related code
|
||||||
|
*/
|
||||||
protected void addVariableXOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
protected void addVariableXOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
||||||
addTargetOptions(options, option, targetNum, game);
|
addTargetOptions(options, option, targetNum, game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI related code
|
||||||
|
*/
|
||||||
protected void addTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
protected void addTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
||||||
|
// TODO: target options calculated for triggered ability too, but do not used in real game
|
||||||
for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) {
|
for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) {
|
||||||
Ability newOption = option.copy();
|
Ability newOption = option.copy();
|
||||||
if (target instanceof TargetAmount) {
|
if (target instanceof TargetAmount) {
|
||||||
|
|
@ -4521,7 +4527,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true);
|
newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (targetNum < option.getTargets().size() - 2) {
|
if (targetNum < option.getTargets().size() - 2) { // wtf
|
||||||
addTargetOptions(options, newOption, targetNum + 1, game);
|
addTargetOptions(options, newOption, targetNum + 1, game);
|
||||||
} else if (!option.getCosts().getTargets().isEmpty()) {
|
} else if (!option.getCosts().getTargets().isEmpty()) {
|
||||||
addCostTargetOptions(options, newOption, 0, game);
|
addCostTargetOptions(options, newOption, 0, game);
|
||||||
|
|
@ -4531,6 +4537,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI related code
|
||||||
|
*/
|
||||||
private void addCostTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
private void addCostTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
||||||
for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(playerId, option, game)) {
|
for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(playerId, option, game)) {
|
||||||
Ability newOption = option.copy();
|
Ability newOption = option.copy();
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ public interface Target extends Serializable {
|
||||||
|
|
||||||
boolean isLegal(Ability source, Game game);
|
boolean isLegal(Ability source, Game game);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI related code. Returns all possible different target combinations
|
||||||
|
*/
|
||||||
List<? extends Target> getTargetOptions(Ability source, Game game);
|
List<? extends Target> getTargetOptions(Ability source, Game game);
|
||||||
|
|
||||||
boolean canChoose(UUID sourceControllerId, Game game);
|
boolean canChoose(UUID sourceControllerId, Game game);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
package mage.target;
|
package mage.target;
|
||||||
|
|
||||||
|
import mage.MageObject;
|
||||||
import mage.abilities.Ability;
|
import mage.abilities.Ability;
|
||||||
import mage.abilities.dynamicvalue.DynamicValue;
|
import mage.abilities.dynamicvalue.DynamicValue;
|
||||||
import mage.abilities.dynamicvalue.common.StaticValue;
|
import mage.abilities.dynamicvalue.common.StaticValue;
|
||||||
|
import mage.cards.Card;
|
||||||
import mage.constants.Outcome;
|
import mage.constants.Outcome;
|
||||||
import mage.game.Game;
|
import mage.game.Game;
|
||||||
|
import mage.game.permanent.Permanent;
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
|
import mage.util.DebugUtil;
|
||||||
|
import mage.util.RandomUtil;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
@ -118,45 +123,229 @@ public abstract class TargetAmount extends TargetImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<? extends TargetAmount> getTargetOptions(Ability source, Game game) {
|
final public List<? extends TargetAmount> getTargetOptions(Ability source, Game game) {
|
||||||
|
if (!amountWasSet) {
|
||||||
|
setAmount(source, game);
|
||||||
|
}
|
||||||
|
|
||||||
List<TargetAmount> options = new ArrayList<>();
|
List<TargetAmount> options = new ArrayList<>();
|
||||||
Set<UUID> possibleTargets = possibleTargets(source.getControllerId(), source, game);
|
Set<UUID> possibleTargets = possibleTargets(source.getControllerId(), source, game);
|
||||||
|
|
||||||
addTargets(this, possibleTargets, options, source, game);
|
// optimizations for less memory/cpu consumptions
|
||||||
|
printTargetsTableAndVariations("before optimize", game, possibleTargets, options, false);
|
||||||
|
optimizePossibleTargets(source, game, possibleTargets);
|
||||||
|
printTargetsTableAndVariations("after optimize", game, possibleTargets, options, false);
|
||||||
|
|
||||||
// debug target variations
|
// calc possible amount variations
|
||||||
//printTargetsVariations(possibleTargets, options);
|
addTargets(this, possibleTargets, options, source, game);
|
||||||
|
printTargetsTableAndVariations("after calc", game, possibleTargets, options, true);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printTargetsVariations(Set<UUID> possibleTargets, List<TargetAmount> options) {
|
/**
|
||||||
// debug target variations
|
* AI related, trying to reduce targets for simulations
|
||||||
// permanent index + amount
|
*/
|
||||||
// example: 7 -> 2; 8 -> 3; 9 -> 1
|
private void optimizePossibleTargets(Ability source, Game game, Set<UUID> possibleTargets) {
|
||||||
|
// remove duplicated/same creatures (example: distribute 3 damage between 10+ same tokens)
|
||||||
|
|
||||||
|
// it must have additional threshold to keep more variations for analyse
|
||||||
|
//
|
||||||
|
// bad example:
|
||||||
|
// - Blessings of Nature
|
||||||
|
// - Distribute four +1/+1 counters among any number of target creatures.
|
||||||
|
// on low targets threshold AI can put 1/1 to opponent's creature instead own, see TargetAmountAITest.test_AI_SimulateTargets
|
||||||
|
|
||||||
|
int maxPossibleTargetsToSimulate = this.remainingAmount * 2;
|
||||||
|
if (possibleTargets.size() < maxPossibleTargetsToSimulate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split targets by groups
|
||||||
|
Map<UUID, String> targetGroups = new HashMap<>();
|
||||||
|
possibleTargets.forEach(id -> {
|
||||||
|
String groupKey = "";
|
||||||
|
|
||||||
|
// player
|
||||||
|
Player player = game.getPlayer(id);
|
||||||
|
if (player != null) {
|
||||||
|
groupKey = getTargetGroupKeyAsPlayer(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// game object
|
||||||
|
MageObject object = game.getObject(id);
|
||||||
|
if (object != null) {
|
||||||
|
groupKey = object.getName();
|
||||||
|
if (object instanceof Permanent) {
|
||||||
|
groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object);
|
||||||
|
} else if (object instanceof Card) {
|
||||||
|
groupKey += getTargetGroupKeyAsCard(game, (Card) object);
|
||||||
|
} else {
|
||||||
|
groupKey += getTargetGroupKeyAsOther(game, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown - use all
|
||||||
|
if (groupKey.isEmpty()) {
|
||||||
|
groupKey = id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
targetGroups.put(id, groupKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, List<UUID>> groups = new HashMap<>();
|
||||||
|
targetGroups.forEach((id, groupKey) -> {
|
||||||
|
groups.computeIfAbsent(groupKey, k -> new ArrayList<>());
|
||||||
|
groups.get(groupKey).add(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// optimize logic:
|
||||||
|
// - use one target from each target group all the time
|
||||||
|
// - add random target from random group until fill all remainingAmount condition
|
||||||
|
|
||||||
|
// use one target per group
|
||||||
|
Set<UUID> newPossibleTargets = new HashSet<>();
|
||||||
|
groups.forEach((groupKey, groupTargets) -> {
|
||||||
|
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||||
|
if (targetId != null) {
|
||||||
|
newPossibleTargets.add(targetId);
|
||||||
|
groupTargets.remove(targetId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// use random target until fill condition
|
||||||
|
while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
|
||||||
|
String groupKey = RandomUtil.randomFromCollection(groups.keySet());
|
||||||
|
if (groupKey == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
List<UUID> groupTargets = groups.getOrDefault(groupKey, null);
|
||||||
|
if (groupTargets == null || groupTargets.isEmpty()) {
|
||||||
|
groups.remove(groupKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||||
|
if (targetId != null) {
|
||||||
|
newPossibleTargets.add(targetId);
|
||||||
|
groupTargets.remove(targetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep final result
|
||||||
|
possibleTargets.clear();
|
||||||
|
possibleTargets.addAll(newPossibleTargets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTargetGroupKeyAsPlayer(Player player) {
|
||||||
|
// use all
|
||||||
|
return String.join(";", Arrays.asList(
|
||||||
|
player.getName(),
|
||||||
|
String.valueOf(player.getId().hashCode())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) {
|
||||||
|
// split by name and stats
|
||||||
|
// TODO: rework and combine with PermanentEvaluator (to use battlefield score)
|
||||||
|
|
||||||
|
// try to use short text/hash for lesser data on debug
|
||||||
|
return String.join(";", Arrays.asList(
|
||||||
|
permanent.getName(),
|
||||||
|
String.valueOf(permanent.getControllerId().hashCode()),
|
||||||
|
String.valueOf(permanent.getOwnerId().hashCode()),
|
||||||
|
String.valueOf(permanent.isTapped()),
|
||||||
|
String.valueOf(permanent.getPower().getValue()),
|
||||||
|
String.valueOf(permanent.getToughness().getValue()),
|
||||||
|
String.valueOf(permanent.getDamage()),
|
||||||
|
String.valueOf(permanent.getCardType(game).toString().hashCode()),
|
||||||
|
String.valueOf(permanent.getSubtype(game).toString().hashCode()),
|
||||||
|
String.valueOf(permanent.getCounters(game).getTotalCount()),
|
||||||
|
String.valueOf(permanent.getAbilities(game).size()),
|
||||||
|
String.valueOf(permanent.getRules(game).toString().hashCode())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTargetGroupKeyAsCard(Game game, Card card) {
|
||||||
|
// split by name and stats
|
||||||
|
return String.join(";", Arrays.asList(
|
||||||
|
card.getName(),
|
||||||
|
String.valueOf(card.getOwnerId().hashCode()),
|
||||||
|
String.valueOf(card.getCardType(game).toString().hashCode()),
|
||||||
|
String.valueOf(card.getSubtype(game).toString().hashCode()),
|
||||||
|
String.valueOf(card.getCounters(game).getTotalCount()),
|
||||||
|
String.valueOf(card.getAbilities(game).size()),
|
||||||
|
String.valueOf(card.getRules(game).toString().hashCode())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTargetGroupKeyAsOther(Game game, MageObject item) {
|
||||||
|
// use all
|
||||||
|
return String.join(";", Arrays.asList(
|
||||||
|
item.getName(),
|
||||||
|
String.valueOf(item.getId().hashCode())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug only. Print targets table and variations.
|
||||||
|
*/
|
||||||
|
private void printTargetsTableAndVariations(String info, Game game, Set<UUID> possibleTargets, List<TargetAmount> options, boolean isPrintOptions) {
|
||||||
|
if (!DebugUtil.AI_SHOW_TARGET_OPTIMIZATION_LOGS) return;
|
||||||
|
|
||||||
|
// output example:
|
||||||
|
//
|
||||||
|
// Targets (after optimize): 5
|
||||||
|
// 0. Balduvian Bears [ac8], C, BalduvianBears, DKM:22::0, 2/2
|
||||||
|
// 1. PlayerA (SimulatedPlayer2)
|
||||||
|
//
|
||||||
|
// Target variations (info): 126
|
||||||
|
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 1; 4 -> 1
|
||||||
|
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 2
|
||||||
|
// 0 -> 1; 1 -> 1; 2 -> 1; 4 -> 2
|
||||||
|
|
||||||
|
// print table
|
||||||
List<UUID> list = new ArrayList<>(possibleTargets);
|
List<UUID> list = new ArrayList<>(possibleTargets);
|
||||||
|
Collections.sort(list);
|
||||||
HashMap<UUID, Integer> targetNumbers = new HashMap<>();
|
HashMap<UUID, Integer> targetNumbers = new HashMap<>();
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(String.format("Targets (%s): %d", info, list.size()));
|
||||||
for (int i = 0; i < list.size(); i++) {
|
for (int i = 0; i < list.size(); i++) {
|
||||||
targetNumbers.put(list.get(i), i);
|
targetNumbers.put(list.get(i), i);
|
||||||
|
String targetName;
|
||||||
|
Player player = game.getPlayer(list.get(i));
|
||||||
|
if (player != null) {
|
||||||
|
targetName = player.toString();
|
||||||
|
} else {
|
||||||
|
MageObject object = game.getObject(list.get(i));
|
||||||
|
if (object != null) {
|
||||||
|
targetName = object.toString();
|
||||||
|
} else {
|
||||||
|
targetName = "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(String.format("%d. %s", i, targetName));
|
||||||
}
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
if (!isPrintOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// print amount variations
|
||||||
List<String> res = options
|
List<String> res = options
|
||||||
.stream()
|
.stream()
|
||||||
.map(t -> t.getTargets()
|
.map(t -> t.getTargets()
|
||||||
.stream()
|
.stream()
|
||||||
.map(id -> targetNumbers.get(id) + " -> " + t.getTargetAmount(id))
|
.map(id -> targetNumbers.get(id) + " -> " + t.getTargetAmount(id))
|
||||||
.sorted()
|
.sorted()
|
||||||
.collect(Collectors.joining("; ")))
|
.collect(Collectors.joining("; "))).sorted().collect(Collectors.toList());
|
||||||
.collect(Collectors.toList());
|
|
||||||
Collections.sort(res);
|
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println(res.stream().collect(Collectors.joining("\n")));
|
System.out.println(String.format("Target variations (info): %d", options.size()));
|
||||||
|
System.out.println(String.join("\n", res));
|
||||||
System.out.println();
|
System.out.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addTargets(TargetAmount target, Set<UUID> possibleTargets, List<TargetAmount> options, Ability source, Game game) {
|
final protected void addTargets(TargetAmount target, Set<UUID> possibleTargets, List<TargetAmount> options, Ability source, Game game) {
|
||||||
if (!amountWasSet) {
|
|
||||||
setAmount(source, game);
|
|
||||||
}
|
|
||||||
Set<UUID> usedTargets = new HashSet<>();
|
Set<UUID> usedTargets = new HashSet<>();
|
||||||
for (UUID targetId : possibleTargets) {
|
for (UUID targetId : possibleTargets) {
|
||||||
usedTargets.add(targetId);
|
usedTargets.add(targetId);
|
||||||
|
|
|
||||||
|
|
@ -446,13 +446,6 @@ public abstract class TargetImpl implements Target {
|
||||||
return !targets.isEmpty();
|
return !targets.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all possible different target combinations
|
|
||||||
*
|
|
||||||
* @param source
|
|
||||||
* @param game
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public List<? extends TargetImpl> getTargetOptions(Ability source, Game game) {
|
public List<? extends TargetImpl> getTargetOptions(Ability source, Game game) {
|
||||||
List<TargetImpl> options = new ArrayList<>();
|
List<TargetImpl> options = new ArrayList<>();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ public class DebugUtil {
|
||||||
|
|
||||||
public static boolean NETWORK_SHOW_CLIENT_CALLBACK_MESSAGES_LOG = false; // show all callback messages (server commands)
|
public static boolean NETWORK_SHOW_CLIENT_CALLBACK_MESSAGES_LOG = false; // show all callback messages (server commands)
|
||||||
|
|
||||||
|
// AI
|
||||||
|
// game simulations runs in multiple threads, if you stop code to debug then it will be terminated by timeout
|
||||||
|
// so AI debug mode will make single simulation thread without any timeouts
|
||||||
|
public static boolean AI_ENABLE_DEBUG_MODE = false;
|
||||||
|
public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount
|
||||||
|
|
||||||
// cards basic (card panels)
|
// cards basic (card panels)
|
||||||
public static boolean GUI_CARD_DRAW_OUTER_BORDER = false;
|
public static boolean GUI_CARD_DRAW_OUTER_BORDER = false;
|
||||||
public static boolean GUI_CARD_DRAW_INNER_BORDER = false;
|
public static boolean GUI_CARD_DRAW_INNER_BORDER = false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue