diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml new file mode 100644 index 0000000000..da32006920 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + + org.mage + Mage-Server-Plugins + 0.8.1 + + + Mage-Player-AI-MCTS + jar + Mage Player AI MCTS + + + + log4j + log4j + 1.2.14 + jar + + + ${project.groupId} + Mage + ${project.version} + + + ${project.groupId} + Mage-Player-AI + ${project.version} + + + + + src + + + org.apache.maven.plugins + maven-compiler-plugin + 2.0.2 + + 1.6 + 1.6 + + + + maven-resources-plugin + + UTF-8 + + + + + + mage-player-aimcts + + + + + diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java new file mode 100644 index 0000000000..8700aa8d31 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java @@ -0,0 +1,302 @@ +/* + * Copyright 2011 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 java.util.List; +import java.util.UUID; +import mage.Constants.RangeOfInfluence; +import mage.abilities.Ability; +import mage.abilities.ActivatedAbility; +import mage.game.Game; +import mage.game.combat.Combat; +import mage.game.combat.CombatGroup; +import mage.game.permanent.Permanent; +import mage.players.Player; +import org.apache.log4j.Logger; + +/** + * + * @author BetaSteward_at_googlemail.com + */ +public class ComputerPlayerMCTS extends ComputerPlayer implements Player { + + protected transient MCTSNode root; + protected int thinkTime; + private final static transient Logger logger = Logger.getLogger(ComputerPlayerMCTS.class); + + public ComputerPlayerMCTS(String name, RangeOfInfluence range, int skill) { + super(name, range); + human = false; + thinkTime = skill; + } + + protected ComputerPlayerMCTS(UUID id) { + super(id); + } + + public ComputerPlayerMCTS(final ComputerPlayerMCTS player) { + super(player); + } + + @Override + public ComputerPlayerMCTS copy() { + return new ComputerPlayerMCTS(this); + } + + @Override + public void priority(Game game) { + getNextAction(game); + Ability ability = root.getAction(); + activateAbility((ActivatedAbility)ability, game); + } + + protected void calculateActions(Game game) { + if (root == null) { + Game sim = createMCTSGame(game); + MCTSPlayer player = (MCTSPlayer) sim.getPlayer(playerId); + player.setNextAction(MCTSPlayer.NextAction.PRIORITY); + root = new MCTSNode(sim); + } + applyMCTS(); + root = root.bestChild(); + root.emancipate(); + } + + protected void getNextAction(Game game) { + if (root != null) { + root = root.getMatchingState(game.getState().getValue().hashCode()); + if (root != null) + root.emancipate(); + } + calculateActions(game); + } + +// @Override +// public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map options) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean choose(Outcome outcome, Cards cards, TargetCard target, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean chooseMulligan(Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean chooseUse(Outcome outcome, String message, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public boolean choose(Outcome outcome, Choice choice, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } + +// @Override +// public boolean playMana(ManaCost unpaid, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } + +// @Override +// public boolean playXMana(VariableManaCost cost, ManaCosts costs, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public int chooseEffect(List rEffects, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public TriggeredAbility chooseTriggeredAbility(TriggeredAbilities abilities, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public Mode chooseMode(Modes modes, Ability source, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } + + @Override + public void selectAttackers(Game game) { + Game sim = createMCTSGame(game); + MCTSPlayer player = (MCTSPlayer) sim.getPlayer(playerId); + player.setNextAction(MCTSPlayer.NextAction.SELECT_ATTACKERS); + root = new MCTSNode(sim); + applyMCTS(); + Combat combat = root.bestChild().getCombat(); + UUID opponentId = game.getCombat().getDefenders().iterator().next(); + for (UUID attackerId: combat.getAttackers()) { + this.declareAttacker(attackerId, opponentId, game); + } + } + + @Override + public void selectBlockers(Game game) { + Game sim = createMCTSGame(game); + MCTSPlayer player = (MCTSPlayer) sim.getPlayer(playerId); + player.setNextAction(MCTSPlayer.NextAction.SELECT_BLOCKERS); + root = new MCTSNode(sim); + applyMCTS(); + Combat combat = root.bestChild().getCombat(); + 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); + } + } + } + } + +// @Override +// public UUID chooseAttackerOrder(List attacker, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public UUID chooseBlockerOrder(List blockers, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public void assignDamage(int damage, List targets, String singleTargetName, UUID sourceId, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public int getAmount(int min, int max, String message, Game game) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public void sideboard(Match match, Deck deck) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public void construct(Tournament tournament, Deck deck) { +// throw new UnsupportedOperationException("Not supported yet."); +// } +// +// @Override +// public void pickCard(List cards, Deck deck, Draft draft) { +// throw new UnsupportedOperationException("Not supported yet."); +// } + + protected void applyMCTS() { + long startTime = System.nanoTime(); + long endTime = startTime + (thinkTime * 1000000000l); + MCTSNode current; + + if (root.getNumChildren() == 1) + //there is only one possible action + return; + + logger.info("applyMCTS - Thinking for " + (endTime - startTime)/1000000000.0 + "s"); + while (true) { + long currentTime = System.nanoTime(); + logger.info("Remaining time: " + (endTime - currentTime)/1000000000.0 + "s"); + if (currentTime > endTime) + break; + current = root; + + // Selection + while (!current.isLeaf()) { + current = current.select(); + } + + int result; + if (!current.isTerminal()) { + // Expansion + current.expand(); + + if (current == root && current.getNumChildren() == 1) + //there is only one possible action + return; + + // Simulation + current = current.select(); + result = current.simulate(this.playerId); + } + else { + result = current.isWinner(this.playerId)?1:0; + } + // Backpropagation + current.backpropagate(result); + } + logger.info("Created " + root.getNodeCount() + " nodes"); + return; + } + + /** + * Copies game and replaces all players in copy with mcts players + * Shuffles each players library so that there is no knowledge of its order + * + * @param game + * @return a new game object with simulated players + */ + protected Game createMCTSGame(Game game) { + Game mcts = game.copy(); + + for (Player copyPlayer: mcts.getState().getPlayers().values()) { + Player origPlayer = game.getState().getPlayers().get(copyPlayer.getId()); + MCTSPlayer newPlayer = new MCTSPlayer(copyPlayer.getId()); + newPlayer.restore(origPlayer); + newPlayer.shuffleLibrary(mcts); + mcts.getState().getPlayers().put(copyPlayer.getId(), newPlayer); + } + mcts.setSimulation(true); + return mcts; + } + +} diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java new file mode 100644 index 0000000000..82dc47d2dc --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java @@ -0,0 +1,284 @@ +/* + * Copyright 2011 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 java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import mage.Constants.PhaseStep; +import mage.abilities.Ability; +import mage.abilities.ActivatedAbility; +import mage.game.Game; +import mage.game.GameState; +import mage.game.combat.Combat; +import mage.game.combat.CombatGroup; +import mage.game.turn.Step.StepPart; +import mage.players.Player; +import org.apache.log4j.Logger; + +/** + * + * @author BetaSteward_at_googlemail.com + */ +public class MCTSNode { + + private static final double selectionCoefficient = 1; + private final static transient Logger logger = Logger.getLogger(MCTSNode.class); + + private int visits = 0; + private int wins = 0; + private MCTSNode parent; + private List children = new ArrayList(); + private Ability action; + private Combat combat; + private Game game; + private int stateValue; + + private static int nodeCount; + + public MCTSNode(Game game) { + this.game = game; + this.stateValue = game.getState().getValue().hashCode(); + nodeCount = 1; + } + + protected MCTSNode(MCTSNode parent, Game game, int state, Ability action) { + this.game = game; + this.stateValue = state; + this.parent = parent; + this.action = action; + nodeCount++; + } + + protected MCTSNode(MCTSNode parent, Game game, int state, Combat combat) { + this.game = game; + this.stateValue = state; + this.parent = parent; + this.combat = combat; + nodeCount++; + } + + public MCTSNode select() { + double bestValue = Double.NEGATIVE_INFINITY; + MCTSNode bestChild = null; +// logger.info("start select"); + if (children.size() == 1) { + return children.get(0); + } + for (MCTSNode node: children) { + double uct; + if (node.visits > 0) + uct = (node.wins / (node.visits + 1.0)) + (selectionCoefficient * Math.sqrt(Math.log(visits + 1.0) / (node.visits + 1.0))); + else + // ensure that a random unvisited node is played first + uct = 10000 + 1000 * Math.random(); +// logger.info("uct: " + uct); + if (uct > bestValue) { + bestChild = node; + bestValue = uct; + } + } +// logger.info("stop select"); + return bestChild; + } + + public void expand() { + MCTSPlayer player; + if (game.getStep().getStepPart() == StepPart.PRIORITY) + player = (MCTSPlayer) game.getPlayer(game.getPriorityPlayerId()); + else { + if (game.getStep().getType() == PhaseStep.DECLARE_BLOCKERS) + player = (MCTSPlayer) game.getPlayer(game.getCombat().getDefenders().iterator().next()); + else + player = (MCTSPlayer) game.getPlayer(game.getActivePlayerId()); + } + if (player.getNextAction() == null) { + logger.fatal("next action is null"); + } + switch (player.getNextAction()) { + case PRIORITY: + logger.info("Priority for player:" + player.getName() + " turn: " + game.getTurnNum() + " phase: " + game.getPhase().getType() + " step: " + game.getStep().getType()); + List abilities = player.getPlayableOptions(game); + for (Ability ability: abilities) { + Game sim = game.copy(); + int simState = sim.getState().getValue().hashCode(); + logger.info("expand " + ability.toString()); + MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId()); + simPlayer.activateAbility((ActivatedAbility)ability, sim); + sim.resume(); + children.add(new MCTSNode(this, sim, simState, ability)); + } + break; + case SELECT_ATTACKERS: + logger.info("Select attackers:" + player.getName()); + List> attacks = player.getAttacks(game); + UUID defenderId = game.getOpponents(player.getId()).iterator().next(); + for (List attack: attacks) { + Game sim = game.copy(); + int simState = sim.getState().getValue().hashCode(); + MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId()); + for (UUID attackerId: attack) { + simPlayer.declareAttacker(attackerId, defenderId, sim); + } + sim.resume(); + children.add(new MCTSNode(this, sim, simState, sim.getCombat())); + } + break; + case SELECT_BLOCKERS: + logger.info("Select blockers:" + player.getName()); + List>> blocks = player.getBlocks(game); + for (List> block: blocks) { + Game sim = game.copy(); + int simState = sim.getState().getValue().hashCode(); + MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId()); + List groups = sim.getCombat().getGroups(); + for (int i = 0; i < groups.size(); i++) { + if (i < block.size()) { + for (UUID blockerId: block.get(i)) { + simPlayer.declareBlocker(blockerId, groups.get(i).getAttackers().get(0), sim); + } + } + } + sim.resume(); + children.add(new MCTSNode(this, sim, simState, sim.getCombat())); + } + break; + } + } + + public int simulate(UUID playerId) { + long startTime = System.nanoTime(); + Game sim = createSimulation(game); + sim.resume(); + long duration = System.nanoTime() - startTime; + int retVal = 0; + for (Player simPlayer: sim.getPlayers().values()) { + logger.info(simPlayer.getName() + " calculated " + ((SimulatedPlayerMCTS)simPlayer).getActionCount() + " actions in " + duration/1000000000.0 + "s"); + if (simPlayer.getId().equals(playerId) && simPlayer.hasWon()) { + logger.info("AI won the simulation"); + retVal = 1; + } + } + return retVal; + } + + public void backpropagate(int result) { + if (result == 1) + wins++; + visits++; + if (parent != null) + parent.backpropagate(result); + } + + public boolean isLeaf() { + return children.isEmpty(); + } + + public MCTSNode bestChild() { + double bestCount = -1; + MCTSNode bestChild = null; + for (MCTSNode node: children) { + if (node.visits > bestCount) { + bestChild = node; + bestCount = node.visits; + } + } + return bestChild; + } + + public void emancipate() { + this.parent = null; + } + + public Ability getAction() { + return action; + } + + public int getNumChildren() { + return children.size(); + } + + public MCTSNode getParent() { + return parent; + } + + public Combat getCombat() { + return combat; + } + + public int getNodeCount() { + return nodeCount; + } + + /** + * Copies game and replaces all players in copy with simulated players + * Shuffles each players library so that there is no knowledge of its order + * + * @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()).copy(); + SimulatedPlayerMCTS newPlayer = new SimulatedPlayerMCTS(copyPlayer.getId(), true); + newPlayer.restore(origPlayer); + newPlayer.shuffleLibrary(sim); + sim.getState().getPlayers().put(copyPlayer.getId(), newPlayer); + } + sim.setSimulation(true); + return sim; + } + + public boolean isTerminal() { + return game.isGameOver(); + } + + public boolean isWinner(UUID playerId) { + Player player = game.getPlayer(playerId); + if (player != null && player.hasWon()) + return true; + return false; + } + + public MCTSNode getMatchingState(int state) { + for (MCTSNode node: children) { +// logger.info(state); +// logger.info(node.stateValue); + if (node.stateValue == state) { + return node; + } + MCTSNode match = node.getMatchingState(state); + if (match != null) + return node; + } + return null; + } + +} diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSPlayer.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSPlayer.java new file mode 100644 index 0000000000..ef9f94e245 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSPlayer.java @@ -0,0 +1,304 @@ +/* + * Copyright 2011 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.PassAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.game.Game; +import mage.game.combat.Combat; +import mage.game.permanent.Permanent; +import org.apache.log4j.Logger; + +/** + * + * @author BetaSteward_at_googlemail.com + */ +public class MCTSPlayer extends ComputerPlayer { + + private final static transient Logger logger = Logger.getLogger(MCTSPlayer.class); + + protected PassAbility pass = new PassAbility(); + + private NextAction nextAction; + + public enum NextAction { + PRIORITY, SELECT_ATTACKERS, SELECT_BLOCKERS; + } + + public MCTSPlayer(UUID id) { + super(id); + this.pass.setControllerId(id); + } + + public MCTSPlayer(final MCTSPlayer player) { + super(player); + this.pass = player.pass.copy(); + this.nextAction = player.nextAction; + } + + @Override + public MCTSPlayer copy() { + return new MCTSPlayer(this); + } + + protected List getPlayableAbilities(Game game) { + List playables = getPlayable(game, true); + playables.add(pass); + return playables; + } + + public List getPlayableOptions(Game game) { + List all = new ArrayList(); + List playables = getPlayableAbilities(game); + for (Ability ability: playables) { + List options = game.getPlayer(playerId).getPlayableOptions(ability, game); + if (options.isEmpty()) { + if (ability.getManaCosts().getVariableCosts().size() > 0) { + simulateVariableCosts(ability, all, game); + } + else { + all.add(ability); + } + } + else { + for (Ability option: options) { + if (ability.getManaCosts().getVariableCosts().size() > 0) { + simulateVariableCosts(option, all, game); + } + else { + all.add(option); + } + } + } + } + return all; + } + + protected void simulateVariableCosts(Ability ability, List options, Game game) { + int numAvailable = getAvailableManaProducers(game).size() - ability.getManaCosts().convertedManaCost(); + int start = 0; + if (!(ability instanceof SpellAbility)) { + //only use x=0 on spell abilities + if (numAvailable == 0) + return; + else + start = 1; + } + for (int i = start; i < numAvailable; i++) { + Ability newAbility = ability.copy(); + newAbility.addManaCost(new GenericManaCost(i)); + options.add(newAbility); + } + } + + public List> getAttacks(Game game) { + List> engagements = new ArrayList>(); + List attackersList = super.getAvailableAttackers(game); + //use binary digits to calculate powerset of attackers + int powerElements = (int) Math.pow(2, attackersList.size()); + StringBuilder binary = new StringBuilder(); + for (int i = powerElements - 1; i >= 0; i--) { + binary.setLength(0); + binary.append(Integer.toBinaryString(i)); + while (binary.length() < attackersList.size()) { + binary.insert(0, "0"); + } + List engagement = new ArrayList(); + for (int j = 0; j < attackersList.size(); j++) { + if (binary.charAt(j) == '1') + engagement.add(attackersList.get(j).getId()); + } + engagements.add(engagement); + } + return engagements; + } + + public List>> getBlocks(Game game) { + List>> engagements = new ArrayList>>(); + int numGroups = game.getCombat().getGroups().size(); + if (numGroups == 0) return engagements; + + //add a node with no blockers + List> engagement = new ArrayList>(); + for (int i = 0; i < numGroups; i++) { + engagement.add(new ArrayList()); + } + engagements.add(engagement); + + List blockers = getAvailableBlockers(game); + addBlocker(game, engagement, blockers, engagements); + + return engagements; + } + + private List> copyEngagement(List> engagement) { + List> newEngagement = new ArrayList>(); + for (List group: engagement) { + newEngagement.add(new ArrayList(group)); + } + return newEngagement; + } + + protected void addBlocker(Game game, List> engagement, List blockers, List>> engagements) { + if (blockers.isEmpty()) + return; + int numGroups = game.getCombat().getGroups().size(); + //try to block each attacker with each potential blocker + Permanent blocker = blockers.get(0); +// if (logger.isDebugEnabled()) +// logger.debug("simulating -- block:" + blocker); + List remaining = remove(blockers, blocker); + for (int i = 0; i < numGroups; i++) { + if (game.getCombat().getGroups().get(i).canBlock(blocker, game)) { + List>newEngagement = copyEngagement(engagement); + newEngagement.get(i).add(blocker.getId()); + engagements.add(newEngagement); +// logger.debug("simulating -- found redundant block combination"); + addBlocker(game, newEngagement, remaining, engagements); // and recurse minus the used blocker + } + } + addBlocker(game, engagement, remaining, engagements); + } + + public NextAction getNextAction() { + return nextAction; + } + + public void setNextAction(NextAction action) { + this.nextAction = action; + } + + @Override + public void priority(Game game) { +// logger.info("Paused for Priority for player:" + getName()); + game.pause(); + nextAction = NextAction.PRIORITY; + } + +// @Override +// public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game) { +// game.end(); +// } +// +// @Override +// public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map options) { +// game.end(); +// } +// +// @Override +// public boolean choose(Outcome outcome, Cards cards, TargetCard target, Game game) { +// game.end(); +// } +// +// @Override +// public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { +// game.end(); +// } +// +// @Override +// public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { +// game.end(); +// } +// +// @Override +// public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { +// game.end(); +// } +// +// @Override +// public boolean chooseMulligan(Game game) { +// game.end(); +// } +// +// @Override +// public boolean chooseUse(Outcome outcome, String message, Game game) { +// game.end(); +// } +// +// @Override +// public boolean choose(Outcome outcome, Choice choice, Game game) { +// game.end(); +// } +// +// @Override +// public int chooseEffect(List rEffects, Game game) { +// game.end(); +// } +// +// @Override +// public TriggeredAbility chooseTriggeredAbility(TriggeredAbilities abilities, Game game) { +// game.end(); +// } +// +// @Override +// public Mode chooseMode(Modes modes, Ability source, Game game) { +// game.end(); +// } + + @Override + public void selectAttackers(Game game) { +// logger.info("Paused for select attackers for player:" + getName()); + game.pause(); + nextAction = NextAction.SELECT_ATTACKERS; + } + + @Override + public void selectBlockers(Game game) { +// logger.info("Paused for select blockers for player:" + getName()); + game.pause(); + nextAction = NextAction.SELECT_BLOCKERS; + } + +// @Override +// public UUID chooseAttackerOrder(List attacker, Game game) { +// game.end(); +// } +// +// @Override +// public UUID chooseBlockerOrder(List blockers, Game game) { +// game.end(); +// } +// +// @Override +// public void assignDamage(int damage, List targets, String singleTargetName, UUID sourceId, Game game) { +// game.end(); +// } +// +// @Override +// public int getAmount(int min, int max, String message, Game game) { +// game.end(); +// } + +} diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java new file mode 100644 index 0000000000..77ba63399b --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java @@ -0,0 +1,418 @@ +/* + * Copyright 2011 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 java.io.Serializable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import mage.Constants.Outcome; +import mage.Constants.Zone; +import mage.abilities.Ability; +import mage.abilities.ActivatedAbility; +import mage.abilities.Mode; +import mage.abilities.Modes; +import mage.abilities.TriggeredAbilities; +import mage.abilities.TriggeredAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.VariableManaCost; +import mage.abilities.effects.ReplacementEffect; +import mage.abilities.mana.ManaAbility; +import mage.cards.Card; +import mage.cards.Cards; +import mage.choices.Choice; +import mage.game.Game; +import mage.game.combat.CombatGroup; +import mage.game.permanent.Permanent; +import mage.game.stack.StackAbility; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetAmount; +import mage.target.TargetCard; +import org.apache.log4j.Logger; + +/** + * + * plays randomly + * + * @author BetaSteward_at_googlemail.com + */ +public class SimulatedPlayerMCTS extends MCTSPlayer { + + private boolean isSimulatedPlayer; + private static Random rnd = new Random(); + private int actionCount = 0; + private final static transient Logger logger = Logger.getLogger(SimulatedPlayerMCTS.class); + + public SimulatedPlayerMCTS(UUID id, boolean isSimulatedPlayer) { + super(id); + this.isSimulatedPlayer = isSimulatedPlayer; + } + + public SimulatedPlayerMCTS(final SimulatedPlayerMCTS player) { + super(player); + this.isSimulatedPlayer = player.isSimulatedPlayer; + } + + @Override + public SimulatedPlayerMCTS copy() { + return new SimulatedPlayerMCTS(this); + } + + public boolean isSimulatedPlayer() { + return this.isSimulatedPlayer; + } + + public int getActionCount() { + return actionCount; + } + + @Override + public void priority(Game game) { +// logger.info("priority"); + while (true) { + List playables = getPlayableAbilities(game); + Ability ability; + if (playables.size() == 1) + ability = playables.get(0); + else + ability = playables.get(rnd.nextInt(playables.size())); + List options = getPlayableOptions(ability, game); + if (!options.isEmpty()) { + if (options.size() == 1) + ability = options.get(0); + else + ability = options.get(rnd.nextInt(options.size())); + } + if (ability.getManaCosts().getVariableCosts().size() > 0) { + int amount = getAvailableManaProducers(game).size() - ability.getManaCosts().convertedManaCost(); + if (amount > 0) + ability.addManaCost(new GenericManaCost(rnd.nextInt(amount))); + } +// logger.info("simulate " + ability.toString()); + activateAbility((ActivatedAbility) ability, game); + + actionCount++; + if (ability.isUsesStack()) + break; + } + } + + @Override + public boolean triggerAbility(TriggeredAbility source, Game game) { +// logger.info("trigger"); + if (source != null && source.canChooseTarget(game)) { + Ability ability; + List options = getPlayableOptions(source, game); + if (options.isEmpty()) { + ability = source; + } + else { + if (options.size() == 1) + ability = options.get(0); + else + ability = options.get(rnd.nextInt(options.size())); + } + if (ability.isUsesStack()) { + game.getStack().push(new StackAbility(ability, playerId)); + if (ability.activate(game, false)) { + actionCount++; + return true; + } + } else { + if (ability.activate(game, false)) { + ability.resolve(game); + actionCount++; + return true; + } + } + } + return false; + } + + @Override + public void selectAttackers(Game game) { + //useful only for two player games - will only attack first opponent +// logger.info("select attackers"); + UUID defenderId = game.getOpponents(playerId).iterator().next(); + List attackersList = super.getAvailableAttackers(game); + //use binary digits to calculate powerset of attackers + int powerElements = (int) Math.pow(2, attackersList.size()); + int value = rnd.nextInt(powerElements); + StringBuilder binary = new StringBuilder(); + binary.append(Integer.toBinaryString(value)); + while (binary.length() < attackersList.size()) { + binary.insert(0, "0"); //pad with zeros + } + for (int i = 0; i < attackersList.size(); i++) { + if (binary.charAt(i) == '1') + game.getCombat().declareAttacker(attackersList.get(i).getId(), defenderId, game); + } + actionCount++; + } + + @Override + public void selectBlockers(Game game) { +// logger.info("select blockers"); + int numGroups = game.getCombat().getGroups().size(); + if (numGroups == 0) return; + + List blockers = getAvailableBlockers(game); + for (Permanent blocker: blockers) { + int check = rnd.nextInt(numGroups + 1); + if (check < numGroups) { + CombatGroup group = game.getCombat().getGroups().get(check); + if (group.getAttackers().size() > 0) + this.declareBlocker(blocker.getId(), group.getAttackers().get(0), game); + } + } + actionCount++; + } + + @Override + public void abort() { + abort = true; + } + + protected boolean chooseRandom(Target target, Game game) { + Set possibleTargets = target.possibleTargets(playerId, game); + if (possibleTargets.isEmpty()) + return !target.isRequired(); + if (!target.isRequired()) { + if (rnd.nextInt(possibleTargets.size() + 1) == 0) { + return false; + } + } + if (possibleTargets.size() == 1) { + target.add(possibleTargets.iterator().next(), game); + return true; + } + Iterator it = possibleTargets.iterator(); + int targetNum = rnd.nextInt(possibleTargets.size()); + UUID targetId = it.next(); + for (int i = 0; i < targetNum; i++) { + targetId = it.next(); + } + target.add(targetId, game); + return true; + } + + protected boolean chooseRandomTarget(Target target, Ability source, Game game) { + Set possibleTargets = target.possibleTargets(source==null?null:source.getSourceId(), playerId, game); + if (possibleTargets.isEmpty()) + return false; + if (!target.isRequired()) { + if (rnd.nextInt(possibleTargets.size() + 1) == 0) { + return false; + } + } + if (possibleTargets.size() == 1) { + target.addTarget(possibleTargets.iterator().next(), source, game); + return true; + } + Iterator it = possibleTargets.iterator(); + int targetNum = rnd.nextInt(possibleTargets.size()); + UUID targetId = it.next(); + for (int i = 0; i < targetNum; i++) { + targetId = it.next(); + } + target.addTarget(targetId, source, game); + return true; + } + + @Override + public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game) { + return chooseRandom(target, game); + } + + @Override + public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map options) { + return chooseRandom(target, game); + } + + @Override + public boolean choose(Outcome outcome, Cards cards, TargetCard target, Game game) { + if (cards.isEmpty()) + return !target.isRequired(); + Card card = cards.getRandom(game); + target.add(card.getId(), game); + return true; + } + + @Override + public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { + return chooseRandomTarget(target, source, game); + } + + @Override + public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { + if (cards.isEmpty()) + return !target.isRequired(); + Card card = cards.getRandom(game); + target.addTarget(card.getId(), source, game); + return true; + } + + @Override + public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { + Set possibleTargets = target.possibleTargets(source==null?null:source.getSourceId(), playerId, game); + if (possibleTargets.isEmpty()) + return !target.isRequired(); + if (!target.isRequired()) { + if (rnd.nextInt(possibleTargets.size() + 1) == 0) { + return false; + } + } + if (possibleTargets.size() == 1) { + target.addTarget(possibleTargets.iterator().next(), target.getAmountRemaining(), source, game); + return true; + } + Iterator it = possibleTargets.iterator(); + int targetNum = rnd.nextInt(possibleTargets.size()); + UUID targetId = it.next(); + for (int i = 0; i < targetNum; i++) { + targetId = it.next(); + } + target.addTarget(targetId, rnd.nextInt(target.getAmountRemaining()) + 1, source, game); + return true; + } + + @Override + public boolean chooseMulligan(Game game) { + return rnd.nextBoolean(); + } + + @Override + public boolean chooseUse(Outcome outcome, String message, Game game) { + return rnd.nextBoolean(); + } + + @Override + public boolean choose(Outcome outcome, Choice choice, Game game) { + Iterator it = choice.getChoices().iterator(); + String sChoice = it.next(); + int choiceNum = rnd.nextInt(choice.getChoices().size()); + for (int i = 0; i < choiceNum; i++) { + sChoice = it.next(); + } + choice.setChoice(sChoice); + return true; + } + + @Override + public boolean playXMana(VariableManaCost cost, ManaCosts costs, Game game) { + for (Permanent perm: this.getAvailableManaProducers(game)) { + for (ManaAbility ability: perm.getAbilities().getAvailableManaAbilities(Zone.BATTLEFIELD, game)) { + if (rnd.nextBoolean()) + activateAbility(ability, game); + } + } + + // don't allow X=0 + if (getManaPool().count() == 0) { + return false; + } + + cost.setPaid(); + return true; + } + + @Override + public int chooseEffect(List rEffects, Game game) { + return rnd.nextInt(rEffects.size()); + } + + @Override + public TriggeredAbility chooseTriggeredAbility(TriggeredAbilities abilities, Game game) { + return abilities.get(rnd.nextInt(abilities.size())); + } + + @Override + public Mode chooseMode(Modes modes, Ability source, Game game) { + Iterator it = modes.values().iterator(); + Mode mode = it.next(); + if (modes.size() == 1) + return mode; + int modeNum = rnd.nextInt(modes.values().size()); + for (int i = 0; i < modeNum; i++) { + mode = it.next(); + } + return mode; + } + + @Override + public UUID chooseAttackerOrder(List attackers, Game game) { + return attackers.get(rnd.nextInt(attackers.size())).getId(); + } + + @Override + public UUID chooseBlockerOrder(List blockers, Game game) { + return blockers.get(rnd.nextInt(blockers.size())).getId(); + } + + @Override + public void assignDamage(int damage, List targets, String singleTargetName, UUID sourceId, Game game) { + int remainingDamage = damage; + UUID targetId; + int amount; + while (remainingDamage > 0) { + if (targets.size() == 1) { + targetId = targets.get(0); + amount = remainingDamage; + } + else { + targetId = targets.get(rnd.nextInt(targets.size())); + amount = rnd.nextInt(damage + 1); + } + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + permanent.damage(amount, sourceId, game, true, false); + remainingDamage -= amount; + } + else { + Player player = game.getPlayer(targetId); + if (player != null) { + player.damage(amount, sourceId, game, false, true); + remainingDamage -= amount; + } + } + targets.remove(targetId); + } + } + + @Override + public int getAmount(int min, int max, String message, Game game) { + return rnd.nextInt(max - min) + min; + } + +} diff --git a/Mage.Server.Plugins/pom.xml b/Mage.Server.Plugins/pom.xml index 0cd409d307..bfebc63055 100644 --- a/Mage.Server.Plugins/pom.xml +++ b/Mage.Server.Plugins/pom.xml @@ -22,6 +22,7 @@ Mage.Player.AI Mage.Player.AIMinimax Mage.Player.AI.MA + Mage.Player.AIMCTS Mage.Player.Human Mage.Tournament.BoosterDraft Mage.Tournament.Sealed diff --git a/Mage.Server/config/config.xml b/Mage.Server/config/config.xml index 0af9337b3a..3703d1642a 100644 --- a/Mage.Server/config/config.xml +++ b/Mage.Server/config/config.xml @@ -6,6 +6,7 @@ + diff --git a/Mage.Server/plugins/mage-player-aimcts.jar b/Mage.Server/plugins/mage-player-aimcts.jar new file mode 100644 index 0000000000..6d732f5da1 Binary files /dev/null and b/Mage.Server/plugins/mage-player-aimcts.jar differ