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..87d9679e 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java @@ -13,11 +13,13 @@ 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; 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; @@ -124,6 +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()); + setBattleAIFactory(); _battleSimulation.StartBattle(player, enemy); _currentBattleState = _battleSimulation.getState().clone(); // Set an initial battle-state _battleCallback = callback; @@ -141,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 2230ae86..01894900 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; 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..61bc6410 --- /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/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..c51b3d68 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,9 @@ 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); + battleView.startBattle(null, null, null); } } else { if (Objects.equals(menuOptions[currentOption], "Play")) { @@ -393,6 +406,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 +480,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 +584,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); } }