From 670bd1e0c96aca90422205942a797208be687a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20N=C3=B8rup?= Date: Tue, 7 Nov 2023 09:11:51 +0100 Subject: [PATCH 1/7] Fix infinity issue --- .../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 a85b8e10..4db08525 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -188,7 +188,7 @@ private float defaultPolicy(Node node) { var reward = getReward(state); if (reward > 0) { - reward *= (1f / depth); // Making sure that the deeper, the worse reward + reward *= (2f / (depth+1)); // Making sure that the deeper, the worse reward } return reward; From c8433f43c28cc08e633088bec078e23275422f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20N=C3=B8rup?= Date: Tue, 7 Nov 2023 09:30:29 +0100 Subject: [PATCH 2/7] Added times visited count --- .../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 4db08525..6fa462b6 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -78,7 +78,7 @@ public void doAction() { var bestChild = bestChild(rootNode, 0); - System.out.println(String.format("Simulated %d actions in %dms", this.numSimulatedActions, ((System.nanoTime() - startTime) / 1000000))); + System.out.println(String.format("Expanded %d nodes, based on %d simulated actions in %dms", rootNode.getTimesVisited(), this.numSimulatedActions, ((System.nanoTime() - startTime) / 1000000))); System.out.println(explainNodeOptions(rootNode)); // System.out.println(explainBestChild(bestChild)); From 22bd32c19835bcbfcc2ae86dab46f908ff64b496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20N=C3=B8rup?= Date: Tue, 7 Nov 2023 11:19:32 +0100 Subject: [PATCH 3/7] Added knowlegde states with setting --- .../sdu/mmmi/modulemon/BattleAI/BattleAI.java | 13 +-- .../BattleSimulation/BattleSimulation.java | 2 + .../KnowledgeState.java | 23 ++++- src/main/java/dk/sdu/mmmi/modulemon/Game.java | 3 + .../modulemon/MCTSBattleAI/EmptyMove.java | 25 +++++ .../modulemon/MCTSBattleAI/MCTSBattleAI.java | 90 ++++++++++++------ .../modulemon/common/SettingsRegistry.java | 5 + .../mmmi/modulemon/gameviews/MenuView.java | 17 +++- src/main/resources/sounds/alert.ogg | Bin 15061 -> 15082 bytes 9 files changed, 140 insertions(+), 38 deletions(-) create mode 100644 src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/EmptyMove.java 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 c517ba2d..fa5bc082 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java @@ -30,7 +30,9 @@ public class BattleAI implements IBattleAI { public BattleAI(IBattleSimulation battleSimulation, IBattleParticipant participantToControl, IGameSettings settings) { - knowledgeState = new KnowledgeState(); + var enableKnowlegdeStates = (Boolean) settings.getSetting(SettingsRegistry.getInstance().getAIKnowlegdeStateEnabled()); + System.out.println(String.format("Minimax AI using knowledge states: %b", enableKnowlegdeStates)); + knowledgeState = new KnowledgeState(!enableKnowlegdeStates); this.participantToControl = participantToControl; this.opposingParticipant = participantToControl == battleSimulation.getState().getPlayer() ? battleSimulation.getState().getEnemy() @@ -249,7 +251,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.getEnemyMonsters().contains(x)) //only consider monsters we've seen + .filter(x -> knowledgeState.hasSeenMonster(x)) //only consider monsters we've seen .allMatch(x -> x.getHitPoints()<=0); if (allEnemyMonstersDead) return true; @@ -272,7 +274,7 @@ private float utility(IBattleState battleState) { int enemyMonsterHPSum = 0; for(IMonster monster : opposingParticipant.getMonsterTeam()) { - if (knowledgeState.getEnemyMonsters().contains(monster)) { + if (knowledgeState.hasSeenMonster(monster)) { if (monster.getHitPoints()>0) enemyMonsterHPSum += monster.getHitPoints(); } } @@ -291,8 +293,7 @@ private List successorFunction(IBattleState battleState) { for (IMonsterMove move : activeParticipant.getActiveMonster().getMoves()) { if (!activeParticipant.equals(participantToControl)) { - if (!(knowledgeState.getMonsterMoves().containsKey(activeParticipant.getActiveMonster()) && - knowledgeState.getMonsterMoves().get(activeParticipant.getActiveMonster()).contains(move))){ + if (!(knowledgeState.hasSeenMove(activeParticipant.getActiveMonster(), move))){ continue; // If we have not seen this move, don't consider the option where it is used } } @@ -306,7 +307,7 @@ private List successorFunction(IBattleState battleState) { for (IMonster monster : activeParticipant.getMonsterTeam()) { if (!activeParticipant.equals(participantToControl)) { - if (!knowledgeState.getEnemyMonsters().contains(monster)){ + if (!knowledgeState.hasSeenMonster(monster)){ continue; } } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java index c3865dda..7148ea5a 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java @@ -278,6 +278,7 @@ private void doOpponentMove() { getOpponentAI().doAction(); } catch (Exception ex) { System.out.println(ex); + ex.printStackTrace(); nextEvent = new AICrashedEvent(String.format("The opponent %s controller has crashed!", this.opponentAIFactory), this.battleState.getActiveParticipant(), ex, battleState.clone()); onNextEvent = this::switchTurns; } @@ -302,6 +303,7 @@ private void doPlayerMove() { getPlayerAI().doAction(); } catch (Exception ex) { System.out.println(ex); + ex.printStackTrace(); nextEvent = new AICrashedEvent(String.format("The player %s controller has crashed!", this.playerAIFactory), this.battleState.getActiveParticipant(), ex, battleState.clone()); onNextEvent = this::switchTurns; } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java index 1635ec78..6ac8d822 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/KnowledgeState.java @@ -14,7 +14,10 @@ public class KnowledgeState { // 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() { + private boolean allKnowing = false; + + public KnowledgeState(boolean allKnowing) { + this.allKnowing = allKnowing; enemyMonsters = new ArrayList<>(); monsterMoves = new HashMap<>(); } @@ -26,4 +29,22 @@ public List getEnemyMonsters() { public Map> getMonsterMoves() { return monsterMoves; } + + public boolean hasSeenMove(IMonster monster, IMonsterMove move){ + if(allKnowing){ + return true; + } + if(!this.monsterMoves.containsKey(monster)){ + return false; + } + + return this.monsterMoves.get(monster).contains(move); + } + + public boolean hasSeenMonster(IMonster monster){ + if(allKnowing){ + return true; + } + return this.enemyMonsters.contains(monster); + } } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/Game.java b/src/main/java/dk/sdu/mmmi/modulemon/Game.java index d3956b30..63dd03ad 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/Game.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/Game.java @@ -175,6 +175,9 @@ public void setSettingsService(IGameSettings settings) { if (settings.getSetting(SettingsRegistry.getInstance().getAIAlphaBetaSetting()) == null) { settings.setSetting(SettingsRegistry.getInstance().getAIAlphaBetaSetting(), true); } + if (settings.getSetting(SettingsRegistry.getInstance().getAIKnowlegdeStateEnabled()) == null) { + settings.setSetting(SettingsRegistry.getInstance().getAIKnowlegdeStateEnabled(), true); + } if (settings.getSetting(SettingsRegistry.getInstance().getBattleMusicThemeSetting()) == null) { settings.setSetting(SettingsRegistry.getInstance().getBattleMusicThemeSetting(), "Original"); } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/EmptyMove.java b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/EmptyMove.java new file mode 100644 index 00000000..bf1345da --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/EmptyMove.java @@ -0,0 +1,25 @@ +package dk.sdu.mmmi.modulemon.MCTSBattleAI; + +import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; + +public class EmptyMove implements IMonsterMove { + @Override + public String getName() { + return "Loaf around"; + } + + @Override + public String getSoundPath() { + return null; + } + + @Override + public String getBattleDescription() { + return "Your monster loafs around. It does nothing."; + } + + @Override + public String getSummaryScreenDescription() { + return getBattleDescription(); + } +} 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 6fa462b6..0dec5397 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -16,7 +16,6 @@ import java.util.stream.Stream; public class MCTSBattleAI implements IBattleAI { - private KnowledgeState knowledgeState; private IBattleParticipant participantToControl; private IBattleParticipant opposingParticipant; @@ -30,7 +29,9 @@ public class MCTSBattleAI implements IBattleAI { private final float EXPLORATION_COEFFICIENT = (float) (1.0 / Math.sqrt(2)); public MCTSBattleAI(IBattleSimulation battleSimulation, IBattleParticipant participantToControl, IGameSettings settings) { - knowledgeState = new KnowledgeState(); + var enableKnowlegdeStates = (Boolean) settings.getSetting(SettingsRegistry.getInstance().getAIKnowlegdeStateEnabled()); + System.out.println(String.format("MCTS AI using knowledge states: %b", enableKnowlegdeStates)); + knowledgeState = new KnowledgeState(!enableKnowlegdeStates); this.participantToControl = participantToControl; this.opposingParticipant = participantToControl == battleSimulation.getState().getPlayer() ? battleSimulation.getState().getEnemy() @@ -138,7 +139,7 @@ private String explainBestChild(Node bestChild) { .append(bestChildOfCurrentlyExpanding.getReward()) .append(")"); - if (isTerminal(currentlyExpanding.getState())) { + if (isTerminal(currentlyExpanding.getState(), true)) { stringBuilder.append(" [TERMINAL STATE]"); } @@ -174,10 +175,10 @@ private float defaultPolicy(Node node) { participant2 = participantToControl; } - while (!isTerminal(state) && depth < MAX_SIMULATE_DEPTH) { + while (!isTerminal(state, false) && depth < MAX_SIMULATE_DEPTH) { var action1 = chooseRandomAction(getParticipantFromState(state, participant1)); state = simulateAction(participant1, action1, state); - if (!isTerminal(state)) { + if (!isTerminal(state, false)) { // Need to check for terminal state here as well, since participant2 might have lost. var action2 = chooseRandomAction(getParticipantFromState(state, participant2)); state = simulateAction(participant2, action2, state); @@ -240,6 +241,9 @@ private Object chooseRandomAction(IBattleParticipant participant) { private IBattleState simulateAction(IBattleParticipant actor, Object action, IBattleState currentState) { this.numSimulatedActions++; if (action instanceof IMonsterMove move) { + if(move instanceof EmptyMove){ + return battleSimulation.simulateDoMove(actor, null, currentState); + } return battleSimulation.simulateDoMove(actor, move, currentState); } else if (action instanceof IMonster monster) { return battleSimulation.simulateSwitchMonster(actor, monster, currentState); @@ -248,7 +252,7 @@ private IBattleState simulateAction(IBattleParticipant actor, Object action, IBa } private Node treePolicy(Node node) { - while (!isTerminal(node.getState())) { + while (!isTerminal(node.getState(), true)) { if (!fullyExpanded(node)) { return expandNode(node); } else { @@ -287,10 +291,17 @@ private Node expandNode(Node node) { Node child; if (action instanceof IMonsterMove move) { - child = new Node( - battleSimulation.simulateDoMove(node.getParticipant(), move, node.getState()), - node, - move); + if(move instanceof EmptyMove){ + child = new Node( + battleSimulation.simulateDoMove(node.getParticipant(), null, node.getState()), + node, + move); + }else { + child = new Node( + battleSimulation.simulateDoMove(node.getParticipant(), move, node.getState()), + node, + move); + } } else if (action instanceof IMonster monster) { child = new Node( battleSimulation.simulateSwitchMonster(node.getParticipant(), monster, node.getState()), @@ -304,22 +315,21 @@ private Node expandNode(Node node) { } private Object untriedAction(Node node) { - var possibleMoves = node.getParticipant().getActiveMonster().getMoves(); - var possibleSwitchActions = node.getParticipant().getMonsterTeam().stream() - .filter(m -> m.getHitPoints() > 0 && m != node.getParticipant().getActiveMonster()).toList(); + var allActions = getAllActionsForNode(node); List possibleActions = new ArrayList(); - // TODO: If the node.getParticipant() is not us, then check the knowlegde state - 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); + for (var action : allActions ) { + if(action instanceof IMonsterMove move) { + if (node.getChildren().stream().noneMatch(c -> c.getParentMove() == move)) { + possibleActions.add(move); + } + }else if(action instanceof IMonster switchMonster){ + if (node.getChildren().stream().noneMatch(c -> c.getParentSwitch() == switchMonster)) { + possibleActions.add(switchMonster); + } } } + var possibleActionCount = possibleActions.size(); if (possibleActionCount == 0) { return -1; @@ -329,16 +339,36 @@ private Object untriedAction(Node node) { } } - private boolean fullyExpanded(Node node) { - var moveCount = node.getParticipant().getActiveMonster().getMoves().size(); - var switchCount = node.getParticipant().getMonsterTeam().stream() - .filter(m -> m.getHitPoints() > 0 && m != node.getParticipant().getActiveMonster()) - .count(); + public List getAllActionsForNode(Node node){ + var monster = node.getParticipant().getActiveMonster(); + var possibleMoves = monster.getMoves(); + var possibleSwitchActions = node.getParticipant().getMonsterTeam().stream() + .filter(m -> m.getHitPoints() > 0 && m != node.getParticipant().getActiveMonster()).toList(); + + boolean useKnowledgeState = !node.getParticipant().equals(this.participantToControl); + + // If useKnowledgeState is true, then filter away all moves that haven't been seen before. + var actions = Stream.concat(possibleMoves.stream().filter(x -> !useKnowledgeState || this.knowledgeState.hasSeenMove(monster, x)), + possibleSwitchActions.stream().filter(x -> !useKnowledgeState || this.knowledgeState.hasSeenMonster(x))).toList(); + + if(actions.isEmpty()){ + if(useKnowledgeState){ + actions = new ArrayList<>(1); + actions.add(new EmptyMove()); + }else{ + throw new IllegalStateException(String.format("There were no actions for the node: %s", node)); + } + } - return node.getChildren().size() >= (moveCount + switchCount); + return actions; + } + + private boolean fullyExpanded(Node node) { + var numActions = getAllActionsForNode(node).size(); + return node.getChildren().size() >= numActions ; } - private boolean isTerminal(IBattleState battleState) { + private boolean isTerminal(IBattleState battleState, boolean useKnowlegdeState) { IBattleParticipant thisParticipant = battleState.getPlayer(); IBattleParticipant opposingParticipant = battleState.getEnemy(); @@ -353,7 +383,7 @@ private boolean isTerminal(IBattleState battleState) { // Check if all the opposing participant's (known) monster are dead boolean allEnemyMonstersDead = opposingParticipant.getMonsterTeam().stream() -// .filter(x -> knowledgeState.getEnemyMonsters().contains(x)) //only consider monsters we've seen + .filter(x -> !useKnowlegdeState || knowledgeState.getEnemyMonsters().contains(x)) //only consider monsters we've seen .allMatch(x -> x.getHitPoints() <= 0); return allEnemyMonstersDead || allOwnMonstersDead; 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 82f371b8..cab15868 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/common/SettingsRegistry.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/common/SettingsRegistry.java @@ -13,6 +13,7 @@ public class SettingsRegistry { private UUID music_volume = UUID.randomUUID(); private UUID sound_volume = UUID.randomUUID(); private UUID ai_alphaBeta = UUID.randomUUID(); + private UUID ai_knowlegdeState = UUID.randomUUID(); private UUID ai_processing_time = UUID.randomUUID(); private UUID rectangle_style = UUID.randomUUID(); private UUID battle_theme = UUID.randomUUID(); @@ -33,6 +34,7 @@ private void populateSettings(){ settingsMap.put(music_volume, "musicVolume"); settingsMap.put(sound_volume, "soundVolume"); settingsMap.put(ai_alphaBeta, "AI alpha-beta pruning"); + settingsMap.put(ai_knowlegdeState, "AI knowlegde state"); settingsMap.put(ai_processing_time, "AI processing time"); settingsMap.put(rectangle_style, "personaRectangles"); settingsMap.put(battle_theme, "battleMusicTheme"); @@ -51,6 +53,9 @@ public String getSoundVolumeSetting(){ return settingsMap.get(sound_volume); } + public String getAIKnowlegdeStateEnabled(){ + return settingsMap.get(ai_knowlegdeState); + } public String getAIAlphaBetaSetting(){ return settingsMap.get(ai_alphaBeta); } 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 0489d6ae..aafef9b6 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java @@ -72,7 +72,8 @@ public class MenuView implements IGameViewService { "Use AI Alpha-beta pruning", "AI Processing Time", "Battle Music Theme", - "AI" + "AI", + "Use AI knowledge states", }; private int AIIndex = 0; @@ -561,6 +562,18 @@ private void boolean_settings_switch_on_off() { settings.setSetting(settingsRegistry.getAIAlphaBetaSetting(), true); } chooseSound.play(getSoundVolumeAsFloat()); + } else if (menuOptions[currentOption].equalsIgnoreCase("Use AI knowledge states")) { + /* + If setting for using knowledge states in the AI, + */ + if (((Boolean) settings.getSetting(settingsRegistry.getAIKnowlegdeStateEnabled()))) { + settingsValueList.set(7, "Off"); + settings.setSetting(settingsRegistry.getAIKnowlegdeStateEnabled(), false); + } else { + settingsValueList.set(7, "On"); + settings.setSetting(settingsRegistry.getAIKnowlegdeStateEnabled(), true); + } + chooseSound.play(getSoundVolumeAsFloat()); } } } @@ -588,6 +601,8 @@ private void settingsInitializer() { AI = (String) settings.getSetting(settingsRegistry.getBattleAISetting()); settingsValueList.add(AI); + + settingsValueList.add((Boolean) settings.getSetting(settingsRegistry.getAIKnowlegdeStateEnabled()) ? "On" : "Off"); AIIndex = Arrays.asList(AIOptions).indexOf(AI); } } diff --git a/src/main/resources/sounds/alert.ogg b/src/main/resources/sounds/alert.ogg index c1a4e3d64fd35205ab50fe34f2329fdd6809ac90..164cae957084191070bbc226af118af864e9fe83 100644 GIT binary patch delta 10842 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 delta 10821 zcmZ8{4Oo(A`}cJt5F!{jAi``&R*2f5#9Cj&1T)0~1wyPnh*^r-*UD|R#Vsk_p*F4M z%vM88Lxct;wzbtzp_P413z@Uk+E!biTidEV&psaim;1l(alFS1$D#Lq!}W2V*LnWV z^ZcDW*gmqIvueYJe1HT0=cPCP@5S0RMaP;rb7oK6Bk+;*H{z5S{rLa)YKo}ZwAE1d zT58iK&eXX=;e25#m!DU%ZhLXr#tmHA#;UTdn>JQ@54mY3;s4G%G2g^ueS`F#=}X4b zmZUFQwQ4+aeA0OA_`LDu{cGC82g?nY<9@vL`xWoXrIe)cp7PE$ zA~GO-Tv^}nP~JXkxs&{Z=x*i0ZB^=**5ZG?eB>wAk?_&1%X8w7Un_b4#BZx3AODft zw|~-GQ@=EokIy)8_w~fS&(mM|^KJKn-;ws=ZwKa5^P8p~xO^Fa&{ty#4To9>MEFOwRsjB3<{Yu&0Fhx9K!G~Vc4`|iMK z@%6kPY$XL_-VNXaJ};eySJD!hgmi`lePuLlv^#2r&Qvons;F=#Yht=&{Y0Yw6rtK%KUDdtw>o?cY(~J<^n;%Ov;%i0osp~d=ct*h)g7tzm6B^b zli+M%8UWKQ-x_a>zy0vjV_W`uSY3j}zh2ZbMz3UgZWHT4@bp?HMomo0c6|L%GZkH=<`Yg#OMC&vu z(^vQRF2#!n6Ri>w1mdL+^AS_ZrTSwhntm3k36&$(3BLr-~uPLm+97roIH(@l`R%@TtDb* zPF26n+Nm49>64C_!PJ^5@d981JARx-{_o?$lm`cvzSa}-&C7fGO@C^lHvMsV*!ox3 z>h45?@o0=#newe|a>cFmWVQ8$Nvko-0!FYVa>9cc9w%#2Ro#|XUVqhH zM34#QeW9aE@si5A&gQCSjL-BdrKmA1FoWISW$zc&77QEIKYbDVv1t z#kC%uz3x9LD(l_G0E*V_Mbsf@Cu|$-dvHKU!dGweBwVADJh0><|v@G(+uRPXp(&U7x zW1mG$`LZ@X4wqx{)FOJdPg@rq1h?kjyx_TV;Ntq?D-O-Bdxtl!8~x#8X(*)M{+_Qg2mE-cyRg-$W!o_}P6||?bYV#RUXECaY}kt&(g_G zOt|h7sZi&x66yCuIU3dK6>5C7zaZ(D? z0&uw@#gwMdTyJe;i(zD$Pk@@JPPK<82q%Z6c;MGmZgaA7CXWwRmg!bA(h))`M~Lxw zX17tyq1VfVNzZ;x<}}z8&)JRUzlp%Jq`PcSTj6y;+UzKdNWev1T){Rc-~(-wSbp|H zfdtg|G(6tiSigD_9-zp#Be%!N;M(nj%_*7w4Vpvqu z!pFt_K&7|g$z7_}ZF|~ALo6uNHY{sJs4*w}0XT}?Ed6Oi`G1sW)*pX0ud_55%jELHYB+g)iQk!FugZ>69$`OW6ddA&H_ptn1c{ zaFVdIL7|CG>2?Q&l|8i$c1uS`wOydOqfPw&Dl43#q$d-ilENO9#66na>1=fZY>c;@ z!mP%FMcYW#b$Cgd-$FVcw{ONBkNX6kdom!(-9zXWq47( zRkt5Q*?R5inHV%zyV>Y+7#dPc^_yN3C!AZSI~%j96anPO3OWEo#;ABI71)Q{^!0{h zcOl2z<>2XWGSoh!-N?q}e9FTzp(FuYX)>EO{6Rzn-C27ghIb6u$OlK7v@)d2fisT+ z;PfpVZUbOC)Y*TR3@&~Apnm3xZWrVKy)|_o+qMcfWBpf`dLw>~WqVSp>@6%(5H#(( zTIKH-A%@{K+Q^DaxfHk_s(#Gq#;8_Z=xW&5+S}V=*f4si%IP$6pIcgb(>f!8)R@%i zkX42v&}>8Q5p0UfqMa#L(-_JMd0j(SXF~(CFE!wwo==y$EwX@z_uOjg>eRU`WuqJ; zjzqRO(+bEW4O7IDD(mkJb)h(e(}|_H>kLI7ltcm0S+uFFyVkHt>#QdAi}&$QfW}C@ zPN#Krbz-{fC$j*W$sEFhnA&}>M?2K=zJ`QPo%F)pvt{kWj&K)@6dtyuSj1A& z{rmoV$i&9tQL08EaozJ^NPq;Til&ukN4Ugs$tKKFRI@@zx?y|>|Zi(A^YDP%qqEQ<0$ht>vheu!G+e#1BT7Eo0Ix2 zSGXrWe(C*@e#XmK$JXECCbU^bSBj{nx+7&93`$zPlb)_ZBr!jHFBIR}jspUFtBdf$ zD~=3D?2mJfh&J3g>uxg~a2?zMB$P!f6hpVsVBvQXf)5)#T&0zzq_M-)93TC#oSq>s zP}>F;WfLtSdyY5 zJTzHxV0ud&`0~5{`u^6tN0V36qHw1oZtK>JDz)?C3g(=;-Iu^qOS4ymmA_AdTH_18 z9wNqU^c?U!%0RnTCQ86J06d+9(E#eHe*8+}$xI@e0Rn5e)QX2`;6Qa9;qylOF+ufc zq0LKTC_7gp z;NGE?>gCH)@`E0R!T4HCF^fq6LQyWF05pP- zDwYozv9q#ZkCzY3Mddo7{JLQ*7PT9SOG_L~08C<|JIFUQh#583y`q{V0VOhhA`dR5 z&g>%L)BmD49H}efwy5J5a8PT zaDZgYg){fSCtQI+cY6IP^#Cz8Sqf*B4g;Hd8#ZcrZTwFncom~wG~HmIx_EHOiAUF7 zLvkV*hKyg+&R1+Lc_-TmP?*0dI1lN%7{8U zaEvfw3;nlDuL6LF=%7~u#XxGM380s{t+-zyR2nTp!cArDpO>WV2Q%S~R z_H$NuLlHJY1Cowg`#YQid7GflP+rU6-4)6?dt#tI9 z4Cf($5^D;rN1{LjpBSs|(W-q;1b9}a!;Jt85A?fpHNl>VZ6#NhY3HS=9u_vvoe;R1%c6V=_Iq#*J zeTMZU)ZY6Xq6koaiy)ZebnCmOnR7g6Kbnn3vR(v(E_Y(}No>bVE{>+W;5dL3x%Uu~ z(?|X15287fN3Tj2L^ONeAB4kcbq=Pw`#fg`MxGnNb>dJfy`66^^38MMfWE<-$tY*Q ztR;4*SN%dhyz-nD$tB(i_(A&|bUMW#gx5YtA19q@+V0=G7YF!&Ybh)CVmg`G3RNqp zO6xTy!S934F-+!#rX>wdFKjhj5mE-0jl^8{>@~l1q39!h;%dqJcjw|jOWJ5Dp%3Dg zTZ;txY1f@%cjPb){a9BS4^3QE6YY& z)pg1+Wia#g0RZwDIZ0u6fm+>7?2PB>;_O?0 zlG_p3*chIBX<=gI9Fy&CM`sqeGarDh5ghd&uq<>g*Y);V?yYL!u|cQ- zy;n<(!s60+qhkPd1)a)vfxdxfH{%57q4qFcJ5R#|c!|q!TtgQ_>m&fX%^|DE-5GQO zo&)oS*~LMQjIOopKGo&Ooi!q%iU^ngtV1gG&S;EN0HV6_-QvPWY9@s z<>{1s`9oCe9nPFc9l@odjupRt%dC^yFQWdfqys|;9oa!4K-o}U?S%y*gn8G__86Wpikl=wF`AOq(aNQT^9NlC;LDBvWc{~q<$gt~LnRa0Adsw3 zF?TDytZt)0j7I{74L+GfqLx7K=++0FwzOUtt~{e-t%h#1bA&~Z%f*#cks1#aNo=pd ztz#az`!Shh0{ea$bsKQ|VqVN?8R%c{RXl8V@HFyRIgbzK?D=AA;A0h|b}$RIMHE_8MjOGqQC6wgj;@RX`hr!nRw>~|#F~;Uk$b~s zE_d_3Nc2yN3KkPU=&Dnzhpo^BWgSY>yaTy=JBEpQGiIc*JK>x$V>ochW@IqkNMHyG z8DfN5i`v`rlQWYcD4}MmMp^wYmOdXrbC7cz;_{9b&YC=Dy7J{2W&R}(8-M+J?wgx` z!3L)iB6c~&3CN20x$(zb7a#0#xD#<=p9DCAY z0<^qG2vAhyD)R{vkH^2TrB~9Ou^Ry{gaptS0e8-!q##owaTlx>(XJ~V zrWKyP$kOD}nZSQcTA@%A6bqUup!riW02K`e2v~cfbI%|UdImhHt{bImD+AKR7c5%f z|IcKgR3?o&EVaO944NQ@Y~xPInS`fY2QHXWIwyQ>*X*05*K=;|;-1rzae*V-W_&Gv z^Uvyk>m%`tH{5)81sBH3zu*A_<@{rZME8lrtc^&I@ZZ1&%PfBRJi8nvApkQJ9obci zu!y)CQtYUPvt%?nYC%Nanu*o8>=3(L<_VklU@w`h04}vsDr>-SEwg$x5x#r^zzf~Z z=XM-08fM!tEe!vZouUf7$V)(g6sYLxVp%;~1UK)@0-^Ilm#x>D#n@8@UD$3A)0*Ds zzE6jkm2hC&5Nn01>%#}RGPlKWtjzuoS)=E4HbuAF`WK5>kpC-T_aCpfaccq&b_QVq zWlx5KC^AR8^2HKAdZ}n{i{lvWofwjSx7s({Y>5ZxU}YN;5>ln$`dqKEsn#96`Q8xM z39&%o?JRI-BPc4?K?!4B`*L&3o5Pdp+LS%LV-rG4Bgu|7;}sg1&W>BTxVfuolGk+n z*{qrwrTT3Pe$jSjP7lwR@#((RN9Za>tQ|BCnCo3>Xw@hCJ5morECr zy6T`o=i;YXT5*_~4kW;3(jnz;CtnK{9_#x&oGITabG+*+j-Y`@?zE~j3);FpGe#m! zNvj$?1V-yfW^0iS39vLSt(PZgHk9qvS1{{K)$L@!cND2BdyE`Fb7#`&H}t!i87c%2 z0@f6_V`wh1f)h|9fL7eSo|Oxsft#q`V=l#ErcwqxC+O$Nm5Cp)fkd8@)U2tii^|f1aJLQw7H$ShKYo3Vo0}_JpJ>p9b0ppQHFJiIKp==vFeR0wxri5 zPbF+jvX!(?`4qF?T87z=Qs$Hvn~rDW{KO|jj8d>Ecg18{wM5y_>N2USh3`|JNR@@a zUjG0mn5}!%e}CmS*M=s59cX1iLvguG*2go78B8y;TPBn%y#}wL!O&nW!?jegd=88w z#_^!nR8=N8Giz|bfl`+m-$x>ROdo~-{k+yo+YV3#+EDns5wo!fLW7JhvRQH5&NxfF z1~D-!LaG^kJ^2|F0^KWBGhGEDx|EvJ%?hh1`3Mk7g2ckHYYHF<_MdUYT>Yw6W6K}r zfMl(R6M$;8J3Hgkx^bQ^|4~(S0B6rHF8oI~x!q~G5vYE53e{C`S^;j)ZJa~NiA|PK ze4XG?g#`MEd3&sD{YsPTA3>ouQ*VW*u@)aVM1T76X$DiY|3+)Zw@WTAej3O=AHV;_ zq9XO&5liw5Yu4ZYEk?iemB?v+&u4)9@K~ySKFM1d6%o<&5hmXZlSwJo`FuW#?yHuBji!oal(ZW2JnrV|-A5eZcgM(-rEJ00RtYs8jF zA_@%JO8HJcs?~`IT@T*6&ckhieOAGYx z035Jbb?mSlhE*Cgs$nf46~wMS>kF*+p3umX5fH(qrJ9}kqF~-gD%C5k48@n4n0Qc% z##T!QP=qq8ZsD4laAZj^Gv_NCz#Pw9ED}cIbQ6(S(9#HkO*8i(j0r*?Rj2BD3=^3w z&^%Tuy^z&x!Tq%u-0Gb+eHU>{^{#JeZ&qIV_S+4&aX0(pe~sQ+IriGsq3@{J4kRn< zCk?3dXZA>6;hv~8&8V;WLX}t}Vv33;X47w&Jppn^E06>{`KLF_@ZGeM%AM+ z_i7IT*i|5Lp7nz_^y;6Qn$cAhaISgU^u@cUrk`kfs@;?%{*yoefKP z0^9b3?zbV0v0t#>S;e{n*l8?EzPsu<8l;IssZ7y`5Rt0sA>pG6JUFi%Zp&awD7K7l z2_dIoHUtyLx@;99eoTOb0+3M(_hB5Kv-r~1z%>BsU|!;Fo(Y^`BpBoB7H1+sQ`)G& zX)hJxFm0+83Q8h%cDy`hO;Lh3Lh0C!!82u6)CpUY)=Wwr-)USII1`V1mwWFG@$S?e zg^{b4;T6#-0+i^NUl@d=TRQO5!KWWS1G%Xj=*J);X2RrlsUIDr{SoAr3){ZjKQQp2 z^=!$mOd_;zj{3+sLdjVHF&a6?uoX*?>XXG>@=MfH&hKICnj|O&_rv% zKtd){G2%2#+oAQdXLyDk`pmex3xfuLpg(PE%;V-$fRG4c6KU@~&uKCJ=w~_|_Y^ds zn5Tn*0zi2uL2L8xBE(hi636lGk8O$glerClW@H<7Y)|mS+XqU{671RF*xS=to)=Ow zxnsXGR!iOzB<|ZCA(6m6RZ#p_GlRwrvn@JVP^**$I6>bCJM zYg<6H_gN=lPd^wz3X!DR^ai-kSCWZD`vH zd9a{!0z2x|Iku~Upin=?1>A*lpQPUzlf+>l^cjuA{;QDtBdEMb-c&HvjI4Rzux6nx zvy3A>L>zR4)NpAIOeSEKdz)gSz757Rdx&)Ww9L=~+66F+M2$Ywq^3GWFb(cN9#NKNSkMp0040nR$g(Qh!&y>y|wS0i=BrX6Zd zyOCEJZ>jC-^)#(|qyM|NmMvI={Q`*i{K}ICwV&3v`{o(42BuO4?Cb~>`DtD1!=Vqu zzXnZjry+V-9f8Kht5VKmn6otJCR7a29&^~{!&u&UKa@@11_c4@<3Z%8-5Mr)vj76x zGL*>?fcJ)v4K{1dpKVY6{EPA9%Zmuu4`w1r+pb3`=WYOf3R%-ZTtEXyEniFC4E}*h zPiI}7#+yU^xcU$BQrzF>@_=axrE#iq-GzlQK}bQ+Hj=nSKb0+tlE(kR-MT1;TmMEq zJGBVGCq%-M{##qw42!ia%hPTgdrym9keW1<7yHKQM!V1j(|?q2&IB>n|I3azn*D*PS+R6)YZ^fzQTTXf zpYuGV$(`&QR&}|CsUY~5xkpRm(GCt6g_TCBI!>qOpG13fC|0f0O`2LbwUgHZ!Wd5n z>Xq3~A;?NP+86Mo3{W0JLOH{KXr_e)5$L*i20kF1_a(E+v*0&|_)kcFhf6J`NJ#Ax z6Ew(mGuBAwq5}WC^EkaSi4{NdBt38&hhzc~!1QaWUONdyFlhfvKH!H@coy(AK|VmM z0G)TgEslm{!_x8i@fXK=(yFd3kiThpj@*z6Q!O&6-AFoKw8mey$)?HA4G2e45n+(0$xng(CnR*s*v{6dlbk2iQZM^%;Qh5L)&>jx)aX_nVXwaCthmABps)f3X5?A(Gj^eDaH%x|nO`lWT zXDzZO0&ZGS5iDf$N+-nj_f+mabO`tpY@W*rNnjdroqj5O%)r<6K^h9;n!kqpB{w1A zu#i)p(nztyhvrHKNjH+Yi6u22E*@NnCc_?v^xD4j+|4IGPoHc8T4E5yfB=|A{tN6X z`MT!v@|MXxyJoiEGnIV(@5_sxPRjmlzfxEw%9<(*H7B*{0ocHKI>0mysp3uL( z_=V+Bu~Eq`o5 z1bpd^W(1)t^4&9yT)^wq`C41BB1mH5!YXO863FGT1XV?vOosR+)Y}B0J?4ylHsv-7O#SCezrTI^DdB`2YB5Bag;PXgkEi9p0I**J3qBU8!=cdiYo|QOO+#_&jQ;B3QBOH0f1-6;DP!xHoQ)n3?WJ}jH%1(;Wp7xInL2) z9PaWG2wDE>iO!yD0I7^YUqVm>-)BqT4(6iqldj?X`lXY(aJ5-D#<`MdnTwZerUZO5 z*!6E-T2OC#qrnx8tzIQ$f5WOfV^ghY>%6+CB%E!}OF*l>_DG`g;;O2nMiX?D_>r`E zU9Y|+L~kHB|;^m7J99y^=#0njvz4m41BCCiqygq276lJ z2%9CCs_-+CEb+a)Xzljv)>^Gs>M?mdJu6_zj%r7^)J*l1G-quhK=tM_=M|Qb1IxrA z1n7b|5PfQUST3UEz|u$hl(i)arQfs{_5fRp^;PJ)C?$1Af;1a|UqB#BmShu=6}XbI zxy(^m0KXiw`&~Da!Y6+9US3Tz0`dT|d)bZVbiqgN`AVDVgR|J=qPG6!np!J_vuV@U zU*7Uz{>#Znn$LJ@SDoL`BKrL7%DbLzx(EHNrBi>iUuZ-f%^%Q8I+i&LS3q{KVX<(D z_QTxURJ5K$YqX^O@G|%$|6taZ4s3$~9v!6eotl8IobK`(8~B)0Y*t3;89)d+nM~$R zipOh$1R4xyKvC@Gs9d~Jr;H&%C@(AuYFy?9@^g(EC_MYh${elD?C{7nAp>z44kYp+ zgqXFXrFAr9l*RC8UL(LbDG19MHp#Ie9fiRM#&N5EXgG# z61OMvHm>HQueW!S;&X#VvmjvxOUwBX|vhbHQFyk=#ws~8_%G%WN`E_`kr=DT(8@My3H_|`azj*6Ib9Jx$G&g_NJM;Yq z#9!sc`Y0u5fn-jV%`I$W?)+d$f=~Af!Y@07hTns%H|Dq3pZX z+F>0I8J54q0uSMDA_yS4qQ{QHzBy~RgQH$|rZ9bE6h};Mc_|k#5%nH2xQ%%B)_VC1 znI&%A4|ro&Oj%U7*eRocw?JM_X~Azj3BXZPByNwi16+Z>?9G_;MT4&uL`br6R?YYS z0c{1C613#w?wqtB`r9en3dk1mN2i$Z-2eJL`CqLddg%XNNVu8*bOI(M0f2w;o32k5 OM>tYeKNEb(2LA)EA_kHG From f4c723ca7e71bb08ec91a76ab9add4cc1853e992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20N=C3=B8rup?= Date: Tue, 7 Nov 2023 12:02:21 +0100 Subject: [PATCH 4/7] Added better results screen to custom battle --- .../modulemon/BattleScene/BattleResult.java | 9 +++- .../modulemon/BattleScene/BattleView.java | 6 ++- .../CommonBattleClient/IBattleResult.java | 1 + .../CustomBattleView/CustomBattleScene.java | 2 +- .../CustomBattleView/CustomBattleView.java | 51 +++++++++++-------- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleResult.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleResult.java index e55a4d8c..962f665c 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleResult.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleResult.java @@ -7,11 +7,13 @@ public class BattleResult implements IBattleResult { private IBattleParticipant winner; private IBattleParticipant player; private IBattleParticipant enemy; + private int turnCount; - public BattleResult(IBattleParticipant winner, IBattleParticipant player, IBattleParticipant enemy) { + public BattleResult(IBattleParticipant winner, IBattleParticipant player, IBattleParticipant enemy, int turnCount) { this.winner = winner; this.player = player; this.enemy = enemy; + this.turnCount = turnCount; } @Override @@ -28,4 +30,9 @@ public IBattleParticipant getPlayer() { public IBattleParticipant getEnemy() { return enemy; } + + @Override + public int getTurns() { + return turnCount; + } } 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 8fabb187..1afcc615 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java @@ -47,6 +47,7 @@ public class BattleView implements IGameViewService, IBattleView { private boolean _isInitialized; private boolean _battleStarted; private IBattleCallback _battleCallback; + private int _numTurns; private IBattleSimulation _battleSimulation; private IBattleState _currentBattleState; private BattleScene _battleScene; @@ -131,6 +132,7 @@ public void startBattle(List playerMonsters, List enemyMonst setBattleAIFactory(); _battleSimulation.StartBattle(player, enemy); _currentBattleState = _battleSimulation.getState().clone(); // Set an initial battle-state + _numTurns = 0; _battleCallback = callback; _battleMusic.play(); _battleMusic.setLooping(true); @@ -174,7 +176,7 @@ public void forceBattleEnd() { public void handleBattleEnd(VictoryBattleEvent victoryBattleEvent) { if (_battleCallback != null) { - _battleCallback.onBattleEnd(new BattleResult(victoryBattleEvent.getWinner(), _battleSimulation.getState().getPlayer(), _battleSimulation.getState().getEnemy())); + _battleCallback.onBattleEnd(new BattleResult(victoryBattleEvent.getWinner(), _battleSimulation.getState().getPlayer(), _battleSimulation.getState().getEnemy(), _numTurns)); } else { gameViewManager.setDefaultView(); } @@ -314,6 +316,7 @@ public void update(GameData gameData, IGameViewManager gameViewManager) { if (battleEvent != null) { IBattleState eventState = battleEvent.getState(); if (battleEvent instanceof MoveBattleEvent) { + this._numTurns++; _currentBattleState = eventState; MoveBattleEvent event = (MoveBattleEvent) battleEvent; if (event.getUsingParticipant().isPlayerControlled()) { @@ -342,6 +345,7 @@ public void update(GameData gameData, IGameViewManager gameViewManager) { this._battleScene.setTextToDisplay(event.getText()); } else if (battleEvent instanceof ChangeMonsterBattleEvent) { + this._numTurns++; ChangeMonsterBattleEvent event = (ChangeMonsterBattleEvent) battleEvent; boolean causedByFaintingMonster = event instanceof MonsterFaintChangeBattleEvent; if (event.getParticipant().isPlayerControlled()) { diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleResult.java b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleResult.java index db7d51ac..e6c7b082 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleResult.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleResult.java @@ -6,4 +6,5 @@ public interface IBattleResult { IBattleParticipant getWinner(); IBattleParticipant getPlayer(); IBattleParticipant getEnemy(); + int getTurns(); } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleScene.java b/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleScene.java index 9442b76b..3fd83577 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleScene.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleScene.java @@ -179,7 +179,7 @@ public void draw(GameData gameData) { var topOfResults = resultContainer.getY() + resultContainer.getHeight(); var headerY = topOfResults - 50; var textY = headerY - 60; - var lineHeight = 25; + var lineHeight = 30; var textX = resultContainer.getX() + 50; text.setCoordinateMode(TextUtils.CoordinateMode.CENTER); text.drawTitleFont(spriteBatch, resultsHeader, Color.BLACK, gameData.getDisplayWidth() / 2f, headerY ); diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java b/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java index 8c2b2e94..7f99ec5b 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.TimeUtils; import dk.sdu.mmmi.modulemon.CommonBattleClient.IBattleView; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAIFactory; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleSimulation; @@ -59,7 +60,7 @@ public void init(IGameViewManager gameViewManager) { private int cursorPosition = 0; private boolean editingMode = false; private boolean redrawRoulettes = true; - private boolean showingResults = false; + private boolean showingResults = false; @Override @@ -67,9 +68,9 @@ public void update(GameData gameData, IGameViewManager gameViewManager) { cursorPosition = MathUtils.clamp(cursorPosition, 0, 14); scene.setShowResults(showingResults); var nextAnimation = backgroundAnimations.peek(); - if(nextAnimation != null){ + if (nextAnimation != null) { nextAnimation.update(gameData); - if(nextAnimation.isFinished()){ + if (nextAnimation.isFinished()) { backgroundAnimations.poll(); } } @@ -139,7 +140,7 @@ public void handleInput(GameData gameData, IGameViewManager gameViewManager) { } if (gameData.getKeys().isPressed(GameKeys.ACTION)) { - if(showingResults){ + if (showingResults) { showingResults = false; return; } @@ -148,7 +149,7 @@ public void handleInput(GameData gameData, IGameViewManager gameViewManager) { chooseSound.play(getSoundVolume()); } - if(showingResults){ + if (showingResults) { return; // Don't allow any movement when showing results } @@ -206,14 +207,14 @@ public void handleInput(GameData gameData, IGameViewManager gameViewManager) { } } - private void startBattle(IGameViewManager gameViewManager){ + private void startBattle(IGameViewManager gameViewManager) { var teamA = Arrays.stream(getMonsterArray(selectedTeamAIndicies)).filter(Objects::nonNull).toList(); var teamB = Arrays.stream(getMonsterArray(selectedTeamBIndicies)).filter(Objects::nonNull).toList(); var teamAAI = getSelectedAI(selectedTeamAAI); var teamBAI = getSelectedAI(selectedTeamBAI); - if(teamA.isEmpty() || teamB.isEmpty() || teamBAI == null){ + if (teamA.isEmpty() || teamB.isEmpty() || teamBAI == null) { wrongSound.play(1); var anim = new ErrorTextAnimation(scene, "Add some monsters to both teams you dork!"); anim.start(); @@ -227,19 +228,27 @@ private void startBattle(IGameViewManager gameViewManager){ gameViewManager.setView(battleView.getGameView(), false); // Do not dispose the map customBattleMusic.stop(); + long startTime = TimeUtils.millis(); battleView.startBattle(teamA, teamB, result -> { customBattleMusic.play(); gameViewManager.setView(this); boolean teamAWon = result.getWinner() == result.getPlayer(); var winnerTeamName = teamAWon ? - (teamAAI == null ? "You" : teamAAI.toString()) - : (teamBAI.toString()); - scene.setResultsHeader("The winner is: " + winnerTeamName +"!"); - scene.setResultLines(new String[]{ - "TODO: Collect some stats from the battle and display them here", - "On multiple lines", - "Wow! This is amazing!!" - }); + (teamAAI == null ? "You" : teamAAI.toString()) + : (teamBAI.toString()); + scene.setResultsHeader("The winner is: " + winnerTeamName + "!"); + + var resultLines = new ArrayList() {{ + add(String.format("Total turns: %d", result.getTurns())); + add(String.format("Battle time: %.2f seconds", (TimeUtils.timeSinceMillis(startTime) / 1000f))); + add(String.format("The winning team ended up as so:")); + }}; + + for (var monster : result.getWinner().getMonsterTeam()) { + resultLines.add(String.format(" - %s", monster)); + } + + scene.setResultLines(resultLines.toArray(new String[resultLines.size()])); cursorPosition = 0; showingResults = true; editingMode = false; @@ -247,7 +256,7 @@ private void startBattle(IGameViewManager gameViewManager){ // Set the battle AI after the startBattle method to override the configs. battleSimulation.setOpponentAIFactory(teamBAI); - if(teamAAI != null) { + if (teamAAI != null) { battleSimulation.setPlayerAIFactory(teamAAI); } } @@ -257,15 +266,15 @@ private void addToSelectedIndicies(Integer a) { if (cursorPosition < 12) { var numMonsters = this.monsterRegistry.getMonsterAmount() - 1; if (isTeamA(cursorPosition)) { - selectedTeamAIndicies[getGridAdjustedCursor(cursorPosition)] = scrollIndexWithNull(selectedTeamAIndicies[getGridAdjustedCursor(cursorPosition)],a , numMonsters); + selectedTeamAIndicies[getGridAdjustedCursor(cursorPosition)] = scrollIndexWithNull(selectedTeamAIndicies[getGridAdjustedCursor(cursorPosition)], a, numMonsters); } else { - selectedTeamBIndicies[getGridAdjustedCursor(cursorPosition)] = scrollIndexWithNull(selectedTeamBIndicies[getGridAdjustedCursor(cursorPosition)],a, numMonsters); + selectedTeamBIndicies[getGridAdjustedCursor(cursorPosition)] = scrollIndexWithNull(selectedTeamBIndicies[getGridAdjustedCursor(cursorPosition)], a, numMonsters); } } else if (cursorPosition == 12) { selectedTeamAAI = scrollIndexWithNull(selectedTeamAAI, a, this.battleAIFactoryList.size() - 1); } else if (cursorPosition == 13) { var newBValue = scrollIndexWithNull(selectedTeamBAI, a, this.battleAIFactoryList.size() - 1); - if(newBValue == null){ + if (newBValue == null) { newBValue = selectedTeamBAI == 0 ? this.battleAIFactoryList.size() - 1 : 0; } selectedTeamBAI = newBValue; @@ -278,9 +287,9 @@ private Integer scrollIndexWithNull(Integer input, Integer a, int maxValue) { } if (input == null) { - if (a > 0){ + if (a > 0) { return 0; - }else{ + } else { return maxValue; } } From f6d2a79d2a3b980149b2982752ecba731febf0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20N=C3=B8rup?= Date: Tue, 7 Nov 2023 12:23:40 +0100 Subject: [PATCH 5/7] Added a randomize team command --- .../CustomBattleView/CustomBattleView.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java b/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java index 7f99ec5b..78525f1e 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java @@ -32,6 +32,8 @@ public class CustomBattleView implements IGameViewService { private Queue backgroundAnimations; private CustomBattleScene scene; + private Random random = new Random(); + // LibGDX Sound stuff private Sound selectSound; private Sound chooseSound; @@ -139,6 +141,25 @@ public void handleInput(GameData gameData, IGameViewManager gameViewManager) { } } + if (gameData.getKeys().isDown(GameKeys.K) && cursorPosition < 12) { + // Shuffle random monsters + if (gameData.getKeys().isDown(GameKeys.LEFT_CTRL)) { + // If holding CTRL, randomize both teams + for (int i = 0; i < selectedTeamAIndicies.length; i++) { + selectedTeamAIndicies[i] = random.nextInt(monsterRegistry.getMonsterAmount()); + selectedTeamBIndicies[i] = random.nextInt(monsterRegistry.getMonsterAmount()); + } + } else { + if (isTeamA(cursorPosition)) { + selectedTeamAIndicies[getGridAdjustedCursor(cursorPosition)] = random.nextInt(monsterRegistry.getMonsterAmount()); + } else { + selectedTeamBIndicies[getGridAdjustedCursor(cursorPosition)] = random.nextInt(monsterRegistry.getMonsterAmount()); + } + } + redrawRoulettes = true; + chooseSound.play(getSoundVolume()); + } + if (gameData.getKeys().isPressed(GameKeys.ACTION)) { if (showingResults) { showingResults = false; @@ -154,7 +175,16 @@ public void handleInput(GameData gameData, IGameViewManager gameViewManager) { } if (gameData.getKeys().isPressed(GameKeys.DELETE) && cursorPosition < 13) { - addToSelectedIndicies(null); + if (gameData.getKeys().isDown(GameKeys.LEFT_CTRL)) { + // If holding CTRL, empty both teams + for (int i = 0; i < selectedTeamAIndicies.length; i++) { + selectedTeamAIndicies[i] = null; + selectedTeamBIndicies[i] = null; + } + } else { + addToSelectedIndicies(null); + } + redrawRoulettes = true; editingMode = false; chooseSound.play(getSoundVolume()); } From d63dc5a09ba292eafe4385b4096785c8e8102622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20N=C3=B8rup?= Date: Tue, 7 Nov 2023 13:46:32 +0100 Subject: [PATCH 6/7] Now deciding move on when to win instesd of best-child method --- .../modulemon/MCTSBattleAI/MCTSBattleAI.java | 21 +++++++++++++++++-- .../sdu/mmmi/modulemon/MCTSBattleAI/Node.java | 15 ++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) 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 0dec5397..6bcbed45 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -11,6 +11,7 @@ import dk.sdu.mmmi.modulemon.common.services.IGameSettings; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Random; import java.util.stream.Stream; @@ -77,7 +78,9 @@ public void doAction() { backpropagation(newNode, reward); } - var bestChild = bestChild(rootNode, 0); + var minTurnTillWin = rootNode.getChildren().stream().map(Node::getTurnsTillWin).min(Integer::compare).orElse(Integer.MAX_VALUE); + var bestChild = rootNode.getChildren().stream().filter(x -> x.getTurnsTillWin() <= minTurnTillWin) + .max((a,b) -> Float.compare(a.getReward(), b.getReward())).orElseThrow(); System.out.println(String.format("Expanded %d nodes, based on %d simulated actions in %dms", rootNode.getTimesVisited(), this.numSimulatedActions, ((System.nanoTime() - startTime) / 1000000))); System.out.println(explainNodeOptions(rootNode)); @@ -157,9 +160,23 @@ private void backpropagation(Node node, float reward) { if (Float.isNaN(reward)) { throw new IllegalArgumentException("Reward must be a number"); } + + if(!node.getParticipant().equals(this.participantToControl) && isTerminal(node.getState(), true)){ + // This is a final node where this AI wins. + // Let's take note of that + node.setTurnsTillWin(0); + } + do { node.incrementTimesVisited(); node.setReward(node.getReward() + reward); + if(!node.getChildren().isEmpty()){ + node.setTurnsTillWin(node.getChildren().stream() + .map(Node::getTurnsTillWin) + .filter(x -> x != Integer.MAX_VALUE) + .min(Integer::compare) + .orElse(Integer.MAX_VALUE - 1) + 1); + } node = node.getParent(); } while (node != null); } @@ -383,7 +400,7 @@ private boolean isTerminal(IBattleState battleState, boolean useKnowlegdeState) // Check if all the opposing participant's (known) monster are dead boolean allEnemyMonstersDead = opposingParticipant.getMonsterTeam().stream() - .filter(x -> !useKnowlegdeState || knowledgeState.getEnemyMonsters().contains(x)) //only consider monsters we've seen + .filter(x -> !useKnowlegdeState || knowledgeState.hasSeenMonster(x)) //only consider monsters we've seen .allMatch(x -> x.getHitPoints() <= 0); return allEnemyMonstersDead || allOwnMonstersDead; diff --git a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java index 6129a41a..1d198d94 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java @@ -16,6 +16,7 @@ public class Node { private IMonster parentSwitch = null; private float reward = 0; private int timesVisited = 0; + private int turnsTillWin = Integer.MAX_VALUE; public Node(IBattleState state, IBattleParticipant participant) { // Constructor used when creating root node this.state = state; @@ -84,6 +85,18 @@ public void setReward(float reward) { this.reward = reward; } + public void setTimesVisited(int timesVisited) { + this.timesVisited = timesVisited; + } + + public int getTurnsTillWin() { + return turnsTillWin; + } + + public void setTurnsTillWin(int turnsTillWin) { + this.turnsTillWin = turnsTillWin; + } + public int getTimesVisited() { return timesVisited; } @@ -113,7 +126,7 @@ public String toString() { }else if(this.parentSwitch != null){ action = "Switching to " + this.parentSwitch; } - return String.format("%s (Reward: %.8f)", action, this.reward); + return String.format("%s (Reward: %.8f; Winning in %d turns)", action, this.reward, this.getTurnsTillWin()); }else{ return "Root node"; } From 320ffdc53d08c70ec12f82d1d94edfc1941b8d3a Mon Sep 17 00:00:00 2001 From: VictorABoye Date: Tue, 14 Nov 2023 09:31:12 +0100 Subject: [PATCH 7/7] Revert "Now deciding move on when to win instesd of best-child method" This reverts commit d63dc5a09ba292eafe4385b4096785c8e8102622. --- .../modulemon/MCTSBattleAI/MCTSBattleAI.java | 21 ++----------------- .../sdu/mmmi/modulemon/MCTSBattleAI/Node.java | 15 +------------ 2 files changed, 3 insertions(+), 33 deletions(-) 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 6bcbed45..0dec5397 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -11,7 +11,6 @@ import dk.sdu.mmmi.modulemon.common.services.IGameSettings; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Random; import java.util.stream.Stream; @@ -78,9 +77,7 @@ public void doAction() { backpropagation(newNode, reward); } - var minTurnTillWin = rootNode.getChildren().stream().map(Node::getTurnsTillWin).min(Integer::compare).orElse(Integer.MAX_VALUE); - var bestChild = rootNode.getChildren().stream().filter(x -> x.getTurnsTillWin() <= minTurnTillWin) - .max((a,b) -> Float.compare(a.getReward(), b.getReward())).orElseThrow(); + var bestChild = bestChild(rootNode, 0); System.out.println(String.format("Expanded %d nodes, based on %d simulated actions in %dms", rootNode.getTimesVisited(), this.numSimulatedActions, ((System.nanoTime() - startTime) / 1000000))); System.out.println(explainNodeOptions(rootNode)); @@ -160,23 +157,9 @@ private void backpropagation(Node node, float reward) { if (Float.isNaN(reward)) { throw new IllegalArgumentException("Reward must be a number"); } - - if(!node.getParticipant().equals(this.participantToControl) && isTerminal(node.getState(), true)){ - // This is a final node where this AI wins. - // Let's take note of that - node.setTurnsTillWin(0); - } - do { node.incrementTimesVisited(); node.setReward(node.getReward() + reward); - if(!node.getChildren().isEmpty()){ - node.setTurnsTillWin(node.getChildren().stream() - .map(Node::getTurnsTillWin) - .filter(x -> x != Integer.MAX_VALUE) - .min(Integer::compare) - .orElse(Integer.MAX_VALUE - 1) + 1); - } node = node.getParent(); } while (node != null); } @@ -400,7 +383,7 @@ private boolean isTerminal(IBattleState battleState, boolean useKnowlegdeState) // Check if all the opposing participant's (known) monster are dead boolean allEnemyMonstersDead = opposingParticipant.getMonsterTeam().stream() - .filter(x -> !useKnowlegdeState || knowledgeState.hasSeenMonster(x)) //only consider monsters we've seen + .filter(x -> !useKnowlegdeState || knowledgeState.getEnemyMonsters().contains(x)) //only consider monsters we've seen .allMatch(x -> x.getHitPoints() <= 0); return allEnemyMonstersDead || allOwnMonstersDead; diff --git a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java index 1d198d94..6129a41a 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java @@ -16,7 +16,6 @@ public class Node { private IMonster parentSwitch = null; private float reward = 0; private int timesVisited = 0; - private int turnsTillWin = Integer.MAX_VALUE; public Node(IBattleState state, IBattleParticipant participant) { // Constructor used when creating root node this.state = state; @@ -85,18 +84,6 @@ public void setReward(float reward) { this.reward = reward; } - public void setTimesVisited(int timesVisited) { - this.timesVisited = timesVisited; - } - - public int getTurnsTillWin() { - return turnsTillWin; - } - - public void setTurnsTillWin(int turnsTillWin) { - this.turnsTillWin = turnsTillWin; - } - public int getTimesVisited() { return timesVisited; } @@ -126,7 +113,7 @@ public String toString() { }else if(this.parentSwitch != null){ action = "Switching to " + this.parentSwitch; } - return String.format("%s (Reward: %.8f; Winning in %d turns)", action, this.reward, this.getTurnsTillWin()); + return String.format("%s (Reward: %.8f)", action, this.reward); }else{ return "Root node"; }