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 357033b4f3..b8a6c69c3f 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 @@ -109,8 +109,9 @@ public class ComputerPlayer extends PlayerImpl implements Player { public boolean chooseMulligan(Game game) { log.debug("chooseMulligan"); if (hand.size() < 6 - || isTestMode() - || game.getClass().getName().contains("Momir")) { + || isTestsMode() // ignore mulligan in tests + || game.getClass().getName().contains("Momir") // ignore mulligan in Momir games + ) { return false; } Set lands = hand.getCards(new FilterLandCard(), game); @@ -2880,9 +2881,10 @@ public class ComputerPlayer extends PlayerImpl implements Player { @Override public boolean isHuman() { if (human) { - log.error("computer must be not human", new Throwable()); + throw new IllegalStateException("Computer player can't be Human"); + } else { + return false; } - return human; } @Override diff --git a/Mage.Sets/src/mage/cards/a/AdNauseam.java b/Mage.Sets/src/mage/cards/a/AdNauseam.java index 818c72e95b..b6f6c3886c 100644 --- a/Mage.Sets/src/mage/cards/a/AdNauseam.java +++ b/Mage.Sets/src/mage/cards/a/AdNauseam.java @@ -73,7 +73,7 @@ class AdNauseamEffect extends OneShotEffect { controller.revealCards(sourceCard.getIdName() + " put into hand", new CardsImpl(card), game); // AI workaround to stop infinite choose (only one card allows) - if (!controller.isHuman() && !controller.isTestMode()) { + if (controller.isComputer()) { break; } } diff --git a/Mage.Sets/src/mage/cards/c/ChoiceOfDamnations.java b/Mage.Sets/src/mage/cards/c/ChoiceOfDamnations.java index 4ab2e64b8d..ae860b899d 100644 --- a/Mage.Sets/src/mage/cards/c/ChoiceOfDamnations.java +++ b/Mage.Sets/src/mage/cards/c/ChoiceOfDamnations.java @@ -66,7 +66,7 @@ class ChoiceOfDamnationsEffect extends OneShotEffect { // AI hint int amount; - if (!targetPlayer.isHuman() && !targetPlayer.isTestMode()) { + if (targetPlayer.isComputer()) { // AI as defender int safeLifeToLost = Math.max(0, targetPlayer.getLife() / 2); amount = Math.min(numberPermanents, safeLifeToLost); @@ -80,7 +80,7 @@ class ChoiceOfDamnationsEffect extends OneShotEffect { // AI hint boolean chooseLoseLife; - if (!targetPlayer.isHuman() && !targetPlayer.isTestMode()) { + if (targetPlayer.isComputer()) { // AI as attacker chooseLoseLife = (numberPermanents == 0 || amount <= numberPermanents || targetPlayer.getLife() < amount); } else { diff --git a/Mage.Sets/src/mage/cards/c/CrazedFirecat.java b/Mage.Sets/src/mage/cards/c/CrazedFirecat.java index 940b77f0c3..a3bf67ae97 100644 --- a/Mage.Sets/src/mage/cards/c/CrazedFirecat.java +++ b/Mage.Sets/src/mage/cards/c/CrazedFirecat.java @@ -69,7 +69,7 @@ class CrazedFirecatEffect extends OneShotEffect { flipsWon++; // AI workaround to stop on good condition - if (!controller.isHuman() && !controller.isTestMode() && flipsWon >= 2) { + if (controller.isComputer() && flipsWon >= 2) { break; } } diff --git a/Mage.Sets/src/mage/cards/f/FieryGambit.java b/Mage.Sets/src/mage/cards/f/FieryGambit.java index 5a9a4e6d47..849b83dab1 100644 --- a/Mage.Sets/src/mage/cards/f/FieryGambit.java +++ b/Mage.Sets/src/mage/cards/f/FieryGambit.java @@ -72,7 +72,7 @@ class FieryGambitEffect extends OneShotEffect { } // AI workaround to stop flips on good result - if (!controller.isHuman() && !controller.isTestMode() && flipsWon >= 3) { + if (controller.isComputer() && flipsWon >= 3) { controllerStopped = true; break; } diff --git a/Mage.Sets/src/mage/cards/f/Fluctuator.java b/Mage.Sets/src/mage/cards/f/Fluctuator.java index 0682684bab..81885b8c45 100644 --- a/Mage.Sets/src/mage/cards/f/Fluctuator.java +++ b/Mage.Sets/src/mage/cards/f/Fluctuator.java @@ -69,7 +69,7 @@ class FluctuatorEffect extends CostModificationEffectImpl { } if (reduceMax > 0) { int reduce; - if (game.inCheckPlayableState() || !controller.isHuman()) { + if (game.inCheckPlayableState() || controller.isComputer()) { reduce = reduceMax; } else { ChoiceImpl choice = new ChoiceImpl(true); diff --git a/Mage.Sets/src/mage/cards/i/IllicitAuction.java b/Mage.Sets/src/mage/cards/i/IllicitAuction.java index dfc42805a5..667434c85c 100644 --- a/Mage.Sets/src/mage/cards/i/IllicitAuction.java +++ b/Mage.Sets/src/mage/cards/i/IllicitAuction.java @@ -78,7 +78,9 @@ class IllicitAuctionEffect extends GainControlTargetEffect { if (currentPlayer.canRespond() && currentPlayer.chooseUse(Outcome.GainControl, text, source, game)) { int newBid = 0; - if (!currentPlayer.isHuman()) {//AI will evaluate the creature and bid + if (currentPlayer.isComputer()) { + // AI hint + // AI will evaluate the creature and bid CreatureEvaluator eval = new CreatureEvaluator(); int computerLife = currentPlayer.getLife(); int creatureValue = eval.evaluate(targetCreature, game); diff --git a/Mage.Sets/src/mage/cards/i/InvasionPlans.java b/Mage.Sets/src/mage/cards/i/InvasionPlans.java index b01c0b69d6..d08a9c659e 100644 --- a/Mage.Sets/src/mage/cards/i/InvasionPlans.java +++ b/Mage.Sets/src/mage/cards/i/InvasionPlans.java @@ -70,7 +70,7 @@ class InvasionPlansEffect extends ContinuousRuleModifyingEffectImpl { Player blockController = game.getPlayer(game.getCombat().getAttackingPlayerId()); if (blockController != null) { // temporary workaround for AI bugging out while choosing blockers - if (blockController.isHuman()) { + if (!blockController.isComputer()) { game.getCombat().selectBlockers(blockController, source, game); return event.getPlayerId().equals(game.getCombat().getAttackingPlayerId()); } diff --git a/Mage.Sets/src/mage/cards/l/LimDulsVault.java b/Mage.Sets/src/mage/cards/l/LimDulsVault.java index c18e549184..5a0d97054a 100644 --- a/Mage.Sets/src/mage/cards/l/LimDulsVault.java +++ b/Mage.Sets/src/mage/cards/l/LimDulsVault.java @@ -73,7 +73,11 @@ class LimDulsVaultEffect extends OneShotEffect { player.shuffleLibrary(source, game); player.putCardsOnTopOfLibrary(cards, game, source, true); } - } while (doAgain && player.isHuman()); // AI must stop using it as infinite + // AI must stop using it as infinite + if (player.isComputer()) { + break; + } + } while (doAgain); return true; } diff --git a/Mage.Sets/src/mage/cards/m/MagesContest.java b/Mage.Sets/src/mage/cards/m/MagesContest.java index 3164d6732b..7a27c03559 100644 --- a/Mage.Sets/src/mage/cards/m/MagesContest.java +++ b/Mage.Sets/src/mage/cards/m/MagesContest.java @@ -70,13 +70,15 @@ class MagesContestEffect extends OneShotEffect { do { if (currentPlayer.canRespond()) { int newBid = 0; - if (!currentPlayer.isHuman()) { + if (currentPlayer.isComputer()) { + // AI hint // make AI evaluate value of the spell to decide on bidding, should be reworked int maxBid = Math.min(RandomUtil.nextInt(Math.max(currentPlayer.getLife(), 1)) + RandomUtil.nextInt(Math.max(spell.getConvertedManaCost(), 1)), currentPlayer.getLife()); if (highBid + 1 < maxBid) { newBid = highBid + 1; } } else if (currentPlayer.chooseUse(Outcome.Benefit, winner.getLogName() + " has bet " + highBid + " life. Top the bid?", source, game)) { + // Human choose newBid = currentPlayer.getAmount(highBid + 1, Integer.MAX_VALUE, "Choose bid", game); } if (newBid > highBid) { diff --git a/Mage.Sets/src/mage/cards/p/PainsReward.java b/Mage.Sets/src/mage/cards/p/PainsReward.java index 9e379ccff6..cde4a27531 100644 --- a/Mage.Sets/src/mage/cards/p/PainsReward.java +++ b/Mage.Sets/src/mage/cards/p/PainsReward.java @@ -92,7 +92,7 @@ class PainsRewardEffect extends OneShotEffect { private int chooseLifeAmountToBid(Player player, int currentBig, Game game) { int newBid; - if (!player.isHuman() && !player.isTestMode()) { + if (player.isComputer()) { // AI choose newBid = currentBig + 1; } else { diff --git a/Mage.Sets/src/mage/cards/s/SlaughterTheStrong.java b/Mage.Sets/src/mage/cards/s/SlaughterTheStrong.java index 18251fde02..65a4786aae 100644 --- a/Mage.Sets/src/mage/cards/s/SlaughterTheStrong.java +++ b/Mage.Sets/src/mage/cards/s/SlaughterTheStrong.java @@ -92,14 +92,16 @@ class SlaughterTheStrongEffect extends OneShotEffect { // human can de-select targets, but AI must choose only one time Target target; - if (player.isHuman()) { - target = new TargetPermanent(0, 1, currentFilter, true); - } else { + if (player.isComputer()) { + // AI settings FilterControlledCreaturePermanent strictFilter = currentFilter.copy(); selectedCreatures.stream().forEach(id -> { strictFilter.add(Predicates.not(new PermanentIdPredicate(id))); }); target = new TargetPermanent(0, 1, strictFilter, true); + } else { + // Human settings + target = new TargetPermanent(0, 1, currentFilter, true); } player.chooseTarget(Outcome.BoostCreature, target, source, game); @@ -110,7 +112,11 @@ class SlaughterTheStrongEffect extends OneShotEffect { selectedCreatures.add(target.getFirstTarget()); } } else { - if (player.isHuman()) { + if (player.isComputer()) { + // AI stops + selectionDone = true; + } else { + // Human can continue String selected = "Selected: "; for (UUID creatureId : selectedCreatures) { Permanent creature = game.getPermanent(creatureId); @@ -123,8 +129,6 @@ class SlaughterTheStrongEffect extends OneShotEffect { selected, "End the selection", "Continue the selection", source, game); - } else { - selectionDone = true; } } } diff --git a/Mage.Sets/src/mage/cards/v/VolcanoHellion.java b/Mage.Sets/src/mage/cards/v/VolcanoHellion.java index 66e5ff2148..c1f828f3ef 100644 --- a/Mage.Sets/src/mage/cards/v/VolcanoHellion.java +++ b/Mage.Sets/src/mage/cards/v/VolcanoHellion.java @@ -72,7 +72,7 @@ class VolcanoHellionEffect extends OneShotEffect { Permanent permanent = game.getPermanent(source.getFirstTarget()); if (controller != null) { int amount; - if (!controller.isHuman() && !controller.isTestMode()) { + if (controller.isComputer()) { // AI hint: have much life and can destroy target permanent int safeLifeToLost = Math.min(6, controller.getLife() / 2); if (permanent != null && permanent.getToughness().getValue() <= safeLifeToLost) { diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 020d86780d..accfa2b5b0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -86,8 +86,14 @@ public class TestPlayer implements Player { private int maxCallsWithoutAction = 400; private int foundNoAction = 0; - private boolean AIPlayer; // full playable AI - private boolean AICanChooseInStrictMode = false; // AI can choose in custom aiXXX commands (e.g. on one priority or step) + + // full playable AI, TODO: can be deleted? + private boolean AIPlayer; + // AI simulates a real game, e.g. ignores strict mode and play command/priority, see aiXXX commands + // true - unit tests uses real AI logic (e.g. AI hints and AI workarounds in cards) + // false - unit tests uses Human logic and dialogs + private boolean AIRealGameSimulation = false; + private final List actions = new ArrayList<>(); private final Map actionsToRemoveLater = new HashMap<>(); // remove actions later, on next step (e.g. for AI commands) private final Map>> rollbackActions = new HashMap<>(); // actions to add after a executed rollback @@ -125,7 +131,7 @@ public class TestPlayer implements Player { public TestPlayer(final TestPlayer testPlayer) { this.AIPlayer = testPlayer.AIPlayer; - this.AICanChooseInStrictMode = testPlayer.AICanChooseInStrictMode; + this.AIRealGameSimulation = testPlayer.AIRealGameSimulation; this.foundNoAction = testPlayer.foundNoAction; this.actions.addAll(testPlayer.actions); this.choices.addAll(testPlayer.choices); @@ -720,7 +726,7 @@ public class TestPlayer implements Player { // play priority if (command.equals(AI_COMMAND_PLAY_PRIORITY)) { - AICanChooseInStrictMode = true; // disable on action's remove + AIRealGameSimulation = true; // disable on action's remove computerPlayer.priority(game); actions.remove(action); return true; @@ -728,7 +734,7 @@ public class TestPlayer implements Player { // play step if (command.equals(AI_COMMAND_PLAY_STEP)) { - AICanChooseInStrictMode = true; // disable on action's remove + AIRealGameSimulation = true; // disable on action's remove actionsToRemoveLater.put(action, game.getStep().getType()); computerPlayer.priority(game); return true; @@ -1897,7 +1903,7 @@ public class TestPlayer implements Player { } private void chooseStrictModeFailed(String choiceType, Game game, String reason, boolean printAbilities) { - if (strictChooseMode && !AICanChooseInStrictMode) { + if (strictChooseMode && !AIRealGameSimulation) { if (printAbilities) { printStart("Available mana for " + computerPlayer.getName()); printMana(game, computerPlayer.getManaAvailable(game)); @@ -3060,7 +3066,18 @@ public class TestPlayer implements Player { @Override public boolean isHuman() { - return computerPlayer.isHuman(); + return false; + } + + @Override + public boolean isComputer() { + // all players in unit tests are computers, so you must use AIRealGameSimulation to test different logic (Human vs AI) + if (isTestsMode()) { + return AIRealGameSimulation; + } else { + throw new IllegalStateException("Can't use test player outside of unit tests"); + //return !isHuman(); + } } @Override @@ -3449,8 +3466,8 @@ public class TestPlayer implements Player { } @Override - public boolean isTestMode() { - return computerPlayer.isTestMode(); + public boolean isTestsMode() { + return computerPlayer.isTestsMode(); } @Override @@ -4193,8 +4210,8 @@ public class TestPlayer implements Player { return computerPlayer; } - public void setAICanChooseInStrictMode(boolean AICanChooseInStrictMode) { - this.AICanChooseInStrictMode = AICanChooseInStrictMode; + public void setAIRealGameSimulation(boolean AIRealGameSimulation) { + this.AIRealGameSimulation = AIRealGameSimulation; } public Map>> getRollbackActions() { diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java index 5f1e9d2f9d..27475a73d4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java @@ -13,6 +13,10 @@ import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl; import java.io.FileNotFoundException; /** + * PlayerA is full AI player and process all actions as AI logic. You don't need aiXXX commands in that tests. + * + * If you need custom AI tests then use CardTestPlayerBaseWithAIHelps with aiXXX commands + * * @author LevelX2 */ public abstract class CardTestPlayerBaseAI extends CardTestPlayerAPIImpl { @@ -33,6 +37,7 @@ public abstract class CardTestPlayerBaseAI extends CardTestPlayerAPIImpl { if (name.equals("PlayerA")) { TestPlayer testPlayer = new TestPlayer(new TestComputerPlayer7("PlayerA", RangeOfInfluence.ONE, skill)); testPlayer.setAIPlayer(true); + testPlayer.setAIRealGameSimulation(true); // enable AI logic simulation for all turns by default return testPlayer; } return super.createPlayer(name, rangeOfInfluence); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 0ff795c71f..d287515207 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1571,9 +1571,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } /** - * AI play one PRIORITY with multi game simulations (calcs and play ONE best - * action, can be called with stack) All choices must be made by AI - * (e.g.strict mode possible) + * AI play one PRIORITY with multi game simulations like real game + * (calcs and play ONE best action, can be called with stack) + * All choices must be made by AI (e.g.strict mode possible) * * @param turnNum * @param step @@ -1595,11 +1595,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } public PlayerAction createAIPlayerAction(int turnNum, PhaseStep step, String aiCommand) { - // AI actions must disable and enable strict mode + // AI commands must disable and enable real game simulation and strict mode return new PlayerAction("", turnNum, step, AI_PREFIX + aiCommand) { @Override public void onActionRemovedLater(Game game, TestPlayer player) { - player.setAICanChooseInStrictMode(false); + player.setAIRealGameSimulation(false); } }; } diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index d28fa3c596..badd131e29 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -477,7 +477,7 @@ public class PlayerStub implements Player { } @Override - public boolean isTestMode() { + public boolean isTestsMode() { return false; } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index beeb6601ed..22b586c796 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -319,7 +319,7 @@ public abstract class AbilityImpl implements Ability { // unit tests only: it allows to add targets/choices by two ways: // 1. From cast/activate command params (process it here) // 2. From single addTarget/setChoice, it's a preffered method for tests (process it in normal choose dialogs like human player) - if (controller.isTestMode()) { + if (controller.isTestsMode()) { if (!controller.addTargets(this, game)) { return false; } diff --git a/Mage/src/main/java/mage/abilities/keyword/AssistAbility.java b/Mage/src/main/java/mage/abilities/keyword/AssistAbility.java index 5eb2372528..aa638d2295 100644 --- a/Mage/src/main/java/mage/abilities/keyword/AssistAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/AssistAbility.java @@ -94,8 +94,9 @@ public class AssistAbility extends SimpleStaticAbility implements AlternateManaP } // AI can't use assist (can't ask another player to help), maybe in teammode it can be enabled, but tests must works all the time + // Outcome.AIDontUseIt Player controller = game.getPlayer(source.getControllerId()); - if (controller != null && !controller.isTestMode() && !controller.isHuman()) { + if (controller != null && controller.isComputer()) { return options; } @@ -170,7 +171,7 @@ class AssistEffect extends OneShotEffect { if (controller != null && spell != null && targetPlayer != null) { // AI can't assist other players, maybe for teammates only (but tests must work as normal) int amountToPay = 0; - if (targetPlayer.isHuman() || targetPlayer.isTestMode()) { + if (!targetPlayer.isComputer()) { amountToPay = targetPlayer.announceXMana(0, unpaid.getMana().getGeneric(), "How much mana to pay as assist for " + controller.getName() + "?", game, source); } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index e725c6ff25..6b3f899435 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1418,7 +1418,7 @@ public abstract class GameImpl implements Game, Serializable { + ex.getMessage()); } Player activePlayer = this.getPlayer(getActivePlayerId()); - if (activePlayer != null && !activePlayer.isTestMode()) { + if (activePlayer != null && !activePlayer.isTestsMode()) { errorContinueCounter++; continue; } else { diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 4e24915afe..50ec95bf20 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -48,8 +48,32 @@ import java.util.*; */ public interface Player extends MageItem, Copyable { + /** + * Current player is real life player (human). Try to use in GUI and network engine only. + * + * WARNING, you must use isComputer instead isHuman in card's code (for good Human/AI logic testing in unit tests) + * TODO: check combat code and other and replace isHuman to isComputer usage if possible (if AI support that actions) + * @return + */ boolean isHuman(); + boolean isTestsMode(); + + /** + * Current player is AI. Use it in card's code and all other places. + * + * It help to split Human/AI logic and test both by unit tests. + * + * Usage example: AI hint to skip or auto-calculate choices instead call of real choose dialogs + * - unit tests for Human logic: call normal commands + * - unit tests for AI logic: call aiXXX commands + * + * @return + */ + default boolean isComputer() { + return !isHuman(); + } + String getName(); String getLogName(); @@ -314,8 +338,6 @@ public interface Player extends MageItem, Copyable { void setGameUnderYourControl(boolean value, boolean fullRestore); - boolean isTestMode(); - void setTestMode(boolean value); void addAction(String action); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index a0665d5037..d91aa198a7 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2715,7 +2715,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // only humans can use it - if (!targetPlayer.isHuman() && !targetPlayer.isTestMode()) { + if (targetPlayer.isComputer()) { return false; } @@ -3866,7 +3866,7 @@ public abstract class PlayerImpl implements Player, Serializable { } @Override - public boolean isTestMode() { + public boolean isTestsMode() { return isTestMode; }