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/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/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/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/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/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..78525f1e 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; @@ -31,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; @@ -59,7 +62,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 +70,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(); } } @@ -138,8 +141,27 @@ 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){ + if (showingResults) { showingResults = false; return; } @@ -148,12 +170,21 @@ public void handleInput(GameData gameData, IGameViewManager gameViewManager) { chooseSound.play(getSoundVolume()); } - if(showingResults){ + if (showingResults) { return; // Don't allow any movement when showing results } 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()); } @@ -206,14 +237,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 +258,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 +286,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 +296,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 +317,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; } } 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 a85b8e10..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() @@ -78,7 +79,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)); @@ -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); @@ -188,7 +189,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; @@ -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 c1a4e3d6..164cae95 100644 Binary files a/src/main/resources/sounds/alert.ogg and b/src/main/resources/sounds/alert.ogg differ