diff --git a/src/main/java/dk/sdu/mmmi/Main.java b/src/main/java/dk/sdu/mmmi/Main.java index 848cd493..1736c4f2 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.CustomBattleView.CustomBattleView; import dk.sdu.mmmi.modulemon.Game; +import dk.sdu.mmmi.modulemon.HeadlessBattleView.HeadlessBattleView; import dk.sdu.mmmi.modulemon.Interaction.InteractProcessing; import dk.sdu.mmmi.modulemon.MCTSBattleAI.MCTSBattleAIFactory; import dk.sdu.mmmi.modulemon.Map.MapView; @@ -33,6 +34,8 @@ public static void main(String[] args) throws IOException, URISyntaxException { var battleAI = new dk.sdu.mmmi.modulemon.BattleAI.BattleAIFactory(); battleAI.setSettingsService(settings); + var nonAlphaBetaBattleAI = new dk.sdu.mmmi.modulemon.BattleAI.NoABBattleAIFactory(); + nonAlphaBetaBattleAI.setSettingsService(settings); var mctsBattleAI = new MCTSBattleAIFactory(); mctsBattleAI.setSettingsService(settings); var simpleBattleAI = new dk.sdu.mmmi.modulemon.SimpleAI.BattleAIFactory(); // Uncomment for Simple AI @@ -54,6 +57,15 @@ public static void main(String[] args) throws IOException, URISyntaxException { customBattle.addBattleAI(battleAI); customBattle.addBattleAI(mctsBattleAI); customBattle.addBattleAI(simpleBattleAI); + customBattle.addBattleAI(nonAlphaBetaBattleAI); + + var headlessBattle = new HeadlessBattleView(); + headlessBattle.setSettings(settings); + headlessBattle.addBattleAI(battleAI); + headlessBattle.addBattleAI(mctsBattleAI); + headlessBattle.addBattleAI(simpleBattleAI); + headlessBattle.addBattleAI(nonAlphaBetaBattleAI); + headlessBattle.setMonsterRegistry(monsterRegistry); // Map stuff @@ -91,6 +103,7 @@ public static void main(String[] args) throws IOException, URISyntaxException { game.setSettingsService(settings); game.addGameViewServiceList(battle); game.addGameViewServiceList(customBattle); + game.addGameViewServiceList(headlessBattle); game.addGameViewServiceList(map); } } \ No newline at end of file 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 fa5bc082..88823de0 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/BattleAI.java @@ -27,6 +27,19 @@ public class BattleAI implements IBattleAI { private int defaultTimeLimitms = 1000; private IGameSettings settings = null; + public BattleAI(IBattleSimulation battleSimulation, IBattleParticipant participant, IGameSettings settings, boolean useAlphaBeta){ + 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 = participant; + this.opposingParticipant = participantToControl == battleSimulation.getState().getPlayer() + ? battleSimulation.getState().getEnemy() + : battleSimulation.getState().getPlayer(); + this.battleSimulation = battleSimulation; + defaultUseAlphaBetaPruning = useAlphaBeta; + defaultTimeLimitms = (Integer) settings.getSetting(SettingsRegistry.getInstance().getAIProcessingTimeSetting()); + } + public BattleAI(IBattleSimulation battleSimulation, IBattleParticipant participantToControl, IGameSettings settings) { diff --git a/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/NoABBattleAIFactory.java b/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/NoABBattleAIFactory.java new file mode 100644 index 00000000..eb82f20d --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleAI/NoABBattleAIFactory.java @@ -0,0 +1,41 @@ +package dk.sdu.mmmi.modulemon.BattleAI; + +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 NoABBattleAIFactory implements IBattleAIFactory { + private IGameSettings settings = null; + + public NoABBattleAIFactory() { + + } + + @Override + public IBattleAI getBattleAI(IBattleSimulation battleSimulation, IBattleParticipant participantToControl) { + return new BattleAI(battleSimulation, participantToControl, this.settings, false); + } + + public void setSettingsService(IGameSettings settings) { + System.out.println("Settings injected into AIFactory"); + this.settings = settings; + if (settings.getSetting(SettingsRegistry.getInstance().getAIProcessingTimeSetting())==null) { + settings.setSetting(SettingsRegistry.getInstance().getAIProcessingTimeSetting(), 1000); + } + if (settings.getSetting(SettingsRegistry.getInstance().getAIAlphaBetaSetting())==null) { + settings.setSetting(SettingsRegistry.getInstance().getAIAlphaBetaSetting(), true); + } + } + + public void removeSettingsService(IGameSettings settings) { + this.settings = null; + } + + @Override + public String toString() { + return "Minimax -AB"; + } +} 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 962f665c..4dc5d397 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleResult.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleResult.java @@ -8,11 +8,13 @@ public class BattleResult implements IBattleResult { private IBattleParticipant player; private IBattleParticipant enemy; private int turnCount; + private IBattleParticipant starter; - public BattleResult(IBattleParticipant winner, IBattleParticipant player, IBattleParticipant enemy, int turnCount) { + public BattleResult(IBattleParticipant winner, IBattleParticipant player, IBattleParticipant enemy, IBattleParticipant starter, int turnCount) { this.winner = winner; this.player = player; this.enemy = enemy; + this.starter = starter; this.turnCount = turnCount; } @@ -31,8 +33,14 @@ public IBattleParticipant getEnemy() { return enemy; } + @Override + public IBattleParticipant getStarter() { + return starter; + } + @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 1afcc615..863510a4 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleScene/BattleView.java @@ -176,7 +176,17 @@ public void forceBattleEnd() { public void handleBattleEnd(VictoryBattleEvent victoryBattleEvent) { if (_battleCallback != null) { - _battleCallback.onBattleEnd(new BattleResult(victoryBattleEvent.getWinner(), _battleSimulation.getState().getPlayer(), _battleSimulation.getState().getEnemy(), _numTurns)); + var starter = _battleSimulation.playerStarted() + ? _battleSimulation.getState().getPlayer() + : _battleSimulation.getState().getEnemy(); + var result = new BattleResult( + victoryBattleEvent.getWinner(), + _battleSimulation.getState().getPlayer(), + _battleSimulation.getState().getEnemy(), + starter, + _numTurns + ); + _battleCallback.onBattleEnd(result); } else { gameViewManager.setDefaultView(); } 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 7148ea5a..36db889d 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/BattleSimulation/BattleSimulation.java @@ -7,23 +7,21 @@ import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterMove; import java.util.Optional; +import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class BattleSimulation implements IBattleSimulation { - private BattleState battleState; - private IBattleEvent nextEvent; private Runnable onNextEvent; - private IBattleAI opponentAI; private IBattleAIFactory opponentAIFactory; private IBattleAI playerAI; private IBattleAIFactory playerAIFactory; private IBattleMonsterProcessor monsterProcessor; - private ExecutorService AIExecutor = Executors.newFixedThreadPool(1); + private boolean playerStarted; @Override public void StartBattle(IBattleParticipant player, IBattleParticipant enemy) { @@ -51,8 +49,15 @@ public void StartBattle(IBattleParticipant player, IBattleParticipant enemy) { } // Assign first turn - IMonster firstMonster = monsterProcessor.whichMonsterStarts(player.getActiveMonster(), enemy.getActiveMonster()); + IMonster firstMonster; + if(this.playerAIFactory != null && player.getActiveMonster().getName().equals(enemy.getActiveMonster().getName())){ + Random rand = new Random(); + firstMonster = rand.nextFloat() > 0.5 ? player.getActiveMonster() : enemy.getActiveMonster(); + } else { + firstMonster = monsterProcessor.whichMonsterStarts(player.getActiveMonster(), enemy.getActiveMonster()); + } IBattleParticipant firstToTakeTurn = firstMonster == player.getActiveMonster() ? player : enemy; + this.playerStarted = firstMonster == player.getActiveMonster(); this.battleState = new BattleState(player, enemy); this.battleState.setActiveParticipant(firstToTakeTurn); @@ -337,6 +342,11 @@ public IBattleAIFactory getPlayerAIFactory() { return playerAIFactory; } + @Override + public boolean playerStarted() { + return this.playerStarted; + } + @Override public boolean isPlayerControlledByAI() { return this.playerAIFactory != null; 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 e6c7b082..3e0d1fab 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleResult.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleClient/IBattleResult.java @@ -6,5 +6,7 @@ public interface IBattleResult { IBattleParticipant getWinner(); IBattleParticipant getPlayer(); IBattleParticipant getEnemy(); + IBattleParticipant getStarter(); int getTurns(); + } 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 6366b93b..cca27d7a 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/IBattleSimulation.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CommonBattleSimulation/IBattleSimulation.java @@ -32,4 +32,5 @@ public interface IBattleSimulation { IBattleAIFactory getOpponentAIFactory(); void setPlayerAIFactory(IBattleAIFactory BattleAIFactory); IBattleAIFactory getPlayerAIFactory(); + boolean playerStarted(); } 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 78525f1e..821b1cd1 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/CustomBattleView/CustomBattleView.java @@ -350,7 +350,7 @@ private int getGridAdjustedCursor(int cursorPosition) { } private IBattleAIFactory getSelectedAI(Integer index) { - if (index == null) { + if (index == null || battleAIFactoryList.size() == 0) { return null; } return this.battleAIFactoryList.get(index % battleAIFactoryList.size()); diff --git a/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattleScene.java b/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattleScene.java new file mode 100644 index 00000000..e2e72897 --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattleScene.java @@ -0,0 +1,120 @@ +package dk.sdu.mmmi.modulemon.HeadlessBattleView; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import dk.sdu.mmmi.modulemon.Game; +import dk.sdu.mmmi.modulemon.common.data.GameData; +import dk.sdu.mmmi.modulemon.common.drawing.TextUtils; +import dk.sdu.mmmi.modulemon.common.services.IGameSettings; + +public class HeadlessBattleScene { + private SpriteBatch spriteBatch; + public static final Color SelectColor = Color.valueOf("2a75bb"); + private Color startColor = Color.WHITE; + private Color amountColor = Color.WHITE; + private String[] teams = {"Team A", "Team B"}; + private String teamAAIText = ""; + private String teamBAIText = ""; + private int teamIndex = -1; + private int AIIndex = -1; + private int battleAmount = -1; + private boolean choosing = false; + + public HeadlessBattleScene(IGameSettings settings) { + spriteBatch = new SpriteBatch(); + } + + private static TextUtils text = TextUtils.getInstance(); + + public void draw(GameData gameData) { + var screenWidth = gameData.getDisplayWidth(); + var screenHeight = gameData.getDisplayHeight(); + Gdx.gl.glClearColor(0, 0, 0, 1); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + spriteBatch.setProjectionMatrix(Game.cam.combined); + spriteBatch.begin(); + + text.setCoordinateMode(TextUtils.CoordinateMode.CENTER); + final int textTopMargin = 60; + text.drawTitleFont(spriteBatch, "Simulate Battles", Color.valueOf("ffcb05"), gameData.getDisplayWidth() / 2f, gameData.getDisplayHeight() - textTopMargin); + text.setCoordinateMode(TextUtils.CoordinateMode.TOP_LEFT); // Reset + spriteBatch.end(); + + spriteBatch.begin(); + text.setCoordinateMode(TextUtils.CoordinateMode.BOTTOM_RIGHT); + int textGap = 75; + int leftOffset = 150; + int topOffset = 50; + + for (var i = 0; i < teams.length; i++) { + var teamTextColor = teamIndex == i ? SelectColor : Color.WHITE; + var AITextColor = AIIndex == i ? SelectColor : Color.WHITE; + var yPos = ((screenHeight / 2f) + topOffset) - i * textGap; + var AIText = i == 0 ? teamAAIText : teamBAIText; + text.setCoordinateMode(TextUtils.CoordinateMode.BOTTOM_RIGHT); + text.drawBigBoldRoboto(spriteBatch, teams[i], teamTextColor, screenWidth / 2f, yPos); + text.setCoordinateMode(TextUtils.CoordinateMode.CENTER); + var textHeight = 15; + text.drawNormalBoldRoboto(spriteBatch, AIText, AITextColor, screenWidth / 2f + leftOffset, yPos + textHeight); + if (choosing && AIIndex == i) { + var arrowSpacingFromEdge = 20; + var textLength = 220; + var horizontalOffset = 260; + text.drawNormalRoboto(spriteBatch, "<", Color.WHITE, screenWidth / 2f + horizontalOffset + arrowSpacingFromEdge - textLength, yPos + textHeight); + text.drawNormalRoboto(spriteBatch, ">", Color.WHITE, screenWidth / 2f + horizontalOffset - arrowSpacingFromEdge, yPos + textHeight); + } + } + text.setCoordinateMode(TextUtils.CoordinateMode.BOTTOM_RIGHT); + var amountHeight = (screenHeight / 2f) - 100; + text.drawNormalBoldRoboto(spriteBatch, "Battle amount:", choosing ? Color.WHITE : amountColor, screenWidth / 2f, amountHeight); + text.setCoordinateMode(TextUtils.CoordinateMode.CENTER); + var textHeight = 7.5f; + if (choosing && AIIndex == -1) { + var arrowSpacingFromEdge = 20; + var textLength = 125; + var horizontalOffset = 212.5f; + text.drawNormalRoboto(spriteBatch, "<", Color.WHITE, screenWidth / 2f + horizontalOffset + arrowSpacingFromEdge - textLength, amountHeight + textHeight); + text.drawNormalRoboto(spriteBatch, ">", Color.WHITE, screenWidth / 2f + horizontalOffset - arrowSpacingFromEdge, amountHeight + textHeight); + } + text.drawNormalBoldRoboto(spriteBatch, String.valueOf(battleAmount), choosing ? amountColor : Color.WHITE, screenWidth / 2f + leftOffset, amountHeight + textHeight); + text.setCoordinateMode(TextUtils.CoordinateMode.CENTER); + text.drawBigBoldRoboto(spriteBatch, "Start", startColor, screenWidth / 2f, (screenHeight / 2f) - 200); + text.setCoordinateMode(TextUtils.CoordinateMode.TOP_LEFT); + spriteBatch.end(); + + } + + public void setStartColor(Color startColor) { + this.startColor = startColor; + } + + public void setTeamIndex(int teamIndex) { + this.teamIndex = teamIndex; + } + + public void setAIIndex(int AIIndex) { + this.AIIndex = AIIndex; + } + + public void setAmountColor(Color amountColor) { + this.amountColor = amountColor; + } + + public void setTeamAAIText(String teamAAIText) { + this.teamAAIText = teamAAIText; + } + + public void setTeamBAIText(String teamBAIText) { + this.teamBAIText = teamBAIText; + } + + public void setChoosing(boolean choosing) { + this.choosing = choosing; + } + + public void setBattleAmount(int battleAmount) { + this.battleAmount = battleAmount; + } +} diff --git a/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattleView.java b/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattleView.java new file mode 100644 index 00000000..480e0241 --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattleView.java @@ -0,0 +1,395 @@ +package dk.sdu.mmmi.modulemon.HeadlessBattleView; + +import com.badlogic.gdx.audio.Music; +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.Color; +import dk.sdu.mmmi.modulemon.Battle.BattleParticipant; +import dk.sdu.mmmi.modulemon.BattleScene.BattleResult; +import dk.sdu.mmmi.modulemon.BattleSimulation.BattleSimulation; +import dk.sdu.mmmi.modulemon.CommonBattleClient.IBattleResult; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.BattleEvents.AICrashedEvent; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.BattleEvents.ChangeMonsterBattleEvent; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.BattleEvents.VictoryBattleEvent; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.IBattleAIFactory; +import dk.sdu.mmmi.modulemon.CommonMonster.IMonster; +import dk.sdu.mmmi.modulemon.CommonMonster.IMonsterRegistry; +import dk.sdu.mmmi.modulemon.Monster.BattleMonsterProcessor; +import dk.sdu.mmmi.modulemon.common.AssetLoader; +import dk.sdu.mmmi.modulemon.common.SettingsRegistry; +import dk.sdu.mmmi.modulemon.common.data.GameData; +import dk.sdu.mmmi.modulemon.common.data.GameKeys; +import dk.sdu.mmmi.modulemon.common.data.IGameViewManager; +import dk.sdu.mmmi.modulemon.common.drawing.MathUtils; +import dk.sdu.mmmi.modulemon.common.services.IGameSettings; +import dk.sdu.mmmi.modulemon.common.services.IGameViewService; +import dk.sdu.mmmi.modulemon.CommonBattleSimulation.BattleEvents.MoveBattleEvent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.*; + +public class HeadlessBattleView implements IGameViewService { + private IGameSettings settings; + private SettingsRegistry settingsRegistry = SettingsRegistry.getInstance(); + private Sound selectSound; + private Sound chooseSound; + private Sound yaySound; + private List battleAIFactoryList = new ArrayList<>(); + private Music battleWaitMusic; + private Music menuMusic; + private IMonsterRegistry monsterRegistry; + private List> battleResults; + private List> battleResultsToRemove; + private int concurrentBattles = 5; + private int currentBattles = 0; + private ExecutorService battleExecutor = Executors.newFixedThreadPool(concurrentBattles); + private HeadlessBattleScene scene; + private HeadlessBattlingScene battlingScene; + private int teamAWins = 0; + private int teamAStartWins = 0; + private int winTurnsA = 0; + private int teamBWins = 0; + private int teamBStartWins = 0; + private int winTurnsB = 0; + private int completedBattles = 0; + private boolean battling = false; + private boolean doneBattling = false; + private int cursorPosition = 0; + private Integer selectedTeamAAI = 1; + private Integer selectedTeamBAI = 1; + private boolean editingMode = false; + private int[] battleAmounts = {1, 10, 100, 250, 500, 750, 1000}; + private int battleAmountIndex = 1; + + @Override + public void init(IGameViewManager gameViewManager) { + selectSound = AssetLoader.getInstance().getSoundAsset("/sounds/select.ogg", this.getClass()); + chooseSound = AssetLoader.getInstance().getSoundAsset("/sounds/choose.ogg", this.getClass()); + yaySound = AssetLoader.getInstance().getSoundAsset("/sounds/YAY.ogg", this.getClass()); + battleWaitMusic = AssetLoader.getInstance().getMusicAsset("/music/headless.ogg", this.getClass()); + battleWaitMusic.setLooping(true); + battleWaitMusic.setVolume(getSoundVolume()); + menuMusic = AssetLoader.getInstance().getMusicAsset("/music/headlessMenu.ogg", this.getClass()); + menuMusic.setLooping(true); + menuMusic.setVolume(getSoundVolume()); + menuMusic.play(); + + scene = new HeadlessBattleScene(settings); + battlingScene = new HeadlessBattlingScene(); + } + + @Override + public void update(GameData gameData, IGameViewManager gameViewManager) { + if (battling) { + int battleAmount = battleAmounts[battleAmountIndex]; + battlingScene.setTeamAWins(teamAWins); + battlingScene.setTeamBWins(teamBWins); + battlingScene.setTeamAStartWins(teamAStartWins); + battlingScene.setTeamBStartWins(teamBStartWins); + battlingScene.setAvgTurnsToWinA((float)winTurnsA / teamAWins); + battlingScene.setAvgTurnsToWinB((float)winTurnsB / teamBWins); + battlingScene.setBattleProgress(((float) completedBattles / battleAmount)); + battlingScene.setCurrentBattles(currentBattles); + battlingScene.setDoneBattling(doneBattling); + for (var battleResultFuture : battleResults) { + if (battleResultFuture.isDone()) { + try { + var battleResult = battleResultFuture.get(); + if (battleResult.getWinner() == battleResult.getPlayer()) { + teamAWins++; + if(battleResult.getStarter() == battleResult.getWinner()){ + teamAStartWins++; + } + winTurnsA += battleResult.getTurns(); + } else { + teamBWins++; + if(battleResult.getStarter() == battleResult.getWinner()){ + teamBStartWins++; + } + winTurnsB += battleResult.getTurns(); + } + completedBattles++; + currentBattles--; + battleResultsToRemove.add(battleResultFuture); + } catch (InterruptedException | ExecutionException e) { + System.out.println("Battle was cancelled!"); + ; + } + } + } + + if (battleResultsToRemove.size() > 0) { + for (var future : battleResultsToRemove) { + battleResults.remove(future); + } + battleResultsToRemove.clear(); + } + + if (completedBattles >= battleAmount) { + yaySound.play(getSoundVolume()); + battling = false; + doneBattling = true; + currentBattles = 0; + // Stop any remaining battles + stopAllBattles(); + } else if (currentBattles < concurrentBattles && currentBattles + completedBattles < battleAmount) { + var amountBattlesToStart = concurrentBattles - currentBattles; + for (int i = 0; i < amountBattlesToStart; i++) { + battleResults.add(runBattle()); + } + } + } else if (doneBattling) { + battlingScene.setDoneBattling(true); + battlingScene.setTeamAWins(teamAWins); + battlingScene.setTeamBWins(teamBWins); + battlingScene.setTeamAStartWins(teamAStartWins); + battlingScene.setTeamBStartWins(teamBStartWins); + battlingScene.setAvgTurnsToWinA((float)winTurnsA / teamAWins); + battlingScene.setAvgTurnsToWinB((float)winTurnsB / teamBWins); + battlingScene.setBattleProgress((float) completedBattles / battleAmounts[battleAmountIndex]); + battlingScene.setCurrentBattles(currentBattles); + if (battleWaitMusic.getVolume() <= 0) { + battleWaitMusic.stop(); + battleWaitMusic.setVolume(getSoundVolume()); + } else { + battleWaitMusic.setVolume(MathUtils.clamp(battleWaitMusic.getVolume() - 0.0025f, 0, 1)); + } + } else { + cursorPosition = MathUtils.clamp(cursorPosition, 0, 3); + + var teamAAI = getSelectedAI(selectedTeamAAI); + var teamBAI = getSelectedAI(selectedTeamBAI); + if (teamAAI != null) { + scene.setTeamAAIText(teamAAI.toString()); + } else { + scene.setTeamAAIText("No AI's loaded"); + } + if (teamBAI != null) { + scene.setTeamBAIText(getSelectedAI(selectedTeamBAI).toString()); + } else { + scene.setTeamBAIText("No AI's loaded"); + } + + scene.setTeamIndex(-1); + scene.setAIIndex(-1); + scene.setBattleAmount(battleAmounts[battleAmountIndex]); + scene.setStartColor(Color.WHITE); + scene.setAmountColor(Color.WHITE); + scene.setChoosing(editingMode); + + if (cursorPosition >= 0 && cursorPosition <= 1) { + if (editingMode) { + scene.setAIIndex(cursorPosition); + } else { + scene.setTeamIndex(cursorPosition); + } + } else if (cursorPosition == 2) { + scene.setAmountColor(HeadlessBattleScene.SelectColor); + } else if (cursorPosition == 3) { + scene.setStartColor(HeadlessBattleScene.SelectColor); + } + } + } + + private void stopAllBattles() { + doneBattling = true; + currentBattles = 0; + for (var battleFuture : battleResults) { + battleFuture.cancel(true); + } + } + + @Override + public void draw(GameData gameData) { + if (battling || doneBattling) { + battlingScene.draw(gameData); + } else { + scene.draw(gameData); + } + } + + @Override + public void handleInput(GameData gameData, IGameViewManager gameViewManager) { + if (doneBattling) { + if (gameData.getKeys().isPressed(GameKeys.ACTION) || gameData.getKeys().isPressed(GameKeys.BACK)) { + doneBattling = false; + teamAWins = 0; + teamAStartWins = 0; + winTurnsA = 0; + teamBWins = 0; + teamBStartWins = 0; + winTurnsB = 0; + completedBattles = 0; + battlingScene.setAvgTurnsToWinA(0.0f); + battlingScene.setAvgTurnsToWinB(0.0f); + battleWaitMusic.stop(); + menuMusic.play(); + } + return; + } + + if (gameData.getKeys().isPressed(GameKeys.BACK)) { + chooseSound.play(getSoundVolume()); + if (editingMode) { + editingMode = false; + } else if (battling || doneBattling) { + battling = false; + stopAllBattles(); + } else { + gameViewManager.setDefaultView(); + } + } + + if (battling) { + return; + } + + if (editingMode) { + if (gameData.getKeys().isPressed(GameKeys.RIGHT)) { + selectSound.play(getSoundVolume()); + if (cursorPosition == 0) { + selectedTeamAAI = scrollIndex(selectedTeamAAI, 1, battleAIFactoryList.size()); + } else if (cursorPosition == 1) { + selectedTeamBAI = scrollIndex(selectedTeamBAI, 1, battleAIFactoryList.size()); + } else if (cursorPosition == 2) { + battleAmountIndex = scrollIndex(battleAmountIndex, 1, battleAmounts.length); + } + } + if (gameData.getKeys().isPressed(GameKeys.LEFT)) { + selectSound.play(getSoundVolume()); + if (cursorPosition == 0) { + selectedTeamAAI = scrollIndex(selectedTeamAAI, -1, battleAIFactoryList.size()); + } else if (cursorPosition == 1) { + selectedTeamBAI = scrollIndex(selectedTeamBAI, -1, battleAIFactoryList.size()); + } else if (cursorPosition == 2) { + battleAmountIndex = scrollIndex(battleAmountIndex, -1, battleAmounts.length); + } + } + if (gameData.getKeys().isPressed(GameKeys.ACTION)) { + chooseSound.play(getSoundVolume()); + editingMode = false; + } + } else { + if (gameData.getKeys().isPressed(GameKeys.DOWN)) { + selectSound.play(getSoundVolume()); + cursorPosition = cursorPosition >= 3 ? 0 : cursorPosition+1; + } + if (gameData.getKeys().isPressed(GameKeys.UP)) { + selectSound.play(getSoundVolume()); + cursorPosition = cursorPosition <= 0 ? 3 : cursorPosition-1;; + } + if (gameData.getKeys().isPressed(GameKeys.ACTION) && cursorPosition == 3) { + chooseSound.play(getSoundVolume()); + battleResults = new ArrayList>(); + battleResultsToRemove = new ArrayList>(); + for (var i = 0; i < concurrentBattles; i++) { + battleResults.add(runBattle()); + } + menuMusic.stop(); + battleWaitMusic.setVolume(getSoundVolume()); + battleWaitMusic.play(); + battling = true; + } else if (gameData.getKeys().isPressed(GameKeys.ACTION)) { + chooseSound.play(getSoundVolume()); + editingMode = true; + } + } + } + + private Future runBattle() { + var teamAAI = getSelectedAI(selectedTeamAAI); + var teamBAI = getSelectedAI(selectedTeamBAI); + + if (teamAAI == null || teamBAI == null) { + System.out.println("Missing AI for one or more teams"); + return null; + } + + // Currently just static list of monsters. Maybe create random teams, load from file or let user select teams + List teamA = Arrays.asList(monsterRegistry.getMonster(0), monsterRegistry.getMonster(1), monsterRegistry.getMonster(2)); + List teamB = Arrays.asList(monsterRegistry.getMonster(0), monsterRegistry.getMonster(1), monsterRegistry.getMonster(2)); + + var teamAPlayer = new BattleParticipant(teamA, true); + var teamBPlayer = new BattleParticipant(teamB, false); + + var battleSim = new BattleSimulation(); + var processor = new BattleMonsterProcessor(); + battleSim.setMonsterProcessor(processor); + battleSim.setPlayerAIFactory(teamAAI); + battleSim.setOpponentAIFactory(teamBAI); + battlingScene.setTeamAAIName(teamAAI.toString()); + battlingScene.setTeamBAIName(teamBAI.toString()); + + battleSim.StartBattle(teamAPlayer, teamBPlayer); + currentBattles++; + return battleExecutor.submit(() -> { + int turns = 0; + while (true) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + var event = battleSim.getNextBattleEvent(); + if (event instanceof VictoryBattleEvent victoryBattleEvent) { + var winner = victoryBattleEvent.getWinner() == battleSim.getState().getPlayer() + ? battleSim.getState().getPlayer() + : battleSim.getState().getEnemy(); + var starter = battleSim.playerStarted() ? battleSim.getState().getPlayer() : battleSim.getState().getEnemy(); + return new BattleResult(winner, battleSim.getState().getPlayer(), battleSim.getState().getEnemy(), starter, turns); + } else if (event instanceof ChangeMonsterBattleEvent || event instanceof MoveBattleEvent) { + turns++; + } else if (event instanceof AICrashedEvent) { + System.out.println("AI CRASHED!! Oh no! Anyway..."); + } + } + }); + + } + + private Integer scrollIndex(int index, int scrollAmount, int max) { + var out = (index + scrollAmount) % max; + if (out <= -1) { + out = index + scrollAmount + max; + } + return out; + } + + @Override + public void dispose() { + menuMusic.stop(); + menuMusic.dispose(); + menuMusic = null; + battleWaitMusic.stop(); + battleWaitMusic.dispose(); + battleWaitMusic = null; + } + + public String toString() { + return "Simulate Battles"; + } + + + private float getSoundVolume() { + return ((int) settings.getSetting(settingsRegistry.getSoundVolumeSetting()) / 100f); + } + + public void setSettings(IGameSettings settings) { + this.settings = settings; + } + + private IBattleAIFactory getSelectedAI(Integer index) { + if (index == null || battleAIFactoryList.size() == 0) { + return null; + } + + return this.battleAIFactoryList.get(index % battleAIFactoryList.size()); + } + + public void addBattleAI(IBattleAIFactory battleAIFactory) { + this.battleAIFactoryList.add(battleAIFactory); + } + + public void setMonsterRegistry(IMonsterRegistry monsterRegistry) { + this.monsterRegistry = monsterRegistry; + } +} diff --git a/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattlingScene.java b/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattlingScene.java new file mode 100644 index 00000000..cda9396d --- /dev/null +++ b/src/main/java/dk/sdu/mmmi/modulemon/HeadlessBattleView/HeadlessBattlingScene.java @@ -0,0 +1,178 @@ +package dk.sdu.mmmi.modulemon.HeadlessBattleView; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import dk.sdu.mmmi.modulemon.Game; +import dk.sdu.mmmi.modulemon.common.data.GameData; +import dk.sdu.mmmi.modulemon.common.drawing.DrawingUtils; +import dk.sdu.mmmi.modulemon.common.drawing.Position; +import dk.sdu.mmmi.modulemon.common.drawing.Rectangle; +import dk.sdu.mmmi.modulemon.common.drawing.TextUtils; + +public class HeadlessBattlingScene { + private ShapeRenderer shapeRenderer; + private SpriteBatch spriteBatch; + private StringBuilder titleText = new StringBuilder("Simulating Battles"); + private float titleCounter = 0; + private int teamAWins = 0; + private int teamBWins = 0; + private int teamAStartWins = 0; + private int teamBStartWins = 0; + private float avgTurnsToWinA = 0.0f; + private float avgTurnsToWinB = 0.0f; + private String teamAAIName; + private String teamBAIName; + + private int currentBattles = 0; + private float battleProgress = 0; + private boolean doneBattling = false; + + private static TextUtils text = TextUtils.getInstance(); + + private Rectangle teamABackground; + private Rectangle teamBBackground; + + public HeadlessBattlingScene() { + this.shapeRenderer = new ShapeRenderer(); + this.spriteBatch = new SpriteBatch(); + teamABackground = DrawingUtils.createRectangle(Rectangle.class, 0, 0, 0, 0); + teamBBackground = DrawingUtils.createRectangle(Rectangle.class, 0, 0, 0, 0); + } + + public void draw(GameData gameData) { + var screenWidth = gameData.getDisplayWidth(); + var screenHeight = gameData.getDisplayHeight(); + Gdx.gl.glClearColor(0, 0, 0, 1); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + + if (teamABackground.getWidth() <= 0) { + calculateBoxSizes(gameData); + } + shapeRenderer.setAutoShapeType(true); + shapeRenderer.setColor(Color.WHITE); + shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); + teamABackground.draw(shapeRenderer, gameData.getDelta()); + teamBBackground.draw(shapeRenderer, gameData.getDelta()); + shapeRenderer.end(); + + spriteBatch.setProjectionMatrix(Game.cam.combined); + spriteBatch.begin(); + text.setCoordinateMode(TextUtils.CoordinateMode.TOP_LEFT); + final int textTopMargin = 60; + final int textLengthEstimate = 350; + if (!doneBattling) { + animateTitleText(gameData); + } + text.drawTitleFont(spriteBatch, titleText.toString(), Color.valueOf("ffcb05"), screenWidth / 2f - textLengthEstimate, screenHeight - textTopMargin); + final int horizontalOffset = 175; + float aXPos = (screenWidth / 2f) - horizontalOffset; + float bXPos = (screenWidth / 2f) + horizontalOffset; + final int winYGap = 50; + final int boxOffset = 125; + text.setCoordinateMode(TextUtils.CoordinateMode.CENTER); + + var winTextHeight = screenHeight / 2f + boxOffset; + var winNumHeight = winTextHeight - winYGap; + text.drawBigBoldRoboto(spriteBatch, "Team A Wins", Color.BLACK, aXPos, winTextHeight); + text.drawBigBoldRoboto(spriteBatch, "Team B Wins", Color.BLACK, bXPos, winTextHeight); + text.drawSmallRoboto(spriteBatch, teamAAIName, Color.BLACK, aXPos, winTextHeight - 20); + text.drawSmallRoboto(spriteBatch, teamBAIName, Color.BLACK, bXPos, winTextHeight - 20); + text.drawNormalBoldRoboto(spriteBatch, String.valueOf(teamAWins), Color.BLACK, aXPos, winNumHeight); + text.drawNormalBoldRoboto(spriteBatch, String.valueOf(teamBWins), Color.BLACK, bXPos, winNumHeight); + var startingWinTextHeight = winTextHeight - (winYGap * 2); + var startingWinNumHeight = winTextHeight - (winYGap * 2.5f); + text.drawSmallBoldRoboto(spriteBatch, "Wins when starting", Color.BLACK, aXPos, startingWinTextHeight); + text.drawSmallBoldRoboto(spriteBatch, "Wins when starting", Color.BLACK, bXPos, startingWinTextHeight); + text.drawSmallRoboto(spriteBatch, String.valueOf(teamAStartWins), Color.BLACK, aXPos, startingWinNumHeight); + text.drawSmallRoboto(spriteBatch, String.valueOf(teamBStartWins), Color.BLACK, bXPos, startingWinNumHeight); + var turnsToWinTextHeight = winTextHeight - (winYGap * 3.5f); + var turnsToWinNumHeight = winTextHeight - (winYGap * 4f); + text.drawSmallBoldRoboto(spriteBatch, "AVG turns to win", Color.BLACK, aXPos, turnsToWinTextHeight); + text.drawSmallBoldRoboto(spriteBatch, "AVG turns to win", Color.BLACK, bXPos, turnsToWinTextHeight); + text.drawSmallRoboto(spriteBatch, String.valueOf(avgTurnsToWinA), Color.BLACK, aXPos, turnsToWinNumHeight); + text.drawSmallRoboto(spriteBatch, String.valueOf(avgTurnsToWinB), Color.BLACK, bXPos, turnsToWinNumHeight); + + text.drawNormalBoldRoboto(spriteBatch, String.format("Ongoing battles: %d", currentBattles), Color.WHITE, screenWidth / 2f, (screenHeight / 2f) - winYGap * 3f); + text.drawBigBoldRoboto(spriteBatch, String.format("Battle progress %.1f %%", battleProgress * 100), Color.WHITE, screenWidth / 2f, (screenHeight / 2f) - winYGap * 4f); + if (doneBattling) { + text.drawNormalRoboto(spriteBatch, "Press [action] to return", Color.WHITE, screenWidth / 2f, (screenHeight / 2f) - winYGap * 5); + } + text.setCoordinateMode(TextUtils.CoordinateMode.TOP_LEFT); + spriteBatch.end(); + } + + private void animateTitleText(GameData gameData) { + titleCounter += gameData.getDelta(); + if (titleCounter >= 1) { + if (titleText.toString().equalsIgnoreCase("simulating battles...")) { + titleText.delete(18, titleText.length()); + } else { + titleText.append('.'); + } + titleCounter = 0; + } + } + + private void calculateBoxSizes(GameData gameData) { + final int bottomOffset = 250; + final int monsterContainerCenterOffset = 20; + final int freeSpaceToLeaveAtTop = 200; + final float containerWidth = 300; + final float containerHeight = gameData.getDisplayHeight() - bottomOffset - freeSpaceToLeaveAtTop; + teamABackground.setPosition(new Position((gameData.getDisplayWidth() / 2f) - monsterContainerCenterOffset - containerWidth, bottomOffset)); + teamBBackground.setPosition(new Position((gameData.getDisplayWidth() / 2f) + monsterContainerCenterOffset, bottomOffset)); + teamABackground.setWidth(containerWidth); + teamBBackground.setWidth(containerWidth); + teamABackground.setHeight(containerHeight); + teamBBackground.setHeight(containerHeight); + } + + public void setTeamAWins(int teamAWins) { + this.teamAWins = teamAWins; + } + + public void setTeamBWins(int teamBWins) { + this.teamBWins = teamBWins; + } + + public void setTeamAStartWins(int teamAStartWins) { + this.teamAStartWins = teamAStartWins; + } + + public void setTeamBStartWins(int teamBStartWins) { + this.teamBStartWins = teamBStartWins; + } + + public void setAvgTurnsToWinA(float avgTurnsToWinA) { + if(!Float.isNaN(avgTurnsToWinA)) + this.avgTurnsToWinA = avgTurnsToWinA; + } + + public void setAvgTurnsToWinB(float avgTurnsToWinB) { + if(!Float.isNaN(avgTurnsToWinB)) + this.avgTurnsToWinB = avgTurnsToWinB; + } + + public void setTeamAAIName(String teamAAI) { + this.teamAAIName = teamAAI; + } + + public void setTeamBAIName(String teamBAI) { + this.teamBAIName = teamBAI; + } + + public void setDoneBattling(boolean doneBattling) { + this.doneBattling = doneBattling; + } + + public void setCurrentBattles(int currentBattles) { + this.currentBattles = currentBattles; + } + + public void setBattleProgress(float battleProgress) { + this.battleProgress = battleProgress; + } +} 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 aafef9b6..cf9ffde6 100644 --- a/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java +++ b/src/main/java/dk/sdu/mmmi/modulemon/gameviews/MenuView.java @@ -470,7 +470,7 @@ private void handleSettings(String keyInput) { if (menuOptions[currentOption].equalsIgnoreCase("AI Processing Time")) { // Sets the minimum processing time to 500 ms - if (((int) settings.getSetting(settingsRegistry.getAIProcessingTimeSetting()) > 500)) { + if (((int) settings.getSetting(settingsRegistry.getAIProcessingTimeSetting()) > 100)) { // Decreases the processing time by 100 ms int ai_processing_time = (int) settings.getSetting(settingsRegistry.getAIProcessingTimeSetting()) - 100; settings.setSetting(settingsRegistry.getAIProcessingTimeSetting(), ai_processing_time); diff --git a/src/main/resources/music/headless.ogg b/src/main/resources/music/headless.ogg new file mode 100644 index 00000000..65946053 Binary files /dev/null and b/src/main/resources/music/headless.ogg differ diff --git a/src/main/resources/music/headlessMenu.ogg b/src/main/resources/music/headlessMenu.ogg new file mode 100644 index 00000000..2500920c Binary files /dev/null and b/src/main/resources/music/headlessMenu.ogg differ diff --git a/src/main/resources/sounds/YAY.ogg b/src/main/resources/sounds/YAY.ogg new file mode 100644 index 00000000..bc396590 Binary files /dev/null and b/src/main/resources/sounds/YAY.ogg differ