From 366401b0b22ef889f4ad659557bdae18e0a20af3 Mon Sep 17 00:00:00 2001 From: magenoxx Date: Mon, 14 Feb 2011 20:49:16 +0300 Subject: [PATCH] Updated Game interface to skip drawing. Added parsing test scenarios and updating players' zones. --- .../src/mage/player/ai/ComputerPlayer6.java | 656 ++++++++++++++++++ Mage.Tests/config/config.xml | 2 +- Mage.Tests/plugins/mage-player-ai-ma.jar | Bin 41983 -> 54938 bytes .../mage/test/serverside/PlayGameTest.java | 111 ++- .../test/serverside/base/MageTestBase.java | 3 + Mage/src/mage/game/Game.java | 14 +- Mage/src/mage/game/GameImpl.java | 115 ++- Mage/src/mage/players/Player.java | 1 + Mage/src/mage/players/PlayerImpl.java | 11 +- 9 files changed, 895 insertions(+), 18 deletions(-) create mode 100644 Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java 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 new file mode 100644 index 0000000000..340b290b97 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -0,0 +1,656 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ + +package mage.player.ai; + +import mage.Constants.Outcome; +import mage.Constants.PhaseStep; +import mage.Constants.RangeOfInfluence; +import mage.abilities.Ability; +import mage.abilities.ActivatedAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.SearchEffect; +import mage.cards.Card; +import mage.cards.Cards; +import mage.choices.Choice; +import mage.filter.FilterAbility; +import mage.game.Game; +import mage.game.combat.Combat; +import mage.game.combat.CombatGroup; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.StackAbility; +import mage.game.stack.StackObject; +import mage.game.turn.*; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetCard; +import mage.util.Logging; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author nantuko + */ +public class ComputerPlayer6 extends ComputerPlayer implements Player { + + private static final transient Logger logger = Logging.getLogger(ComputerPlayer6.class.getName()); + private static final ExecutorService pool = Executors.newFixedThreadPool(1); + + protected int maxDepth; + protected int maxNodes; + protected LinkedList actions = new LinkedList(); + protected List targets = new ArrayList(); + protected List choices = new ArrayList(); + protected Combat combat; + protected int currentScore; + protected SimulationNode2 root; + + public ComputerPlayer6(String name, RangeOfInfluence range) { + super(name, range); + maxDepth = Config2.maxDepth; + maxNodes = Config2.maxNodes; + } + + public ComputerPlayer6(final ComputerPlayer6 player) { + super(player); + this.maxDepth = player.maxDepth; + this.currentScore = player.currentScore; + if (player.combat != null) + this.combat = player.combat.copy(); + for (Ability ability: player.actions) { + actions.add(ability); + } + for (UUID targetId: player.targets) { + targets.add(targetId); + } + for (String choice: player.choices) { + choices.add(choice); + } + } + + @Override + public ComputerPlayer6 copy() { + return new ComputerPlayer6(this); + } + + @Override + public void priority(Game game) { + logState(game); + game.firePriorityEvent(playerId); + switch (game.getTurn().getStepType()) { + case UPKEEP: + case DRAW: + pass(); + break; + case PRECOMBAT_MAIN: + case POSTCOMBAT_MAIN: + if (game.getActivePlayerId().equals(playerId)) { + Player player = game.getPlayer(playerId); + System.out.println("Turn::"+game.getTurnNum()); + System.out.println("[" + game.getPlayer(playerId).getName() + "] " + game.getTurn().getStepType().name() +", life=" + player.getLife()); + String s = "["; + for (Card card : player.getHand().getCards(game)) { + s += card.getName() + ";"; + } + s += "]"; + System.out.println("Hand: " + s); + s = "["; + for (Permanent permanent : game.getBattlefield().getAllPermanents()) { + if (permanent.getOwnerId().equals(player.getId())) { + s += permanent.getName() + ";"; + } + } + s += "]"; + System.out.println("Permanents: " + s); + } + if (actions.size() == 0) { + calculateActions(game); + } + act(game); + break; + case BEGIN_COMBAT: + case DECLARE_ATTACKERS: + case DECLARE_BLOCKERS: + case COMBAT_DAMAGE: + case END_COMBAT: + case END_TURN: + pass(); + break; + case CLEANUP: + pass(); + break; + } + } + + protected void act(Game game) { + if (actions == null || actions.size() == 0) + pass(); + else { + boolean usedStack = false; + while (actions.peek() != null) { + Ability ability = actions.poll(); + System.out.println("[" + game.getPlayer(playerId).getName() + "] Action: " + ability.toString()); + this.activateAbility((ActivatedAbility) ability, game); + if (ability.isUsesStack()) + usedStack = true; + } + if (usedStack) + pass(); + } + } + + protected void calculateActions(Game game) { + currentScore = GameStateEvaluator2.evaluate(playerId, game); + if (!getNextAction(game)) { + Game sim = createSimulation(game); + SimulationNode2.resetCount(); + root = new SimulationNode2(sim, maxDepth, playerId); + logger.info("simulating actions"); + addActionsTimed(new FilterAbility()); + if (root.children.size() > 0) { + root = root.children.get(0); + int bestScore = GameStateEvaluator2.evaluate(playerId, root.getGame()); + if (bestScore > currentScore) { + actions = new LinkedList(root.abilities); + combat = root.combat; + } + } + } + } + + protected boolean getNextAction(Game game) { + if (root != null && root.children.size() > 0) { + SimulationNode2 test = root; + root = root.children.get(0); + while (root.children.size() > 0 && !root.playerId.equals(playerId)) { + test = root; + root = root.children.get(0); + } + logger.info("simlating -- game value:" + game.getState().getValue() + " test value:" + test.gameValue); + if (root.playerId.equals(playerId) && root.abilities != null && game.getState().getValue() == test.gameValue) { + logger.info("simulating -- continuing previous action chain"); + actions = new LinkedList(root.abilities); + combat = root.combat; + return true; + } + else { + return false; + } + } + return false; + } + + protected int minimaxAB(SimulationNode2 node, FilterAbility filter, int depth, int alpha, int beta) { + UUID currentPlayerId = node.getGame().getPlayerList().get(); + SimulationNode2 bestChild = null; + for (SimulationNode2 child: node.getChildren()) { + if (alpha >= beta) { + logger.info("alpha beta pruning"); + break; + } + if (SimulationNode2.nodeCount > maxNodes) { + logger.info("simulating -- reached end-state, count=" + SimulationNode2.nodeCount); + break; + } + int val = addActions(child, filter, depth-1, alpha, beta); + if (!currentPlayerId.equals(playerId)) { + if (val < beta) { + beta = val; + bestChild = child; + if (node.getCombat() == null) + node.setCombat(child.getCombat()); + } + } + else { + if (val > alpha) { + alpha = val; + bestChild = child; + if (node.getCombat() == null) + node.setCombat(child.getCombat()); + } + } + } + node.children.clear(); + if (bestChild != null) + node.children.add(bestChild); + if (!currentPlayerId.equals(playerId)) { + //logger.info("returning minimax beta: " + beta); + return beta; + } + else { + //logger.info("returning minimax alpha: " + alpha); + return alpha; + } + } + + protected SearchEffect getSearchEffect(StackAbility ability) { + for (Effect effect: ability.getEffects()) { + if (effect instanceof SearchEffect) { + return (SearchEffect) effect; + } + } + return null; + } + + protected void resolve(SimulationNode2 node, int depth, Game game) { + StackObject ability = game.getStack().pop(); + if (ability instanceof StackAbility) { + SearchEffect effect = getSearchEffect((StackAbility) ability); + if (effect != null && ability.getControllerId().equals(playerId)) { + Target target = effect.getTarget(); + if (!target.doneChosing()) { + for (UUID targetId: target.possibleTargets(ability.getSourceId(), ability.getControllerId(), game)) { + Game sim = game.copy(); + StackAbility newAbility = (StackAbility) ability.copy(); + SearchEffect newEffect = getSearchEffect((StackAbility) newAbility); + newEffect.getTarget().addTarget(targetId, newAbility, sim); + sim.getStack().push(newAbility); + SimulationNode2 newNode = new SimulationNode2(sim, depth, ability.getControllerId()); + node.children.add(newNode); + newNode.getTargets().add(targetId); + logger.fine("simulating search -- node#: " + SimulationNode2.getCount() + "for player: " + sim.getPlayer(ability.getControllerId()).getName()); + } + return; + } + } + } + //logger.info("simulating resolve "); + ability.resolve(game); + game.applyEffects(); + game.getPlayers().resetPassed(); + game.getPlayerList().setCurrent(game.getActivePlayerId()); + } + + protected void addActionsTimed(final FilterAbility filter) { + FutureTask task = new FutureTask(new Callable() { + public Integer call() throws Exception + { + return addActions(root, filter, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + }); + pool.execute(task); + try { + task.get(Config2.maxThinkSeconds, TimeUnit.MINUTES); + } catch (TimeoutException e) { + logger.info("simulating - timed out"); + task.cancel(true); + } catch (ExecutionException e) { + e.printStackTrace(); + task.cancel(true); + } catch (InterruptedException e) { + e.printStackTrace(); + task.cancel(true); + } + } + + protected int addActions(SimulationNode2 node, FilterAbility filter, int depth, int alpha, int beta) { + logger.fine("addActions: " + depth + ", alpha=" + alpha + ", beta=" + beta); + Game game = node.getGame(); + int val; + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.info("interrupted"); + val = GameStateEvaluator2.evaluate(playerId, game); + return val; + } + if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.isGameOver()) { + logger.fine("simulating -- reached end state, node count=" + SimulationNode2.nodeCount + ", depth=" + depth); + val = GameStateEvaluator2.evaluate(playerId, game); + return val; + } + else if (node.getChildren().size() > 0) { + logger.fine("simulating -- somthing added children:" + node.getChildren().size()); + val = minimaxAB(node, filter, depth-1, alpha, beta); + return val; + } + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("simulating -- alpha: " + alpha + " beta: " + beta + " depth:" + depth + " step:" + game.getTurn().getStepType() + " for player:" + (node.getPlayerId().equals(playerId) ? "yes" : "no")); + if (allPassed(game)) { + if (!game.getStack().isEmpty()) { + resolve(node, depth, game); + } + else { + game.getPlayers().resetPassed(); + playNext(game, game.getActivePlayerId(), node); + } + } + + if (game.isGameOver()) { + val = GameStateEvaluator2.evaluate(playerId, game); + } + else if (node.getChildren().size() > 0) { + //declared attackers or blockers or triggered abilities + logger.fine("simulating -- attack/block/trigger added children:" + node.getChildren().size()); + val = minimaxAB(node, filter, depth-1, alpha, beta); + } + else { + val = simulatePriority(node, game, filter, depth, alpha, beta); + } + } + + if (logger.isLoggable(Level.FINE)) + logger.fine("returning -- score: " + val + " depth:" + depth + " step:" + game.getTurn().getStepType() + " for player:" + game.getPlayer(node.getPlayerId()).getName()); + return val; + + } + + protected int simulatePriority(SimulationNode2 node, Game game, FilterAbility filter, int depth, int alpha, int beta) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.info("interrupted"); + return GameStateEvaluator2.evaluate(playerId, game); + } + node.setGameValue(game.getState().getValue()); + SimulatedPlayer2 currentPlayer = (SimulatedPlayer2) game.getPlayer(game.getPlayerList().get()); + //logger.info("simulating -- player " + currentPlayer.getName()); + SimulationNode2 bestNode = null; + List allActions = currentPlayer.simulatePriority(game, filter); + if (logger.isLoggable(Level.FINE)) + logger.fine("simulating -- adding " + allActions.size() + " children:" + allActions); + for (Ability action: allActions) { + Game sim = game.copy(); + if (sim.getPlayer(currentPlayer.getId()).activateAbility((ActivatedAbility) action.copy(), sim)) { + sim.applyEffects(); + if (!sim.isGameOver() && action.isUsesStack()) { + // only pass if the last action uses the stack + sim.getPlayer(currentPlayer.getId()).pass(); + sim.getPlayerList().getNext(); + } + SimulationNode2 newNode = new SimulationNode2(sim, action, depth, currentPlayer.getId()); + if (logger.isLoggable(Level.FINE)) + logger.fine("simulating -- node #:" + SimulationNode2.getCount() + " actions:" + action); + sim.checkStateAndTriggered(); + int val = addActions(newNode, filter, depth-1, alpha, beta); + if (!currentPlayer.getId().equals(playerId)) { + if (val < beta) { + beta = val; + bestNode = newNode; + node.setCombat(newNode.getCombat()); + } + } + else { + if (val > alpha) { + alpha = val; + bestNode = newNode; + node.setCombat(newNode.getCombat()); + if (node.getTargets().size() > 0) + targets = node.getTargets(); + if (node.getChoices().size() > 0) + choices = node.getChoices(); + } + } + if (alpha >= beta) { + //logger.info("simulating -- pruning"); + break; + } + if (SimulationNode2.nodeCount > maxNodes) { + logger.fine("simulating -- reached end-state"); + break; + } + } + } + if (bestNode != null) { + node.children.clear(); + node.children.add(bestNode); + } + if (!currentPlayer.getId().equals(playerId)) { + //logger.info("returning priority beta: " + beta); + return beta; + } + else { + //logger.info("returning priority alpha: " + alpha); + return alpha; + } + } + + protected boolean allPassed(Game game) { + for (Player player: game.getPlayers().values()) { + if (!player.isPassed() && !player.hasLost() && !player.hasLeft()) + return false; + } + return true; + } + + @Override + public boolean choose(Outcome outcome, Choice choice, Game game) { + if (choices.size() == 0) + return super.choose(outcome, choice, game); + if (!choice.isChosen()) { + for (String achoice: choices) { + choice.setChoice(achoice); + if (choice.isChosen()) { + choices.clear(); + return true; + } + } + return false; + } + return true; + } + + @Override + public boolean chooseTarget(Cards cards, TargetCard target, Ability source, Game game) { + if (targets.size() == 0) + return super.chooseTarget(cards, target, source, game); + if (!target.doneChosing()) { + for (UUID targetId: targets) { + target.addTarget(targetId, source, game); + if (target.doneChosing()) { + targets.clear(); + return true; + } + } + return false; + } + return true; + } + + @Override + public boolean choose(Cards cards, TargetCard target, Game game) { + if (targets.size() == 0) + return super.choose(cards, target, game); + if (!target.doneChosing()) { + for (UUID targetId: targets) { + target.add(targetId, game); + if (target.doneChosing()) { + targets.clear(); + return true; + } + } + return false; + } + return true; + } + + public void playNext(Game game, UUID activePlayerId, SimulationNode2 node) { + boolean skip = false; + while (true) { + Phase currentPhase = game.getPhase(); + if (!skip) + currentPhase.getStep().endStep(game, activePlayerId); + game.applyEffects(); + switch (currentPhase.getStep().getType()) { + case UNTAP: + game.getPhase().setStep(new UpkeepStep()); + break; + case UPKEEP: + game.getPhase().setStep(new DrawStep()); + break; + case DRAW: + game.getTurn().setPhase(new PreCombatMainPhase()); + game.getPhase().setStep(new PreCombatMainStep()); + break; + case PRECOMBAT_MAIN: + game.getTurn().setPhase(new CombatPhase()); + game.getPhase().setStep(new BeginCombatStep()); + break; + case BEGIN_COMBAT: + game.getPhase().setStep(new DeclareAttackersStep()); + break; + case DECLARE_ATTACKERS: + game.getPhase().setStep(new DeclareBlockersStep()); + break; + case DECLARE_BLOCKERS: + game.getPhase().setStep(new CombatDamageStep(true)); + break; + case COMBAT_DAMAGE: + if (((CombatDamageStep)currentPhase.getStep()).getFirst()) + game.getPhase().setStep(new CombatDamageStep(false)); + else + game.getPhase().setStep(new EndOfCombatStep()); + break; + case END_COMBAT: + game.getTurn().setPhase(new PostCombatMainPhase()); + game.getPhase().setStep(new PostCombatMainStep()); + break; + case POSTCOMBAT_MAIN: + game.getTurn().setPhase(new EndPhase()); + game.getPhase().setStep(new EndStep()); + break; + case END_TURN: + game.getPhase().setStep(new CleanupStep()); + break; + case CLEANUP: + game.getPhase().getStep().beginStep(game, activePlayerId); + if (!game.checkStateAndTriggered() && !game.isGameOver()) { + game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); + game.getTurn().setPhase(new BeginningPhase()); + game.getPhase().setStep(new UntapStep()); + } + } + if (!game.getStep().skipStep(game, game.getActivePlayerId())) { + if (game.getTurn().getStepType() == PhaseStep.DECLARE_ATTACKERS) { + game.fireEvent(new GameEvent(GameEvent.EventType.DECLARE_ATTACKERS_STEP_PRE, null, null, activePlayerId)); + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, activePlayerId, activePlayerId))) { + for (Combat engagement: ((SimulatedPlayer2)game.getPlayer(activePlayerId)).addAttackers(game)) { + Game sim = game.copy(); + UUID defenderId = game.getOpponents(playerId).iterator().next(); + for (CombatGroup group: engagement.getGroups()) { + for (UUID attackerId: group.getAttackers()) { + sim.getPlayer(activePlayerId).declareAttacker(attackerId, defenderId, sim); + } + } + sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_ATTACKERS, playerId, playerId)); + SimulationNode2 newNode = new SimulationNode2(sim, node.getDepth()-1, activePlayerId); + logger.info("simulating -- node #:" + SimulationNode2.getCount() + " declare attakers"); + newNode.setCombat(sim.getCombat()); + node.children.add(newNode); + } + } + } + else if (game.getTurn().getStepType() == PhaseStep.DECLARE_BLOCKERS) { + game.fireEvent(new GameEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_PRE, null, null, activePlayerId)); + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_BLOCKERS, activePlayerId, activePlayerId))) { + for (UUID defenderId: game.getCombat().getDefenders()) { + //check if defender is being attacked + if (game.getCombat().isAttacked(defenderId, game)) { + for (Combat engagement: ((SimulatedPlayer2)game.getPlayer(defenderId)).addBlockers(game)) { + Game sim = game.copy(); + for (CombatGroup group: engagement.getGroups()) { + for (UUID blockerId: group.getBlockers()) { + group.addBlocker(blockerId, defenderId, sim); + } + } + sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, playerId, playerId)); + SimulationNode2 newNode = new SimulationNode2(sim, node.getDepth()-1, defenderId); + logger.info("simulating -- node #:" + SimulationNode2.getCount() + " declare blockers"); + newNode.setCombat(sim.getCombat()); + node.children.add(newNode); + } + } + } + } + } + else { + game.getStep().beginStep(game, activePlayerId); + } + if (game.getStep().getHasPriority()) + break; + } + else { + skip = true; + } + } + game.checkStateAndTriggered(); + } + + @Override + public void selectAttackers(Game game) { + logger.info("selectAttackers"); + if (combat != null) { + UUID opponentId = game.getCombat().getDefenders().iterator().next(); + for (UUID attackerId: combat.getAttackers()) { + logger.info("declare attacker: " + game.getCard(attackerId).getName()); + this.declareAttacker(attackerId, opponentId, game); + } + } + } + + @Override + public void selectBlockers(Game game) { + logger.info("selectBlockers"); + if (combat != null && combat.getGroups().size() > 0) { + List groups = game.getCombat().getGroups(); + for (int i = 0; i < groups.size(); i++) { + if (i < combat.getGroups().size()) { + for (UUID blockerId: combat.getGroups().get(i).getBlockers()) { + this.declareBlocker(blockerId, groups.get(i).getAttackers().get(0), game); + } + } + } + } + } + + /** + * Copies game and replaces all players in copy with simulated players + * + * @param game + * @return a new game object with simulated players + */ + protected Game createSimulation(Game game) { + Game sim = game.copy(); + + for (Player copyPlayer: sim.getState().getPlayers().values()) { + Player origPlayer = game.getState().getPlayers().get(copyPlayer.getId()); + SimulatedPlayer2 newPlayer = new SimulatedPlayer2(copyPlayer.getId(), copyPlayer.getId().equals(playerId)); + newPlayer.restore(origPlayer); + sim.getState().getPlayers().put(copyPlayer.getId(), newPlayer); + } + return sim; + } + +} diff --git a/Mage.Tests/config/config.xml b/Mage.Tests/config/config.xml index 763e09187a..7af45388f8 100644 --- a/Mage.Tests/config/config.xml +++ b/Mage.Tests/config/config.xml @@ -4,7 +4,7 @@ - + diff --git a/Mage.Tests/plugins/mage-player-ai-ma.jar b/Mage.Tests/plugins/mage-player-ai-ma.jar index 8792f2746e5327201c3dd440fa01359f1760c57b..6e895cae6c74e391b082288ff67dcbecb7e67917 100644 GIT binary patch delta 14107 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
Q6 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 diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java index e8fed69414..157e8001ca 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/PlayGameTest.java @@ -1,6 +1,8 @@ package org.mage.test.serverside; import mage.Constants; +import mage.cards.Card; +import mage.cards.ExpansionSet; import mage.cards.decks.Deck; import mage.game.Game; import mage.game.GameException; @@ -11,42 +13,137 @@ import mage.sets.Sets; import org.junit.Test; import org.mage.test.serverside.base.MageTestBase; +import java.io.File; import java.io.FileNotFoundException; +import java.util.*; +import java.util.regex.Matcher; /** * @author ayratn */ public class PlayGameTest extends MageTestBase { + private List handCardsA = new ArrayList(); + private List handCardsB = new ArrayList(); + private List battlefieldCardsA = new ArrayList(); + private List battlefieldCardsB = new ArrayList(); + private List graveyardCardsA = new ArrayList(); + private List graveyardCardsB = new ArrayList(); + private List libraryCardsA = new ArrayList(); + private List libraryCardsB = new ArrayList(); + + private Map commandsA = new HashMap(); + private Map commandsB = new HashMap(); + @Test public void playOneGame() throws GameException, FileNotFoundException, IllegalArgumentException { Game game = new TwoPlayerDuel(Constants.MultiplayerAttackOption.LEFT, Constants.RangeOfInfluence.ALL); - Player player = createPlayer("computer1", "Computer - mad"); + Player computerA = createPlayer("ComputerA", "Computer - mad"); Deck deck = Deck.load(Sets.loadDeck("RB Aggro.dck")); if (deck.getCards().size() < 40) { throw new IllegalArgumentException("Couldn't load deck, deck side=" + deck.getCards().size()); } - game.addPlayer(player, deck); - game.loadCards(deck.getCards(), player.getId()); + game.addPlayer(computerA, deck); + game.loadCards(deck.getCards(), computerA.getId()); - Player player2 = createPlayer("computer2", "Computer - mad"); + Player computerB = createPlayer("ComputerB", "Computer - mad"); Deck deck2 = Deck.load(Sets.loadDeck("RB Aggro.dck")); if (deck2.getCards().size() < 40) { throw new IllegalArgumentException("Couldn't load deck, deck side=" + deck2.getCards().size()); } - game.addPlayer(player2, deck2); - game.loadCards(deck2.getCards(), player2.getId()); + game.addPlayer(computerB, deck2); + game.loadCards(deck2.getCards(), computerB.getId()); + + parseScenario("scenario1.txt"); + game.cheat(computerA.getId(), commandsA); + game.cheat(computerA.getId(), libraryCardsA, handCardsA, battlefieldCardsA, graveyardCardsA); + game.cheat(computerB.getId(), commandsB); + game.cheat(computerB.getId(), libraryCardsB, handCardsB, battlefieldCardsB, graveyardCardsB); long t1 = System.nanoTime(); - game.start(player.getId()); + game.start(computerA.getId(), true); long t2 = System.nanoTime(); logger.info("Winner: " + game.getWinner()); logger.info("Time: " + (t2 - t1) / 1000000 + " ms"); } + private void addCard(List cards, String name, int count) { + for (int i = 0; i < count; i++) { + Card card = Sets.findCard(name, true); + if (card == null) { + throw new IllegalArgumentException("Couldn't find a card for test: " + name); + } + cards.add(card); + } + } + + private void parseScenario(String filename) throws FileNotFoundException { + File f = new File(filename); + Scanner scanner = new Scanner(f); + try { + while (scanner.hasNextLine()) { + String line = scanner.nextLine().trim(); + if (line.startsWith("#")) continue; + Matcher m = pattern.matcher(line); + if (m.matches()) { + + String zone = m.group(1); + String nickname = m.group(2); + + if (nickname.equals("ComputerA") || nickname.equals("ComputerB")) { + List cards; + Constants.Zone gameZone; + if ("hand".equalsIgnoreCase(zone)) { + gameZone = Constants.Zone.HAND; + cards = nickname.equals("ComputerA") ? handCardsA : handCardsB; + } else if ("battlefield".equalsIgnoreCase(zone)) { + gameZone = Constants.Zone.BATTLEFIELD; + cards = nickname.equals("ComputerA") ? battlefieldCardsA : battlefieldCardsB; + } else if ("graveyard".equalsIgnoreCase(zone)) { + gameZone = Constants.Zone.GRAVEYARD; + cards = nickname.equals("ComputerA") ? graveyardCardsA : graveyardCardsB; + } else if ("library".equalsIgnoreCase(zone)) { + gameZone = Constants.Zone.LIBRARY; + cards = nickname.equals("ComputerA") ? libraryCardsA : libraryCardsB; + } else { + continue; // go parse next line + } + + String cardName = m.group(3); + Integer amount = Integer.parseInt(m.group(4)); + + if (cardName.equals("clear")) { + if (nickname.equals("ComputerA")) { + commandsA.put(gameZone, "clear"); + } else { + commandsB.put(gameZone, "clear"); + } + } else { + for (int i = 0; i < amount; i++) { + Card card = Sets.findCard(cardName, true); + if (card != null) { + cards.add(card); + } else { + logger.severe("Couldn't find a card: " + cardName); + logger.severe("line: " + line); + } + } + } + } else { + logger.warning("Unknown player: " + nickname); + } + } else { + logger.warning("Init string wasn't parsed: " + line); + } + } + } finally { + scanner.close(); + } + } + private Player createPlayer(String name, String playerType) { return PlayerFactory.getInstance().createPlayer(playerType, name, Constants.RangeOfInfluence.ALL); } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java index 56e319cb6c..c9a0700086 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestBase.java @@ -18,6 +18,7 @@ import java.io.File; import java.io.FilenameFilter; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; /** * @author ayratn @@ -29,6 +30,8 @@ public class MageTestBase { private final static String pluginFolder = "plugins"; + protected Pattern pattern = Pattern.compile("([a-zA-Z]*):([\\w]*):([a-zA-Z ,\\-.!'\\d]*):([\\d]*)"); + @BeforeClass public static void init() { logger.info("Starting MAGE tests"); diff --git a/Mage/src/mage/game/Game.java b/Mage/src/mage/game/Game.java index 50550e934d..b0b18e30d3 100644 --- a/Mage/src/mage/game/Game.java +++ b/Mage/src/mage/game/Game.java @@ -28,15 +28,14 @@ package mage.game; +import mage.Constants; import mage.game.match.MatchType; import mage.cards.Card; import mage.game.stack.SpellStack; import mage.MageObject; import java.io.Serializable; -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; + import mage.Constants.MultiplayerAttackOption; import mage.Constants.RangeOfInfluence; import mage.Constants.Zone; @@ -61,6 +60,7 @@ import mage.game.permanent.Permanent; import mage.game.turn.Phase; import mage.game.turn.Step; import mage.game.turn.Turn; +import mage.players.Library; import mage.players.Player; import mage.players.PlayerList; import mage.players.Players; @@ -136,8 +136,9 @@ public interface Game extends MageItem, Serializable { public boolean replaceEvent(GameEvent event); //game play methods -// public void init(UUID choosingPlayerId); + //public void init(UUID choosingPlayerId); public void start(UUID choosingPlayerId); + public void start(UUID choosingPlayerId, boolean testMode); public void end(); public void mulligan(UUID playerId); public void quit(UUID playerId); @@ -156,4 +157,7 @@ public interface Game extends MageItem, Serializable { public void restoreState(); public void removeLastBookmark(); + // game cheats (for tests only) + public void cheat(UUID ownerId, Map commands); + public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard); } diff --git a/Mage/src/mage/game/GameImpl.java b/Mage/src/mage/game/GameImpl.java index c841c089ce..09a801fe5c 100644 --- a/Mage/src/mage/game/GameImpl.java +++ b/Mage/src/mage/game/GameImpl.java @@ -47,11 +47,13 @@ import mage.game.events.*; import mage.game.events.TableEvent.EventType; import mage.game.permanent.Battlefield; import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentCard; import mage.game.stack.SpellStack; import mage.game.stack.StackObject; import mage.game.turn.Phase; import mage.game.turn.Step; import mage.game.turn.Turn; +import mage.players.Library; import mage.players.Player; import mage.players.PlayerList; import mage.players.Players; @@ -289,7 +291,12 @@ public abstract class GameImpl> implements Game, Serializa @Override public void start(UUID choosingPlayerId) { - init(choosingPlayerId); + start(choosingPlayerId, false); + } + + @Override + public void start(UUID choosingPlayerId, boolean testMode) { + init(choosingPlayerId, testMode); PlayerList players = state.getPlayerList(startingPlayerId); Player player = getPlayer(players.get()); while (!isGameOver()) { @@ -311,8 +318,12 @@ public abstract class GameImpl> implements Game, Serializa } protected void init(UUID choosingPlayerId) { + init(choosingPlayerId, false); + } + + protected void init(UUID choosingPlayerId, boolean testMode) { for (Player player: state.getPlayers().values()) { - player.init(this); + player.init(this, testMode); } fireInformEvent("game has started"); saveState(); @@ -347,7 +358,9 @@ public abstract class GameImpl> implements Game, Serializa for (UUID playerId: state.getPlayerList(startingPlayerId)) { Player player = getPlayer(playerId); player.setLife(this.getLife(), this); - player.drawCards(7, this); + if (!testMode) { + player.drawCards(7, this); + } } //20091005 - 103.4 @@ -918,4 +931,100 @@ public abstract class GameImpl> implements Game, Serializa public void resetLKI() { lki.clear(); } + + public void cheat(UUID ownerId, Map commands) { + if (commands != null) { + Player player = getPlayer(ownerId); + if (player != null) { + for (Map.Entry command : commands.entrySet()) { + switch (command.getKey()) { + case HAND: + if (command.getValue().equals("clear")) { + removeCards(player.getHand()); + } + break; + case LIBRARY: + if (command.getValue().equals("clear")) { + for (UUID card : player.getLibrary().getCardList()) { + gameCards.remove(card); + } + player.getLibrary().clear(); + } + break; + } + } + } + } + } + + private void removeCards(Cards cards) { + for (UUID card : cards) { + gameCards.remove(card); + } + cards.clear(); + } + + public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard) { + Player player = getPlayer(ownerId); + if (player != null) { + loadCards(ownerId, library); + loadCards(ownerId, hand); + loadCards(ownerId, battlefield); + loadCards(ownerId, graveyard); + + for (Card card : library) { + setZone(card.getId(), Zone.LIBRARY); + player.getLibrary().putOnTop(card, this); + } + for (Card card : hand) { + setZone(card.getId(), Zone.HAND); + player.getHand().add(card); + } + for (Card card : graveyard) { + setZone(card.getId(), Zone.GRAVEYARD); + player.getGraveyard().add(card); + } + List permanents = new ArrayList(); + for (Card card : battlefield) { + card.setOwnerId(ownerId); + PermanentCard permanent = new PermanentCard(card, ownerId); + getBattlefield().addPermanent(permanent); + } + applyEffects(); + } + } + + private void loadCards(UUID ownerId, List cards) { + if (cards == null) { + return; + } + Set set = new HashSet(); + for (Card card : cards) { + set.add(card); + } + loadCards(set, ownerId); + } + + public void replaceLibrary(List cardsDownToTop, UUID ownerId) { + Player player = getPlayer(ownerId); + if (player != null) { + for (UUID card : player.getLibrary().getCardList()) { + gameCards.remove(card); + } + player.getLibrary().clear(); + Set cards = new HashSet(); + for (Card card : cardsDownToTop) { + cards.add(card); + } + loadCards(cards, ownerId); + + for (Card card : cards) { + player.getLibrary().putOnTop(card, this); + } + } + } + + public void clearGraveyard(UUID playerId) { + + } } diff --git a/Mage/src/mage/players/Player.java b/Mage/src/mage/players/Player.java index 9749716a47..a3fe979ca7 100644 --- a/Mage/src/mage/players/Player.java +++ b/Mage/src/mage/players/Player.java @@ -97,6 +97,7 @@ public interface Player extends MageItem, Copyable { public Set getInRange(); public void init(Game game); + public void init(Game game, boolean testMode); public void useDeck(Deck deck, Game game); public void reset(); public void shuffleLibrary(Game game); diff --git a/Mage/src/mage/players/PlayerImpl.java b/Mage/src/mage/players/PlayerImpl.java index 48d1152861..f1b12a0adb 100644 --- a/Mage/src/mage/players/PlayerImpl.java +++ b/Mage/src/mage/players/PlayerImpl.java @@ -152,9 +152,16 @@ public abstract class PlayerImpl> implements Player, Ser @Override public void init(Game game) { + init(game, false); + } + + @Override + public void init(Game game, boolean testMode) { this.abort = false; - this.hand.clear(); - this.graveyard.clear(); + if (!testMode) { + this.hand.clear(); + this.graveyard.clear(); + } this.abilities.clear(); this.wins = false; this.loses = false;