From 03d225443467acbc13cb1f09317f0293566fb20a Mon Sep 17 00:00:00 2001 From: VictorABoye Date: Tue, 24 Oct 2023 13:48:55 +0200 Subject: [PATCH 1/3] Added initial MCTS as BattleAI and added setting to choose AI --- src/main/java/dk/sdu/mmmi/Main.java | 4 +- .../sdu/mmmi/modulemon/BattleAI/BattleAI.java | 23 +- .../modulemon/BattleScene/BattleView.java | 6 +- .../CommonBattleClient/IBattleView.java | 3 +- .../IBattleSimulation.java | 2 + .../KnowledgeState.java | 29 ++ .../modulemon/MCTSBattleAI/MCTSBattleAI.java | 268 ++++++++++++++++++ .../MCTSBattleAI/MCTSBattleAIFactory.java | 36 +++ .../sdu/mmmi/modulemon/MCTSBattleAI/Node.java | 88 ++++++ .../dk/sdu/mmmi/modulemon/Map/MapView.java | 2 +- .../modulemon/common/SettingsRegistry.java | 5 + .../mmmi/modulemon/gameviews/MenuView.java | 59 +++- 12 files changed, 498 insertions(+), 27 deletions(-) create mode 100644 src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java create mode 100644 src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java create mode 100644 src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAIFactory.java create mode 100644 src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java diff --git a/src/main/java/dk/sdu/mmmi/Main.java b/src/main/java/dk/sdu/mmmi/Main.java index 35c57ece..e3288fe8 100644 --- a/src/main/java/dk/sdu/mmmi/Main.java +++ b/src/main/java/dk/sdu/mmmi/Main.java @@ -5,6 +5,7 @@ import dk.sdu.mmmi.modulemon.Collision.CollisionProcessing; import dk.sdu.mmmi.modulemon.Game; import dk.sdu.mmmi.modulemon.Interaction.InteractProcessing; +import dk.sdu.mmmi.modulemon.MCTSBattleAI.MCTSBattleAIFactory; import dk.sdu.mmmi.modulemon.Map.MapView; import dk.sdu.mmmi.modulemon.MapEntities.MapEntityPlugin; import dk.sdu.mmmi.modulemon.Monster.BattleMonsterProcessor; @@ -29,7 +30,8 @@ public static void main(String[] args) throws IOException, URISyntaxException { var settings = new Settings(); var monsterRegistry = new MonsterRegistry(); var battleMonsterProcessor = new BattleMonsterProcessor(); - var battleAI = new dk.sdu.mmmi.modulemon.BattleAI.BattleAIFactory(); + var battleAI = new MCTSBattleAIFactory(); + //var battleAI = new dk.sdu.mmmi.modulemon.BattleAI.BattleAIFactory(); // var battleAI = new dk.sdu.mmmi.modulemon.SimpleAI.BattleAIFactory(); // Uncomment for Simple AI var battleSimulation = new BattleSimulation(); diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java index e5fde101..c517ba2d 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java @@ -4,6 +4,7 @@ import dk.sdu.mmmi.modulemon.CommonBattle.IBattleParticipant; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleSimulation; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleState; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.KnowledgeState; import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; import dk.sdu.mmmi.modulemon.common.SettingsRegistry; @@ -248,7 +249,7 @@ private boolean isTerminal(IBattleState battleState) { // Check if all the opposing participant's (known) monster are dead boolean allEnemyMonstersDead = enemy.getMonsterTeam().stream() - .filter(x -> knowledgeState.enemyMonsters.contains(x)) //only consider monsters we've seen + .filter(x -> knowledgeState.getEnemyMonsters().contains(x)) //only consider monsters we've seen .allMatch(x -> x.getHitPoints()<=0); if (allEnemyMonstersDead) return true; @@ -326,24 +327,4 @@ private IBattleParticipant getActiveParticipant(IBattleState battleState) { ? battleState.getPlayer() // Return the player, if it is the player's turn : battleState.getEnemy(); // Return the enemy, if it is the enemy's turn } - - private class KnowledgeState { - // Those of the enemy's monsters, the AI has seen - private List enemyMonsters; - // A map, mapping each of the enemy's monsters to a list of the moves, the AI has seen it use - private Map> monsterMoves; - - public KnowledgeState() { - enemyMonsters = new ArrayList<>(); - monsterMoves = new HashMap<>(); - } - - public List getEnemyMonsters() { - return enemyMonsters; - } - - public Map> getMonsterMoves() { - return monsterMoves; - } - } } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java index d875bdf3..835b8a2d 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java @@ -13,6 +13,7 @@ import dk.sdu.mmmi.modulemon.CommonBattleClient.IBattleCallback; import dk.sdu.mmmi.modulemon.CommonBattleClient.IBattleView; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.BattleEvents.*; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAIFactory; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleSimulation; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleState; import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; @@ -91,7 +92,7 @@ public BattleView() { /** * Initialize for IBattleView */ - public void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback) { + public void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback, IBattleAIFactory battleAIFactory) { if (playerMonsters == null) { if (monsterRegistry == null) { return; @@ -124,6 +125,9 @@ public void startBattle(List playerMonsters, List enemyMonst _battleMusic = loader.getMusicAsset("/music/battle_music_" + battleMusic_type.toLowerCase() + ".ogg", this.getClass()); _winSound = loader.getSoundAsset("/sounds/you_won.ogg", this.getClass()); _loseSound = loader.getSoundAsset("/sounds/you_lost.ogg", this.getClass()); + if(battleAIFactory != null){ + _battleSimulation.setAIFactory(battleAIFactory); + } _battleSimulation.StartBattle(player, enemy); _currentBattleState = _battleSimulation.getState().clone(); // Set an initial battle-state _battleCallback = callback; diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java index 2230ae86..8fba6ad5 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java @@ -1,6 +1,7 @@ package dk.sdu.mmmi.modulemon.CommonBattleClient; import dk.sdu.mmmi.modulemon.CommonBattle.IBattleParticipant; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAIFactory; import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; import dk.sdu.mmmi.modulemon.common.services.IGameViewService; @@ -9,7 +10,7 @@ public interface IBattleView { // The init method should take the participants as arguments and create a BattleSimulation // I don't know if this is right, but that's how the interfaces are set up now - void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback); + void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback, IBattleAIFactory BattleAIFactory); IGameViewService getGameView(); void forceBattleEnd(); } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/IBattleSimulation.java b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/IBattleSimulation.java index b1e69cf2..86a932e2 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/IBattleSimulation.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/IBattleSimulation.java @@ -24,4 +24,6 @@ public interface IBattleSimulation { * Returns `null` if there is no more battle events */ IBattleEvent getNextBattleEvent(); + + void setAIFactory(IBattleAIFactory BattleAIFactory); } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java new file mode 100644 index 00000000..1635ec78 --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java @@ -0,0 +1,29 @@ +package dk.sdu.mmmi.modulemon.CommonBattleSimulation; + +import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; +import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class KnowledgeState { + // Those of the enemy's monsters, the AI has seen + private List enemyMonsters; + // A map, mapping each of the enemy's monsters to a list of the moves, the AI has seen it use + private Map> monsterMoves; + + public KnowledgeState() { + enemyMonsters = new ArrayList<>(); + monsterMoves = new HashMap<>(); + } + + public List getEnemyMonsters() { + return enemyMonsters; + } + + public Map> getMonsterMoves() { + return monsterMoves; + } +} diff --git a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java new file mode 100644 index 00000000..e9310f4d --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -0,0 +1,268 @@ +package dk.sdu.mmmi.modulemon.MCTSBattleAI; + +import dk.sdu.mmmi.modulemon.CommonBattle.IBattleParticipant; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAI; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleSimulation; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleState; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.KnowledgeState; +import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; +import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; +import dk.sdu.mmmi.modulemon.common.SettingsRegistry; +import dk.sdu.mmmi.modulemon.common.services.IGameSettings; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Stream; + +public class MCTSBattleAI implements IBattleAI { + + private KnowledgeState knowledgeState; + private IBattleParticipant participantToControl; + private IBattleParticipant opposingParticipant; + private IBattleSimulation battleSimulation; + private long startTime; + private int defaultTimeLimitMs = 1000; + private IGameSettings settings = null; + private final int MAX_SIMULATE_DEPTH = 20; + + private final float EXPLORATION_COEFFICIENT = (float) (1.0/Math.sqrt(2)); + + public MCTSBattleAI(IBattleSimulation battleSimulation, IBattleParticipant participantToControl, IGameSettings settings) { + knowledgeState = new KnowledgeState(); + this.participantToControl = participantToControl; + this.opposingParticipant = participantToControl == battleSimulation.getState().getPlayer() + ? battleSimulation.getState().getEnemy() + : battleSimulation.getState().getPlayer(); + this.battleSimulation = battleSimulation; + this.settings = settings; + } + + public boolean outOfTime() { + return ((System.nanoTime()-startTime)/1000000)>=getTimeLimitms(); + } + + private long getTimeLimitms() { + if (settings==null) { + return defaultTimeLimitMs; + } + Object limitObj = settings.getSetting(SettingsRegistry.getInstance().getAIProcessingTimeSetting()); + if (!(limitObj instanceof Integer)) { + return defaultTimeLimitMs; + } + System.out.println("Setting time limit to: " + (int)limitObj); + return (int) limitObj; + } + + @Override + public void doAction() { + System.out.println("Starting action finding"); + + // Update state, should the enemy have changed their monster + if (!knowledgeState.getEnemyMonsters().contains(opposingParticipant.getActiveMonster())) { + knowledgeState.getEnemyMonsters().add(opposingParticipant.getActiveMonster()); + } + + startTime = System.nanoTime(); + var rootNode = new Node(battleSimulation.getState().clone()); + + while(!outOfTime()){ + var newNode = treePolicy(rootNode); + var reward = defaultPolicy(newNode); + backpropagation(newNode, reward); + } + + var bestChild = bestChild(rootNode, 0); + + if(bestChild.getParentMove() != null){ + battleSimulation.doMove(participantToControl, bestChild.getParentMove()); + } else if(bestChild.getParentSwitch() != null){ + battleSimulation.switchMonster(participantToControl, bestChild.getParentSwitch()); + } else { + throw new IllegalStateException("AI found no moves to do"); + } + + } + + private void backpropagation(Node node, float reward) { + do{ + node.incrementTimesVisited(); + node.setReward(node.getReward() + reward); + node = node.getParent(); + }while(node != null); + } + + private float defaultPolicy(Node node) { + var state = node.getState().clone(); + var depth = 0; + while(!isTerminal(state) && depth < MAX_SIMULATE_DEPTH){ + var controllingAction = chooseRandomAction(participantToControl, state); + simulateAction(participantToControl, controllingAction); + var opposingAction = chooseRandomAction(opposingParticipant, state); + simulateAction(opposingParticipant, opposingAction); + depth++; + } + + return getReward(state); + } + + private float getReward(IBattleState battleState) { + IBattleParticipant participantToControl = battleState.getPlayer().equals(this.participantToControl) + ? battleState.getPlayer() + : battleState.getEnemy(); + + IBattleParticipant opposingParticipant = battleState.getPlayer().equals(this.participantToControl) + ? battleState.getEnemy() + : battleState.getPlayer(); + + int ownMonsterHPSum = 0; + for(IMonster monster : participantToControl.getMonsterTeam()) { + if (monster.getHitPoints()>0) ownMonsterHPSum += monster.getHitPoints(); + } + + int enemyMonsterHPSum = 0; + for(IMonster monster : opposingParticipant.getMonsterTeam()) { + if (knowledgeState.getEnemyMonsters().contains(monster)) { + if (monster.getHitPoints()>0) enemyMonsterHPSum += monster.getHitPoints(); + } + } + + // This will return 1 if all the enemy's monsters are dead, 0 if all the AI's monster + // are dead, and a number in between otherwise, which will be higher if the AI's monsters + // have a larger proportion of the hp of all the monsters in the battle + return (float)ownMonsterHPSum/(ownMonsterHPSum+enemyMonsterHPSum); + } + + private Object chooseRandomAction(IBattleParticipant participant, IBattleState state) { + var possibleMoves = participant.getActiveMonster().getMoves(); + var possibleSwitches = participant.getMonsterTeam(); + List possibleActions = Stream.concat(possibleMoves.stream(), possibleSwitches.stream()).toList(); + var rand = new Random(); + return possibleActions.get(rand.nextInt(possibleActions.size())); + } + + private IBattleState simulateAction(IBattleParticipant actor, Object action){ + if(action instanceof IMonsterMove move){ + return battleSimulation.simulateDoMove(actor, move, battleSimulation.getState()); + } else if(action instanceof IMonster monster){ + return battleSimulation.simulateSwitchMonster(actor, monster, battleSimulation.getState()); + } + throw new IllegalStateException("Cannot simulate when action is not an instance of IMonsterMove or IMonster"); + } + + private Node treePolicy(Node node){ + while(!isTerminal(node.getState())){ + if(!fullyExpanded(node)){ + return expandNode(node); + } else { + node = bestChild(node, EXPLORATION_COEFFICIENT); + } + } + return node; + } + + private Node bestChild(Node node, float exploration_coefficient) { + Node bestChild = null; + var bestUCT = Float.NEGATIVE_INFINITY; + + for (Node child : node.getChildren()) { + var uct = calculateUCT(child, exploration_coefficient); + if(uct > bestUCT){ + bestChild = child; + bestUCT = uct; + } + } + return bestChild; + } + + private float calculateUCT(Node child, float exploration_coefficient) { + var exploitation = child.getReward() / child.getTimesVisited(); + var exploration = Math.sqrt((2*Math.log(child.getParent().getTimesVisited()))/child.getTimesVisited()); + return (float) (exploitation + exploration_coefficient * exploration); + } + + private Node expandNode(Node node) { + Object action = untriedAction(node); + if(action instanceof Integer a && a == -1){ + throw new IllegalStateException("No possible action was found when expanding node"); + } + + Node child; + if(action instanceof IMonsterMove move){ + child = new Node( + battleSimulation.simulateDoMove(participantToControl, move, battleSimulation.getState()), + node, + move); + } else if(action instanceof IMonster monster){ + child = new Node( + battleSimulation.simulateSwitchMonster(participantToControl, monster, battleSimulation.getState()), + node, + monster); + } else { + throw new IllegalStateException("No action was chosen when expanding node"); + } + + return child; + } + + private Object untriedAction(Node node) { + var possibleMoves = participantToControl.getActiveMonster().getMoves(); + var possibleSwitchActions = participantToControl.getMonsterTeam().stream() + .filter(m -> m.getHitPoints() > 0 && m != participantToControl.getActiveMonster()).toList(); + List possibleActions = new ArrayList(); + for (IMonsterMove move : possibleMoves) { + if(node.getChildren().stream().noneMatch(c -> c.getParentMove() == move)){ + possibleActions.add(move); + } + } + for (IMonster monster : possibleSwitchActions) { + if(node.getChildren().stream().noneMatch(c -> c.getParentSwitch() == monster)){ + possibleActions.add(monster); + } + } + var possibleActionCount = possibleActions.size(); + if(possibleActionCount == 0) return -1; + else{ + var rand = new Random(); + return possibleActions.get(rand.nextInt((int) possibleActionCount)); + } + } + + private boolean fullyExpanded(Node node) { + var moveCount = participantToControl.getActiveMonster().getMoves().size(); + var switchCount = participantToControl.getMonsterTeam().stream() + .filter(m -> m.getHitPoints() > 0 && m != participantToControl.getActiveMonster()) + .count(); + return node.getChildren().size() == (moveCount + switchCount); + } + + private boolean isTerminal(IBattleState battleState) { + IBattleParticipant enemy = battleState.getPlayer().equals(this.participantToControl) + ? battleState.getEnemy() + : battleState.getPlayer(); + + // Check if all the AIs monsters are dead + boolean allOwnMonstersDead = participantToControl.getMonsterTeam().stream() + .allMatch(x -> x.getHitPoints()<=0); + if (allOwnMonstersDead) return true; + + // Check if all the opposing participant's (known) monster are dead + boolean allEnemyMonstersDead = enemy.getMonsterTeam().stream() + .filter(x -> knowledgeState.getEnemyMonsters().contains(x)) //only consider monsters we've seen + .allMatch(x -> x.getHitPoints()<=0); + return allEnemyMonstersDead; + } + + @Override + public void opposingMonsterUsedMove(IMonster monster, IMonsterMove move) { + // If we don't know anything about the monsters moves yet, add an empty list + if (!knowledgeState.getMonsterMoves().containsKey(monster)){ + knowledgeState.getMonsterMoves().put(monster, new ArrayList<>()); + } + // If this is the first time we see this monster using this move, add the move to + // the list of known moves for that monster + if (!knowledgeState.getMonsterMoves().get(monster).contains(move)) { + knowledgeState.getMonsterMoves().get(monster).add(move); + } + } +} diff --git a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAIFactory.java b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAIFactory.java new file mode 100644 index 00000000..078182bd --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAIFactory.java @@ -0,0 +1,36 @@ +package dk.sdu.mmmi.modulemon.MCTSBattleAI; + +import dk.sdu.mmmi.modulemon.CommonBattle.IBattleParticipant; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAI; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAIFactory; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleSimulation; +import dk.sdu.mmmi.modulemon.common.SettingsRegistry; +import dk.sdu.mmmi.modulemon.common.services.IGameSettings; + +public class MCTSBattleAIFactory implements IBattleAIFactory { + + private IGameSettings settings = null; + + public MCTSBattleAIFactory(){} + + @Override + public IBattleAI getBattleAI(IBattleSimulation battleSimulation, IBattleParticipant participantToControl) { + return new MCTSBattleAI(battleSimulation, participantToControl, this.settings); + } + + public void setSettingsService(IGameSettings settings){ + this.settings = settings; + if(settings.getSetting(SettingsRegistry.getInstance().getAIProcessingTimeSetting())==null){ + settings.setSetting(SettingsRegistry.getInstance().getAIProcessingTimeSetting(), 1000); + } + } + + public void removeSettingsService(IGameSettings settings) { + this.settings = null; + } + + @Override + public String toString() { + return "Monte-Carlo Tree Search"; + } +} diff --git a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java new file mode 100644 index 00000000..49afcfaa --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java @@ -0,0 +1,88 @@ +package dk.sdu.mmmi.modulemon.MCTSBattleAI; + +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleState; +import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; +import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; + +import java.util.ArrayList; + +public class Node { + private IBattleState state; + private ArrayList children = new ArrayList(); + private Node parent = null; + private IMonsterMove parentMove = null; + private IMonster parentSwitch = null; + private float reward = 0; + private int timesVisited = 0; + public Node(IBattleState state) { + this.state = state; + } + public Node(IBattleState state, Node parent, IMonsterMove parentMove) { + this.state = state; + this.parent = parent; + this.parentMove = parentMove; + parent.getChildren().add(this); + } + public Node(IBattleState state, Node parent, IMonster parentSwitch) { + this.state = state; + this.parent = parent; + this.parentSwitch = parentSwitch; + parent.getChildren().add(this); + } + + public IBattleState getState() { + return state; + } + + public void setState(IBattleState state) { + this.state = state; + } + + public ArrayList getChildren() { + return children; + } + + public void setChildren(ArrayList children) { + this.children = children; + } + + public Node getParent() { + return parent; + } + + public void setParent(Node parent) { + this.parent = parent; + } + + public IMonsterMove getParentMove() { + return parentMove; + } + + public void setParentMove(IMonsterMove parentMove) { + this.parentMove = parentMove; + } + + public IMonster getParentSwitch() { + return parentSwitch; + } + + public void setParentSwitch(IMonster parentSwitch) { + this.parentSwitch = parentSwitch; + } + + public float getReward() { + return reward; + } + + public void setReward(float reward) { + this.reward = reward; + } + + public int getTimesVisited() { + return timesVisited; + } + + public void incrementTimesVisited() { + this.timesVisited++; + } +} diff --git a/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java b/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java index 56e0cf44..d767197c 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java @@ -692,7 +692,7 @@ public void startEncounter(Entity player, Entity enemy) { if (!playerWon) { enemyMonsterTeamPart.healAllMonsters(); } - }); + }, null); } @Override diff --git a/src/main/java/dk/sdu/mmmi/modulemon/common/SettingsRegistry.java b/src/main/java/dk/sdu/mmmi/modulemon/common/SettingsRegistry.java index 29d4c441..82f371b8 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/common/SettingsRegistry.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/common/SettingsRegistry.java @@ -16,6 +16,8 @@ public class SettingsRegistry { private UUID ai_processing_time = UUID.randomUUID(); private UUID rectangle_style = UUID.randomUUID(); private UUID battle_theme = UUID.randomUUID(); + private UUID battle_AI = UUID.randomUUID(); + private SettingsRegistry(){ settingsMap = new HashMap<>(); populateSettings(); @@ -34,6 +36,7 @@ private void populateSettings(){ settingsMap.put(ai_processing_time, "AI processing time"); settingsMap.put(rectangle_style, "personaRectangles"); settingsMap.put(battle_theme, "battleMusicTheme"); + settingsMap.put(battle_AI, "AI"); } /* @@ -64,6 +67,8 @@ public String getBattleMusicThemeSetting(){ return settingsMap.get(battle_theme); } + public String getBattleAISetting() { return settingsMap.get(battle_AI); } + public static SettingsRegistry getInstance(){ if(instance == null){ diff --git a/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java b/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java index 38222ed2..1c512302 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java @@ -11,7 +11,10 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator; import dk.sdu.mmmi.modulemon.CommonBattleClient.IBattleView; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAIFactory; import dk.sdu.mmmi.modulemon.Game; +import dk.sdu.mmmi.modulemon.MCTSBattleAI.MCTSBattleAIFactory; +import dk.sdu.mmmi.modulemon.SimpleAI.BattleAIFactory; import dk.sdu.mmmi.modulemon.common.AssetLoader; import dk.sdu.mmmi.modulemon.common.OSGiFileHandle; import dk.sdu.mmmi.modulemon.common.SettingsRegistry; @@ -47,6 +50,7 @@ public class MenuView implements IGameViewService { private String soundVolume = ""; private String aiTime = ""; private String battleTheme = ""; + private String AI = ""; private int battleThemeIndex = 0; private String title = ""; @@ -68,6 +72,15 @@ public class MenuView implements IGameViewService { "Use AI Alpha-beta pruning", "AI Processing Time", "Battle Music Theme", + "AI" + }; + + private int AIIndex = 0; + + private String[] AIOptions = new String[]{ + "MCTS", + "Simple", + "Minimax" }; private Sound selectSound; @@ -318,9 +331,17 @@ private void selectOption(IGameViewManager gvm) { } IGameViewService selectedView = views.get(currentOption); gvm.setView(selectedView); - if (selectedView instanceof IBattleView) { + if (selectedView instanceof IBattleView battleView) { chooseSound.play(getSoundVolumeAsFloat()); - ((IBattleView) selectedView).startBattle(null, null, null); + IBattleAIFactory desiredAI = null; + if(settings.getSetting("AI").equals("MCTS")){ + desiredAI = new MCTSBattleAIFactory(); + } else if(settings.getSetting("AI").equals("Simple")){ + desiredAI = new dk.sdu.mmmi.modulemon.SimpleAI.BattleAIFactory(); + } else { + desiredAI = new dk.sdu.mmmi.modulemon.BattleAI.BattleAIFactory(); + } + battleView.startBattle(null, null, null, desiredAI); } } else { if (Objects.equals(menuOptions[currentOption], "Play")) { @@ -393,6 +414,19 @@ private void handleSettings(String keyInput) { break; } + if(menuOptions[currentOption].equalsIgnoreCase("AI")){ + AIIndex++; + + AIIndex = AIIndex % AIOptions.length; + settings.setSetting(settingsRegistry.getBattleAISetting(), AIOptions[AIIndex]); + + AI = (String) settings.getSetting(settingsRegistry.getBattleAISetting()); + settingsValueList.set(6, AI); + + chooseSound.play(getSoundVolumeAsFloat()); + break; + } + if (menuOptions[currentOption].equalsIgnoreCase("Battle Music Theme")) { battleThemeIndex++; @@ -454,6 +488,23 @@ private void handleSettings(String keyInput) { break; } + + if(menuOptions[currentOption].equalsIgnoreCase("AI")){ + if (AIIndex == 0) { + AIIndex = AIOptions.length - 1; + } else { + AIIndex--; + } + + settings.setSetting(settingsRegistry.getBattleAISetting(), AIOptions[AIIndex]); + + AI = (String) settings.getSetting(settingsRegistry.getBattleAISetting()); + settingsValueList.set(6, AI); + + chooseSound.play(getSoundVolumeAsFloat()); + break; + } + if (menuOptions[currentOption].equalsIgnoreCase("Battle Music Theme")) { // If the index is at the start, go to the back of the array, ensuring it can loop indefinitely if (battleThemeIndex == 0) { @@ -541,6 +592,10 @@ private void settingsInitializer() { battleTheme = (String) settings.getSetting(settingsRegistry.getBattleMusicThemeSetting()); settingsValueList.add(battleTheme); battleThemeIndex = Arrays.asList(musicThemes).indexOf(battleTheme); // Sets the music theme index to the position of the currently selected theme found in the settings file + + AI = (String) settings.getSetting(settingsRegistry.getBattleAISetting()); + settingsValueList.add(AI); + AIIndex = Arrays.asList(AIOptions).indexOf(AI); } } From db97ea8e07b29dc4c75d9a8408c3b417a4bfafeb Mon Sep 17 00:00:00 2001 From: VictorABoye Date: Tue, 24 Oct 2023 14:01:13 +0200 Subject: [PATCH 2/3] Moved setting of battleAI into BattleView instead of MenuView --- .../modulemon/BattleScene/BattleView.java | 19 +++++++++++++++---- .../CommonBattleClient/IBattleView.java | 2 +- .../dk/sdu/mmmi/modulemon/Map/MapView.java | 2 +- .../mmmi/modulemon/gameviews/MenuView.java | 10 +--------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java index 835b8a2d..87d9679e 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java @@ -19,6 +19,7 @@ import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterRegistry; +import dk.sdu.mmmi.modulemon.MCTSBattleAI.MCTSBattleAIFactory; import dk.sdu.mmmi.modulemon.common.AssetLoader; import dk.sdu.mmmi.modulemon.common.SettingsRegistry; import dk.sdu.mmmi.modulemon.common.animations.BaseAnimation; @@ -92,7 +93,7 @@ public BattleView() { /** * Initialize for IBattleView */ - public void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback, IBattleAIFactory battleAIFactory) { + public void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback) { if (playerMonsters == null) { if (monsterRegistry == null) { return; @@ -125,9 +126,7 @@ public void startBattle(List playerMonsters, List enemyMonst _battleMusic = loader.getMusicAsset("/music/battle_music_" + battleMusic_type.toLowerCase() + ".ogg", this.getClass()); _winSound = loader.getSoundAsset("/sounds/you_won.ogg", this.getClass()); _loseSound = loader.getSoundAsset("/sounds/you_lost.ogg", this.getClass()); - if(battleAIFactory != null){ - _battleSimulation.setAIFactory(battleAIFactory); - } + setBattleAIFactory(); _battleSimulation.StartBattle(player, enemy); _currentBattleState = _battleSimulation.getState().clone(); // Set an initial battle-state _battleCallback = callback; @@ -145,6 +144,18 @@ public void startBattle(List playerMonsters, List enemyMonst _battleStarted = true; } + private void setBattleAIFactory() { + IBattleAIFactory desiredAI; + if(settings.getSetting("AI").equals("MCTS")){ + desiredAI = new MCTSBattleAIFactory(); + } else if(settings.getSetting("AI").equals("Simple")){ + desiredAI = new dk.sdu.mmmi.modulemon.SimpleAI.BattleAIFactory(); + } else { + desiredAI = new dk.sdu.mmmi.modulemon.BattleAI.BattleAIFactory(); + } + _battleSimulation.setAIFactory(desiredAI); + } + @Override public IGameViewService getGameView() { return this; diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java index 8fba6ad5..01894900 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleView.java @@ -10,7 +10,7 @@ public interface IBattleView { // The init method should take the participants as arguments and create a BattleSimulation // I don't know if this is right, but that's how the interfaces are set up now - void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback, IBattleAIFactory BattleAIFactory); + void startBattle(List playerMonsters, List enemyMonsters, IBattleCallback callback); IGameViewService getGameView(); void forceBattleEnd(); } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java b/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java index d767197c..56e0cf44 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/Map/MapView.java @@ -692,7 +692,7 @@ public void startEncounter(Entity player, Entity enemy) { if (!playerWon) { enemyMonsterTeamPart.healAllMonsters(); } - }, null); + }); } @Override diff --git a/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java b/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java index 1c512302..c51b3d68 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java @@ -333,15 +333,7 @@ private void selectOption(IGameViewManager gvm) { gvm.setView(selectedView); if (selectedView instanceof IBattleView battleView) { chooseSound.play(getSoundVolumeAsFloat()); - IBattleAIFactory desiredAI = null; - if(settings.getSetting("AI").equals("MCTS")){ - desiredAI = new MCTSBattleAIFactory(); - } else if(settings.getSetting("AI").equals("Simple")){ - desiredAI = new dk.sdu.mmmi.modulemon.SimpleAI.BattleAIFactory(); - } else { - desiredAI = new dk.sdu.mmmi.modulemon.BattleAI.BattleAIFactory(); - } - battleView.startBattle(null, null, null, desiredAI); + battleView.startBattle(null, null, null); } } else { if (Objects.equals(menuOptions[currentOption], "Play")) { From 4e572f97c22a73351c226c4fc9dbeeea1001897c Mon Sep 17 00:00:00 2001 From: Victor <35064405+VictorABoye@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:05:35 +0200 Subject: [PATCH 3/3] Update src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexander Nørup --- .../java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java index e9310f4d..61bc6410 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -233,7 +233,7 @@ private boolean fullyExpanded(Node node) { var switchCount = participantToControl.getMonsterTeam().stream() .filter(m -> m.getHitPoints() > 0 && m != participantToControl.getActiveMonster()) .count(); - return node.getChildren().size() == (moveCount + switchCount); + return node.getChildren().size() >= (moveCount + switchCount); } private boolean isTerminal(IBattleState battleState) {