diff --git a/src/main/java/dk/sdu/mmmi/Main.java b/src/main/java/dk/sdu/mmmi/Main.java index 11d76c76..848cd493 100644 --- a/src/main/java/dk/sdu/mmmi/Main.java +++ b/src/main/java/dk/sdu/mmmi/Main.java @@ -32,7 +32,9 @@ public static void main(String[] args) throws IOException, URISyntaxException { var battleMonsterProcessor = new BattleMonsterProcessor(); var battleAI = new dk.sdu.mmmi.modulemon.BattleAI.BattleAIFactory(); + battleAI.setSettingsService(settings); var mctsBattleAI = new MCTSBattleAIFactory(); + mctsBattleAI.setSettingsService(settings); var simpleBattleAI = 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/Battle/BattleParticipant.java b/src/main/java/dk/sdu/mmmi/modulemon/Battle/BattleParticipant.java index 13052d0e..8675d850 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/Battle/BattleParticipant.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/Battle/BattleParticipant.java @@ -78,4 +78,9 @@ public boolean equals(Object obj) { } return false; } + + @Override + public String toString() { + return String.format("Participant with %s (UUID: %s...)", this.activeMonster.toString(), this.uuid.toString().substring(0,5)); + } } 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 e7d6f7ba..8fabb187 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java @@ -53,6 +53,7 @@ public class BattleView implements IGameViewService, IBattleView { private Music _battleMusic; private Sound _winSound; private Sound _loseSound; + private Sound _crashSound; private MenuState menuState = MenuState.DEFAULT; private BattleSpeedController forcedAIDelay; private Queue blockingAnimations; @@ -126,6 +127,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()); + _crashSound = loader.getSoundAsset("/sounds/metal-pipe.ogg", this.getClass()); setBattleAIFactory(); _battleSimulation.StartBattle(player, enemy); _currentBattleState = _battleSimulation.getState().clone(); // Set an initial battle-state @@ -149,6 +151,7 @@ private void setBattleAIFactory() { IBattleAIFactory desiredAI; if (settings.getSetting("AI").equals("MCTS")) { desiredAI = new MCTSBattleAIFactory(); + ((MCTSBattleAIFactory) desiredAI).setSettingsService(settings); } else if (settings.getSetting("AI").equals("Simple")) { desiredAI = new dk.sdu.mmmi.modulemon.SimpleAI.BattleAIFactory(); } else { @@ -206,7 +209,6 @@ public void setBattleScene(BattleScene battleScene) { this._battleScene = battleScene; } - @Override public void update(GameData gameData, IGameViewManager gameViewManager) { if (!_isInitialized || _battleSimulation == null || !_battleStarted) { @@ -253,18 +255,18 @@ public void update(GameData gameData, IGameViewManager gameViewManager) { updateHasRunOnce = true; // Check the current AI is done thinking - if(_battleSimulation.hasNextBattleEvent()){ + if (_battleSimulation.hasNextBattleEvent()) { _battleScene.setShowEnemySpinner(false); _battleScene.setShowPlayerSpinner(false); - }else{ - if(_battleSimulation.isPlayerControlledByAI() && _battleSimulation.getState().isPlayersTurn()){ + } else { + if (_battleSimulation.isPlayerControlledByAI() && _battleSimulation.getState().isPlayersTurn()) { // there is no current event, and the player is controlled by an AI _battleScene.setShowPlayerSpinner(true); _battleScene.setShowEnemySpinner(false); - }else if(!_battleSimulation.getState().isPlayersTurn()){ + } else if (!_battleSimulation.getState().isPlayersTurn()) { _battleScene.setShowPlayerSpinner(false); _battleScene.setShowEnemySpinner(true); - }else { + } else { _battleScene.setShowEnemySpinner(false); _battleScene.setShowPlayerSpinner(false); } @@ -283,10 +285,10 @@ public void update(GameData gameData, IGameViewManager gameViewManager) { currentAnimation.start(); } - if(forcedAIDelay.getSpeed() <= 0){ + if (forcedAIDelay.getSpeed() <= 0) { currentAnimation.forceEndAnimation(); - }else if(currentAnimation.getAnimationLength() > forcedAIDelay.getSpeed() - && menuState == MenuState.SPECTATOR){ // We only rescale animations while spectating + } else if (currentAnimation.getAnimationLength() > forcedAIDelay.getSpeed() + && menuState == MenuState.SPECTATOR) { // We only rescale animations while spectating // While we can re-scale all animations (even to be longer), we only // scale animations that are longer than our forcedAIDelay. currentAnimation.rescaleAnimation(forcedAIDelay.getSpeed()); @@ -358,7 +360,7 @@ public void update(GameData gameData, IGameViewManager gameViewManager) { PlayerChangeInAnimation changeInAnimation = new PlayerChangeInAnimation(_battleScene); blockingAnimations.add(changeInAnimation); - if(forcedAIDelay.getSpeed() > 0) { + if (forcedAIDelay.getSpeed() > 0) { addEmptyAnimation(forcedAIDelay.getSpeed(), false); } this._battleScene.setTextToDisplay(battleEvent.getText()); @@ -409,10 +411,24 @@ public void update(GameData gameData, IGameViewManager gameViewManager) { e.start(); blockingAnimations.add(e); } + } else if (battleEvent instanceof AICrashedEvent crashEvent) { + BaseAnimation anim; + if (crashEvent.getParticipant().equals(_battleSimulation.getState().getPlayer())) { + anim = new PlayerCrashAnimation(_battleScene); + } else { + anim = new EnemyCrashAnimation(_battleScene); + } + + this._crashSound.play((int) settings.getSetting(SettingsRegistry.getInstance().getSoundVolumeSetting()) / 100f); + anim.start(); + anim.setOnEventDone(() -> _battleScene.resetPositions()); + blockingAnimations.add(anim); + _currentBattleState = battleEvent.getState(); + this._battleScene.setTextToDisplay(battleEvent.getText()); } else { //Unknown event (Or TextEvent) _currentBattleState = eventState; - if(forcedAIDelay.getSpeed() > 0) { + if (forcedAIDelay.getSpeed() > 0) { addEmptyAnimation(forcedAIDelay.getSpeed(), true); } this._battleScene.setTextToDisplay(battleEvent.getText()); diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/animations/EnemyCrashAnimation.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/animations/EnemyCrashAnimation.java new file mode 100644 index 00000000..ac11c981 --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/animations/EnemyCrashAnimation.java @@ -0,0 +1,39 @@ +package dk.sdu.mmmi.modulemon.BattleScene.animations; + +import dk.sdu.mmmi.modulemon.BattleScene.scenes.BattleScene; +import dk.sdu.mmmi.modulemon.BattleScene.scenes.BattleSceneDefaults; +import dk.sdu.mmmi.modulemon.common.animations.BaseAnimation; +import dk.sdu.mmmi.modulemon.common.data.GameData; +import dk.sdu.mmmi.modulemon.common.drawing.Position; + +import java.util.ArrayList; + +public class EnemyCrashAnimation extends BaseAnimation { + + private BattleScene _battleScene; + + public EnemyCrashAnimation(BattleScene battleScene) { + super(); + Timeline = new int[]{0, 2000}; + States = new ArrayList<>(Timeline.length); + + //Initial state + States.add(new float[]{ + BattleSceneDefaults.enemyMonsterRotation() //rotation + }); + + States.add(new float[]{ + 3600f //rotation + }); + + this._battleScene = battleScene; + } + + @Override + public void update(GameData gameData) { + super.tick(); + + float[] states = super.getCurrentStates(); + this._battleScene.setEnemyMonsterRotation(states[0]); + } +} diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/animations/PlayerCrashAnimation.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/animations/PlayerCrashAnimation.java new file mode 100644 index 00000000..20fd0ffa --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/animations/PlayerCrashAnimation.java @@ -0,0 +1,38 @@ +package dk.sdu.mmmi.modulemon.BattleScene.animations; + +import dk.sdu.mmmi.modulemon.BattleScene.scenes.BattleScene; +import dk.sdu.mmmi.modulemon.BattleScene.scenes.BattleSceneDefaults; +import dk.sdu.mmmi.modulemon.common.animations.BaseAnimation; +import dk.sdu.mmmi.modulemon.common.data.GameData; + +import java.util.ArrayList; + +public class PlayerCrashAnimation extends BaseAnimation { + + private BattleScene _battleScene; + + public PlayerCrashAnimation(BattleScene battleScene) { + super(); + Timeline = new int[]{0, 2000}; + States = new ArrayList<>(Timeline.length); + + //Initial state + States.add(new float[]{ + BattleSceneDefaults.playerMonsterRotation() //rotation + }); + + States.add(new float[]{ + 3600f //rotation + }); + + this._battleScene = battleScene; + } + + @Override + public void update(GameData gameData) { + super.tick(); + + float[] states = super.getCurrentStates(); + this._battleScene.setPlayerMonsterRotation(states[0]); + } +} 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 43a01f31..c3865dda 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java @@ -5,6 +5,7 @@ import dk.sdu.mmmi.modulemon.CommonBattleSimulation.BattleEvents.*; import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; + import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -27,25 +28,25 @@ public class BattleSimulation implements IBattleSimulation { @Override public void StartBattle(IBattleParticipant player, IBattleParticipant enemy) { - if (player.getActiveMonster().getHitPoints()<=0){ + if (player.getActiveMonster().getHitPoints() <= 0) { for (IMonster monster : player.getMonsterTeam()) { - if (monster.getHitPoints() > 0){ + if (monster.getHitPoints() > 0) { player.setActiveMonster(monster); break; } } } - if (enemy.getActiveMonster().getHitPoints()<=0){ + if (enemy.getActiveMonster().getHitPoints() <= 0) { for (IMonster monster : enemy.getMonsterTeam()) { - if (monster.getHitPoints() > 0){ + if (monster.getHitPoints() > 0) { enemy.setActiveMonster(monster); break; } } } - if(player.getActiveMonster().getHitPoints()<=0 || enemy.getActiveMonster().getHitPoints()<=0){ + if (player.getActiveMonster().getHitPoints() <= 0 || enemy.getActiveMonster().getHitPoints() <= 0) { throw new RuntimeException("Active monsters should have at least 1 HP"); } @@ -55,17 +56,15 @@ public void StartBattle(IBattleParticipant player, IBattleParticipant enemy) { this.battleState = new BattleState(player, enemy); this.battleState.setActiveParticipant(firstToTakeTurn); - if(opponentAIFactory != null) + if (opponentAIFactory != null) this.opponentAI = opponentAIFactory.getBattleAI(this, enemy); if (!firstToTakeTurn.isPlayerControlled()) { nextEvent = new InfoBattleEvent("The opponent starts the battle", battleState.clone()); onNextEvent = () -> { battleState.setActiveParticipant(firstToTakeTurn); - if (getOpponentAI()!=null) { - AIExecutor.execute(() -> { - getOpponentAI().doAction(); - }); + if (getOpponentAI() != null) { + doOpponentMove(); } else { nextEvent = new InfoBattleEvent("Waiting for an AI module", battleState.clone()); } @@ -79,13 +78,12 @@ public void StartBattle(IBattleParticipant player, IBattleParticipant enemy) { } } + private void switchTurns() { if (battleState.isPlayersTurn()) { - if (getOpponentAI()!=null) { + if (getOpponentAI() != null) { battleState.setActiveParticipant(battleState.getEnemy()); - AIExecutor.execute(() -> { - getOpponentAI().doAction(); - }); + doOpponentMove(); } else { nextEvent = new InfoBattleEvent("Waiting for an AI module", battleState.clone()); } @@ -108,10 +106,11 @@ public IBattleState getState() { public synchronized void doMove(IBattleParticipant battleParticipant, IMonsterMove move) { if (monsterProcessor == null) { nextEvent = new VictoryBattleEvent("Monsters unloaded, it's a draw", battleParticipant, battleState.clone()); - onNextEvent = () -> {}; + onNextEvent = () -> { + }; return; } - if (battleParticipant!=battleState.getActiveParticipant()) { + if (battleParticipant != battleState.getActiveParticipant()) { throw new IllegalArgumentException("It is not that battle participants turn!"); } if (!battleParticipant.getActiveMonster().getMoves().contains(move)) { @@ -125,7 +124,7 @@ public synchronized void doMove(IBattleParticipant battleParticipant, IMonsterMo IBattleParticipant opposingParticipant; if (battleParticipant.equals(battleState.getPlayer())) { - if (getOpponentAI()!=null) { + if (getOpponentAI() != null) { getOpponentAI().opposingMonsterUsedMove(source, move); } } @@ -143,13 +142,13 @@ public synchronized void doMove(IBattleParticipant battleParticipant, IMonsterMo } int damage = monsterProcessor.calculateDamage(source, move, target); - int newHitPoints = target.getHitPoints()-damage; + int newHitPoints = target.getHitPoints() - damage; target.setHitPoints(Math.max(newHitPoints, 0)); nextEvent = new MoveBattleEvent(participantTitle + "'s " + source.getName() + " used " + move.getName() + " for " + damage + " damage!", battleParticipant, move, damage, battleState.clone()); onNextEvent = () -> { - if (newHitPoints>0) { + if (newHitPoints > 0) { switchTurns(); } else { Optional nextMonster = opposingParticipant.getMonsterTeam().stream().filter(x -> x.getHitPoints() > 0).findFirst(); @@ -160,7 +159,8 @@ public synchronized void doMove(IBattleParticipant battleParticipant, IMonsterMo onNextEvent = this::switchTurns; } else { nextEvent = new VictoryBattleEvent(opposingParticipantTitle + "s monster fainted... " + participantTitle + " won the battle.", battleParticipant, battleState.clone()); - onNextEvent = () -> {}; + onNextEvent = () -> { + }; } } }; @@ -168,14 +168,14 @@ public synchronized void doMove(IBattleParticipant battleParticipant, IMonsterMo @Override public synchronized void switchMonster(IBattleParticipant battleParticipant, IMonster monster) { - if (battleState.getActiveParticipant()!=battleParticipant) { + if (battleState.getActiveParticipant() != battleParticipant) { throw new IllegalArgumentException("It is not that battle participants turn!"); } IBattleParticipant participant = battleState.getActiveParticipant(); - if (monster.getHitPoints()<=0) throw new IllegalArgumentException("You can't change to a dead monster"); + if (monster.getHitPoints() <= 0) throw new IllegalArgumentException("You can't change to a dead monster"); if (participant.getMonsterTeam().contains(monster)) { participant.setActiveMonster(monster); - nextEvent = new ChangeMonsterBattleEvent(getActiveParticipantTitle()+" changed monster to " + monster.getName(), participant, monster, battleState.clone()); + nextEvent = new ChangeMonsterBattleEvent(getActiveParticipantTitle() + " changed monster to " + monster.getName(), participant, monster, battleState.clone()); onNextEvent = this::switchTurns; } else { throw new IllegalArgumentException("Can't change a players monster to a monster which is not in their team"); @@ -184,7 +184,7 @@ public synchronized void switchMonster(IBattleParticipant battleParticipant, IMo @Override public synchronized void runAway(IBattleParticipant battleParticipant) { - if (battleParticipant!=battleState.getActiveParticipant()) { + if (battleParticipant != battleState.getActiveParticipant()) { throw new IllegalArgumentException("It is not that battle participants turn!"); } @@ -195,18 +195,15 @@ public synchronized void runAway(IBattleParticipant battleParticipant) { @Override public IBattleState simulateDoMove(IBattleParticipant battleParticipant, IMonsterMove move, IBattleState currentState) { - //Maybe add a check, that the battleParticipant is actually the active participant BattleState newState = (BattleState) currentState.clone(); - - var callingParticipant = newState.getPlayer().equals(battleParticipant) ? newState.getPlayer() : newState.getEnemy(); - IMonster source = callingParticipant.getActiveMonster(); - IBattleParticipant opposingParticipant; - if (newState.isPlayersTurn()) { - opposingParticipant = newState.getEnemy(); - } else { - opposingParticipant = newState.getPlayer(); + if (!newState.getActiveParticipant().equals(battleParticipant)) { + throw new IllegalStateException("Simulated move for battle participant, is not the active participant!"); } + + IMonster source = newState.getActiveParticipant().getActiveMonster(); + IBattleParticipant opposingParticipant = newState.getPlayer().equals(newState.getActiveParticipant()) + ? newState.getEnemy() : newState.getPlayer(); IMonster target = opposingParticipant.getActiveMonster(); int damage = 0; @@ -214,17 +211,14 @@ public IBattleState simulateDoMove(IBattleParticipant battleParticipant, IMonste damage = monsterProcessor.calculateDamage(source, move, target); } - int newHitPoints = target.getHitPoints()-damage; - if (newHitPoints>0) { + int newHitPoints = target.getHitPoints() - damage; + if (newHitPoints > 0) { target.setHitPoints(newHitPoints); - } else { target.setHitPoints(0); Optional nextMonster = opposingParticipant.getMonsterTeam().stream().filter(x -> x.getHitPoints() > 0).findFirst(); - if (nextMonster.isPresent()) { - opposingParticipant.setActiveMonster(nextMonster.get()); - } + nextMonster.ifPresent(opposingParticipant::setActiveMonster); } switchTurns(newState); @@ -235,9 +229,10 @@ public IBattleState simulateDoMove(IBattleParticipant battleParticipant, IMonste @Override public IBattleState simulateSwitchMonster(IBattleParticipant participant, IMonster monster, IBattleState currentState) { BattleState newState = (BattleState) currentState.clone(); - - var callingParticipant = newState.getPlayer().equals(participant) ? newState.getPlayer() : newState.getEnemy(); - callingParticipant.setActiveMonster(monster.clone()); + if (!newState.getActiveParticipant().equals(participant)) { + throw new IllegalStateException("Simulated switch for battle participant, is not the active participant!"); + } + newState.getActiveParticipant().setActiveMonster(monster); switchTurns(newState); @@ -255,7 +250,7 @@ private void switchTurns(BattleState state) { @Override public synchronized IBattleEvent getNextBattleEvent() { IBattleEvent event = nextEvent; - if (event!=null && onNextEvent!=null) { + if (event != null && onNextEvent != null) { nextEvent = null; onNextEvent.run(); } @@ -268,8 +263,8 @@ public boolean hasNextBattleEvent() { } private IBattleAI getOpponentAI() { - if (this.opponentAI ==null) { - if (this.opponentAIFactory ==null) { + if (this.opponentAI == null) { + if (this.opponentAIFactory == null) { return null; } this.opponentAI = this.opponentAIFactory.getBattleAI(this, battleState.getEnemy()); @@ -277,9 +272,21 @@ private IBattleAI getOpponentAI() { return this.opponentAI; } + private void doOpponentMove() { + AIExecutor.execute(() -> { + try { + getOpponentAI().doAction(); + } catch (Exception ex) { + System.out.println(ex); + nextEvent = new AICrashedEvent(String.format("The opponent %s controller has crashed!", this.opponentAIFactory), this.battleState.getActiveParticipant(), ex, battleState.clone()); + onNextEvent = this::switchTurns; + } + }); + } + private IBattleAI getPlayerAI() { - if (this.playerAI ==null) { - if (this.playerAIFactory ==null) { + if (this.playerAI == null) { + if (this.playerAIFactory == null) { return null; } this.playerAI = this.playerAIFactory.getBattleAI(this, battleState.getPlayer()); @@ -287,11 +294,17 @@ private IBattleAI getPlayerAI() { return this.playerAI; } - private void doPlayerMove(){ + private void doPlayerMove() { // If player controlled, do nothing. Otherwise, call the player AI - if (getPlayerAI()!=null) { + if (getPlayerAI() != null) { AIExecutor.execute(() -> { - getPlayerAI().doAction(); + try { + getPlayerAI().doAction(); + } catch (Exception ex) { + System.out.println(ex); + 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/BattleSimulation/BattleState.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleState.java index 0c6d53dd..148775d2 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleState.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleState.java @@ -46,4 +46,9 @@ public IBattleState clone() { clone.setActiveParticipant(activeParticipant); return clone; } + + @Override + public String toString() { + return String.format("%s vs. %s (it is %s's turn)", this.player.getActiveMonster(), this.enemy.getActiveMonster(), this.activeParticipant.getActiveMonster().getName()); + } } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/BattleEvents/AICrashedEvent.java b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/BattleEvents/AICrashedEvent.java new file mode 100644 index 00000000..67c2bce0 --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/BattleEvents/AICrashedEvent.java @@ -0,0 +1,35 @@ +package dk.sdu.mmmi.modulemon.CommonBattleSimulation.BattleEvents; + +import dk.sdu.mmmi.modulemon.CommonBattle.IBattleParticipant; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleState; + +public class AICrashedEvent implements IBattleEvent { + private String text; + private IBattleParticipant participant; + private Exception exception; + private IBattleState state; + public AICrashedEvent(String text, IBattleParticipant participant, Exception ex, IBattleState state) { + this.text = text; + this.participant = participant; + this.exception = ex; + this.state = state; + } + + @Override + public String getText() { + return this.text; + } + + @Override + public IBattleState getState() { + return this.state; + } + + public IBattleParticipant getParticipant() { + return participant; + } + + public Exception getException() { + return exception; + } +} 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 9ece7121..287f6df3 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/MCTSBattleAI.java @@ -24,7 +24,8 @@ public class MCTSBattleAI implements IBattleAI { private long startTime; private int defaultTimeLimitMs = 1000; private IGameSettings settings = null; - private final int MAX_SIMULATE_DEPTH = 20; + private long timeLimit = defaultTimeLimitMs; + private final int MAX_SIMULATE_DEPTH = 5; private final float EXPLORATION_COEFFICIENT = (float) (1.0 / Math.sqrt(2)); @@ -36,10 +37,11 @@ public MCTSBattleAI(IBattleSimulation battleSimulation, IBattleParticipant parti : battleSimulation.getState().getPlayer(); this.battleSimulation = battleSimulation; this.settings = settings; + this.timeLimit = getTimeLimitms(); } public boolean outOfTime() { - return ((System.nanoTime() - startTime) / 1000000) >= getTimeLimitms(); + return ((System.nanoTime() - startTime) / 1000000) >= this.timeLimit; } private long getTimeLimitms() { @@ -54,9 +56,12 @@ private long getTimeLimitms() { return (int) limitObj; } + private int numSimulatedActions = 0; + @Override public void doAction() { - System.out.println("Starting action finding"); + System.out.printf("Starting action finding (time limit: %dms)%n", this.timeLimit); + numSimulatedActions = 0; // Update state, should the enemy have changed their monster if (!knowledgeState.getEnemyMonsters().contains(opposingParticipant.getActiveMonster())) { @@ -64,8 +69,7 @@ public void doAction() { } startTime = System.nanoTime(); - var rootNode = new Node(battleSimulation.getState().clone()); - + var rootNode = new Node(battleSimulation.getState().clone(), this.participantToControl); while (!outOfTime()) { var newNode = treePolicy(rootNode); var reward = defaultPolicy(newNode); @@ -74,7 +78,9 @@ public void doAction() { var bestChild = bestChild(rootNode, 0); - System.out.println(explainBestChild(bestChild)); + System.out.println(String.format("Simulated %d actions in %dms", this.numSimulatedActions, ((System.nanoTime() - startTime) / 1000000))); + System.out.println(explainNodeOptions(rootNode)); +// System.out.println(explainBestChild(bestChild)); if (bestChild.getParentMove() != null) { battleSimulation.doMove(participantToControl, bestChild.getParentMove()); @@ -85,15 +91,31 @@ public void doAction() { } } + private String explainNodeOptions(Node rootNode) { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("MCTS (").append(rootNode.getState().getPlayer().equals(this.participantToControl) ? "player" : "opponent").append(" side) has ").append(rootNode.getChildren().size()).append(" options. In decreasing order of reward they are:").append('\n'); + for (var node : rootNode.getChildren().stream().sorted((a, b) -> Float.compare(a.getReward(), b.getReward()) * -1).toArray()) { + stringBuilder.append("- ").append(node.toString()).append('\n'); + } + + return stringBuilder.toString(); + } + + /** + * Will iterate the best rewards under a given node. + * A nice way to get an overview over what the best scenario (according to MCTS) below a certain Node is. + * It's not suuper usefull, because it has to assume the player is playing in their favour, which they often arent. + */ private String explainBestChild(Node bestChild) { StringBuilder stringBuilder = new StringBuilder(); String action = bestChild.getParentMove() != null ? "using " + bestChild.getParentMove().getName() : "switching to " + bestChild.getParentSwitch(); - stringBuilder.append("MCTS is ").append(action).append(" because it sees the following best scenario:").append('\n'); + stringBuilder.append("Looking forward from ").append(action).append(", it sees the following best scenario:").append('\n'); var currentlyExpanding = bestChild; - var isMCTS = true; + var isMCTS = false; int turnCount = 1; do { var children = currentlyExpanding.getChildren(); @@ -105,9 +127,22 @@ private String explainBestChild(Node bestChild) { var user = isMCTS ? "MCTS" : "The player"; var bestChildAction = bestChildOfCurrentlyExpanding.getParentMove() != null ? - "using " + bestChildOfCurrentlyExpanding.getParentMove().getName() - : "switching to " + bestChildOfCurrentlyExpanding.getParentSwitch(); - stringBuilder.append("- Turn ").append(turnCount).append(": ").append(user).append(" uses ").append(bestChildAction).append('\n'); + " uses " + bestChildOfCurrentlyExpanding.getParentMove().getName() + : " switches to " + bestChildOfCurrentlyExpanding.getParentSwitch(); + stringBuilder.append("- Turn ") + .append(turnCount) + .append(": ") + .append(user) + .append(bestChildAction) + .append(" (Reward: ") + .append(bestChildOfCurrentlyExpanding.getReward()) + .append(")"); + + if (isTerminal(currentlyExpanding.getState())) { + stringBuilder.append(" [TERMINAL STATE]"); + } + + stringBuilder.append('\n'); isMCTS = !isMCTS; turnCount++; @@ -118,6 +153,9 @@ private String explainBestChild(Node bestChild) { } private void backpropagation(Node node, float reward) { + if (Float.isNaN(reward)) { + throw new IllegalArgumentException("Reward must be a number"); + } do { node.incrementTimesVisited(); node.setReward(node.getReward() + reward); @@ -128,15 +166,36 @@ private void backpropagation(Node node, float reward) { private float defaultPolicy(Node node) { var state = node.getState().clone(); var depth = 0; + + IBattleParticipant participant1 = participantToControl; + IBattleParticipant participant2 = opposingParticipant; + if (node.getParticipant().equals(opposingParticipant)) { + participant1 = opposingParticipant; + participant2 = participantToControl; + } + while (!isTerminal(state) && depth < MAX_SIMULATE_DEPTH) { - var controllingAction = chooseRandomAction(participantToControl); - state = simulateAction(participantToControl, controllingAction, state); - var opposingAction = chooseRandomAction(opposingParticipant); - state = simulateAction(opposingParticipant, opposingAction, state); + var action1 = chooseRandomAction(getParticipantFromState(state, participant1)); + state = simulateAction(participant1, action1, state); + if (!isTerminal(state)) { + // 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); + } depth++; } - return getReward(state); + var reward = getReward(state); + + if (reward > 0) { + reward *= (1f / depth); // Making sure that the deeper, the worse reward + } + + return reward; + } + + private IBattleParticipant getParticipantFromState(IBattleState state, IBattleParticipant existingParticipant) { + return state.getPlayer().equals(existingParticipant) ? state.getPlayer() : state.getEnemy(); } private float getReward(IBattleState battleState) { @@ -161,7 +220,7 @@ private float getReward(IBattleState battleState) { var result = (float) ownMonsterHPSum / (ownMonsterHPSum + enemyMonsterHPSum); if (Float.isNaN(result)) { - throw new IllegalStateException("Calculated reward is NaA"); + throw new IllegalStateException("Calculated reward is NaN"); } // This will return 1 if all the enemy's monsters are dead, 0 if all the AI's monster @@ -172,13 +231,14 @@ private float getReward(IBattleState battleState) { private Object chooseRandomAction(IBattleParticipant participant) { var possibleMoves = participant.getActiveMonster().getMoves(); - var possibleSwitches = participant.getMonsterTeam(); + var possibleSwitches = participant.getMonsterTeam().stream().filter(x -> x.getHitPoints() > 0 && !x.equals(participant.getActiveMonster())).toList(); 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, IBattleState currentState) { + this.numSimulatedActions++; if (action instanceof IMonsterMove move) { return battleSimulation.simulateDoMove(actor, move, currentState); } else if (action instanceof IMonster monster) { @@ -228,12 +288,12 @@ private Node expandNode(Node node) { Node child; if (action instanceof IMonsterMove move) { child = new Node( - battleSimulation.simulateDoMove(participantToControl, move, battleSimulation.getState()), + battleSimulation.simulateDoMove(node.getParticipant(), move, node.getState()), node, move); } else if (action instanceof IMonster monster) { child = new Node( - battleSimulation.simulateSwitchMonster(participantToControl, monster, battleSimulation.getState()), + battleSimulation.simulateSwitchMonster(node.getParticipant(), monster, node.getState()), node, monster); } else { @@ -244,10 +304,12 @@ private Node expandNode(Node node) { } private Object untriedAction(Node node) { - var possibleMoves = participantToControl.getActiveMonster().getMoves(); - var possibleSwitchActions = participantToControl.getMonsterTeam().stream() - .filter(m -> m.getHitPoints() > 0 && m != participantToControl.getActiveMonster()).toList(); + var possibleMoves = node.getParticipant().getActiveMonster().getMoves(); + var possibleSwitchActions = node.getParticipant().getMonsterTeam().stream() + .filter(m -> m.getHitPoints() > 0 && m != node.getParticipant().getActiveMonster()).toList(); 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); @@ -259,36 +321,42 @@ private Object untriedAction(Node node) { } } var possibleActionCount = possibleActions.size(); - if (possibleActionCount == 0) return -1; - else { + 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()) + var moveCount = node.getParticipant().getActiveMonster().getMoves().size(); + var switchCount = node.getParticipant().getMonsterTeam().stream() + .filter(m -> m.getHitPoints() > 0 && m != node.getParticipant().getActiveMonster()) .count(); + return node.getChildren().size() >= (moveCount + switchCount); } private boolean isTerminal(IBattleState battleState) { - IBattleParticipant enemy = battleState.getPlayer().equals(this.participantToControl) - ? battleState.getEnemy() - : battleState.getPlayer(); + IBattleParticipant thisParticipant = battleState.getPlayer(); + IBattleParticipant opposingParticipant = battleState.getEnemy(); + + if (!thisParticipant.equals(this.participantToControl)) { + opposingParticipant = battleState.getPlayer(); + thisParticipant = battleState.getEnemy(); + } // Check if all the AIs monsters are dead - boolean allOwnMonstersDead = participantToControl.getMonsterTeam().stream() + boolean allOwnMonstersDead = thisParticipant.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 + boolean allEnemyMonstersDead = opposingParticipant.getMonsterTeam().stream() +// .filter(x -> knowledgeState.getEnemyMonsters().contains(x)) //only consider monsters we've seen .allMatch(x -> x.getHitPoints() <= 0); - return allEnemyMonstersDead; + + return allEnemyMonstersDead || allOwnMonstersDead; } @Override 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 49afcfaa..6129a41a 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/MCTSBattleAI/Node.java @@ -1,5 +1,6 @@ package dk.sdu.mmmi.modulemon.MCTSBattleAI; +import dk.sdu.mmmi.modulemon.CommonBattle.IBattleParticipant; import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleState; import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; @@ -7,6 +8,7 @@ import java.util.ArrayList; public class Node { + private IBattleParticipant participant; private IBattleState state; private ArrayList children = new ArrayList(); private Node parent = null; @@ -14,19 +16,23 @@ public class Node { private IMonster parentSwitch = null; private float reward = 0; private int timesVisited = 0; - public Node(IBattleState state) { + public Node(IBattleState state, IBattleParticipant participant) { + // Constructor used when creating root node this.state = state; + this.participant = participant; } public Node(IBattleState state, Node parent, IMonsterMove parentMove) { this.state = state; this.parent = parent; this.parentMove = parentMove; + this.participant = getOpposingParticipant(parent.getParticipant(), state); parent.getChildren().add(this); } public Node(IBattleState state, Node parent, IMonster parentSwitch) { this.state = state; this.parent = parent; this.parentSwitch = parentSwitch; + this.participant = getOpposingParticipant(parent.getParticipant(), state); parent.getChildren().add(this); } @@ -85,4 +91,31 @@ public int getTimesVisited() { public void incrementTimesVisited() { this.timesVisited++; } + + public IBattleParticipant getParticipant() { + return participant; + } + + public void setParticipant(IBattleParticipant participant) { + this.participant = participant; + } + + private IBattleParticipant getOpposingParticipant(IBattleParticipant current, IBattleState state) { + return state.getPlayer().equals(current) ? state.getEnemy() : state.getPlayer(); + } + + @Override + public String toString() { + if(this.parent != null){ + String action = "doing nothing"; + if(this.parentMove != null){ + action = String.format("%s using %s", this.parent.getParticipant().getActiveMonster().getName(), this.parentMove); + }else if(this.parentSwitch != null){ + action = "Switching to " + this.parentSwitch; + } + return String.format("%s (Reward: %.8f)", action, this.reward); + }else{ + return "Root node"; + } + } } diff --git a/src/main/java/dk/sdu/mmmi/modulemon/Monster/Monster.java b/src/main/java/dk/sdu/mmmi/modulemon/Monster/Monster.java index 707c3ff7..4cb5c697 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/Monster/Monster.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/Monster/Monster.java @@ -142,6 +142,6 @@ public int hashCode() { @Override public String toString() { - return this.getName(); + return String.format("%s (HP: %d/%d)", this.getName(), this.getHitPoints(), this.getMaxHitPoints()); } }