package me.stieglmaier.sphereMiners.model.ai;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import org.sosy_lab.common.Pair;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import me.stieglmaier.sphereMiners.main.Constants;
import me.stieglmaier.sphereMiners.model.physics.Physics;
/**
* This class manages all AIs. It handles the calls for the AIs and submits
* changes made by the AIs to the physics. It also initializes the AIs and
* checks if the AI behaves correctly and reinitialize an abnormally behaving
* AI. For example if the calculation of an AI takes too much time, the AI is
* terminated by the AI Manager and initialized again.
*/
public final class AIManager {
/**
*
* All available AIs which can be used to simulate a game.
*/
private final ObservableList<String> aiList = FXCollections.observableArrayList();
/**
* array of the active AIs, each AI is identified by {@link Team}.
*/
private final Map<Player, SphereMiners2015> ais = new LinkedHashMap<>();
/**
* The loader which loads the ais.
*/
private URLClassLoader loader;
/**
* The physics engine responsible for calculating all the stuff.
*/
private Physics physics;
/**
* path to location with stored ais.
*/
private final String AI_FILELOCATION;
private final Constants constants;
/**
* The constructor of this class. It is responsible for listing the possible
* AIs, so they can be displayed in the View and chosen to simulate games.
*
* @param constants The constants that should be used for the AIs class
* @throws MalformedURLException Could appear if the Constants.AI_LOCATION
* was malformed
*/
public AIManager(Constants constants) throws MalformedURLException {
this.constants = constants;
AI_FILELOCATION = getAIPath();
initalizeClassloader();
makeAiList();
}
/**
* Adds a physics instance to this class
*
* @param physics the Physics object that should be used
*/
public void setPhysics(Physics physics) {
this.physics = physics;
}
/**
* This method creates the ai path, depending on the path of this class.
*
* @return The file location of the ai.
*/
private String getAIPath() {
String fileLoc = null;
try {
fileLoc =
URLDecoder.decode(
AIManager.class.getProtectionDomain().getCodeSource().getLocation().getPath(),
"UTF-8");
} catch (UnsupportedEncodingException e) {
// if this exception is thrown further executing the framework
// makes no sense, so throw a runtime exception
throw new RuntimeException("Invalid encoding chosen for URLDecoder.");
}
// if everything is packaged in a jar file remove the last part and add the ai folder
if (fileLoc.endsWith(".jar")) {
int index = fileLoc.lastIndexOf("/");
fileLoc = fileLoc.substring(0, index + 1) + constants.getAILocation();
// if the program is run without jar file just append the ai folder one step over in the hierarchy
} else {
fileLoc += constants.getAILocation();
}
return fileLoc;
}
/**
* this function initializes the classloader.
*
* @throws MalformedURLException Could appear if the Constants.AI_LOCATION
* was malformed
*/
private void initalizeClassloader() throws MalformedURLException {
File fileloc = new File(AI_FILELOCATION);
final URL[] url = new URL[1];
url[0] = fileloc.toURI().toURL();
AccessController.doPrivileged(
(PrivilegedAction<Object>)
() -> {
loader = new URLClassLoader(url);
return null;
});
}
/**
* This method creates the list of the possible AIs.
*/
private void makeAiList() {
// create Loader
File fileloc = new File(AI_FILELOCATION);
File[] classes = fileloc.listFiles();
// if there are no files, classes will be null. (See JavaManual)
if (classes == null) {
return;
}
Arrays.stream(classes)
.map(f -> f.getName())
// only add ais if they are valid (extend SphereMiner2015 class)
.filter(f -> f.endsWith(".class"))
.map(f -> f.split(".class")[0])
.filter(f -> isValidAi(f))
.forEach(f -> aiList.add(f));
}
/**
* Recreate the list of AI's that could be used for playing
*/
public void reloadAIList() {
aiList.clear();
makeAiList();
}
/**
* This method checks if the selected class is of a valid AI type.
*
* @param path The path to the selected class.
* @return The boolean result, if the class is correct.
*/
private boolean isValidAi(final String path) {
boolean validAi = true;
Class<?> loadedAI;
try {
loadedAI = loader.loadClass(path);
// check if the ai implements the AI interface
// in this case, the ai is valid because it implements the AI
// interface, so the isValidAi method can instantly return true
// without proceeding the check
validAi = loadedAI.getSuperclass().getName().equals(SphereMiners2015.class.getName());
} catch (ClassNotFoundException e) {
// do not throw an exception this method should check if the ai
// is valid, so if not its not necessary to throw an exception
validAi = false;
}
return validAi;
}
/**
* This method returns the complete list of AIs which can be chosen to
* simulate a game.
*
* @return The available AIs.
*/
public ObservableList<String> getAIList() {
return aiList;
}
/**
* This method initializes the AIs which should play against each other in
* the next simulation.
*
* @param aisToPlay The list of AI's that should play against each other
* @return the mapping of ais to their loading status
*/
public Map<Player, LoadingStatus> initializeGameAIs(final List<Player> aisToPlay) {
// cleaning up the list of the last ais.
ais.clear();
Map<Player, LoadingStatus> retVal = new HashMap<>();
aisToPlay
.stream()
.filter(
ai -> {
if (isValidAi(ai.getInternalName())) return true;
retVal.put(ai, LoadingStatus.INVALID_LOCATION);
return false;
})
.parallel()
.forEach(
ai -> {
if (loadAI(ai, loader)) retVal.put(ai, LoadingStatus.LOADED);
else retVal.put(ai, LoadingStatus.INITIALIZING_FAILED);
});
return retVal;
}
/**
* This method loads and initializes an AI if possible.
*
* @param player he player which should be initialized
* @param loader the loader which is used for initialization
* @return indicates if the loading process was successful
*/
private boolean loadAI(final Player player, final URLClassLoader loader) {
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Boolean> future =
exec.submit(
(Callable<Boolean>)
() -> {
Class<?> cl;
try {
cl = loader.loadClass(player.getInternalName());
} catch (ClassNotFoundException e) {
// do nothing, exception is handled in another method
return false;
}
// search for constructor with zero arguments, and make it
// accessible
for (Constructor<?> ct : cl.getConstructors()) {
if (ct.getParameterTypes().length == 0) {
ct.setAccessible(true);
try {
SphereMiners2015 loaded = (SphereMiners2015) ct.newInstance();
loaded.setPlayer(player);
loaded.setPhysics(physics);
loaded.setConstants(constants);
loaded.init();
ais.put(player, loaded);
return true;
} catch (
InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException
e) {
// if any of these errors occured the ai could not
// be loaded properly, so the method returns without
// doing anything
}
}
}
return false;
});
try {
if (future.get(constants.getAIComputationTime(), TimeUnit.MILLISECONDS)) {
return true;
} else {
return false;
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
// nothing special to do here, just ignore the exceptions and cancel
// the task
future.cancel(true);
ais.remove(player);
constants
.getLogger()
.log(
Level.INFO, "AI " + player.getInternalName() + " could not be initialized properly.");
}
return false;
}
/**
* This method lets all AIs compute one step. If an AIs calculation lasts
* too long, it is terminated and reinitialized again.
*/
public void applyMoves() {
ais.entrySet()
.parallelStream() // compute in parallel if possible
.map(e -> Pair.of(e.getKey(), e.getValue().evaluateTurn())) // evaluate the turns
.filter(p -> !p.getSecond()) // get those ais who did not finish successfully
.forEach(p -> reinitializeAi(p.getFirst())); // and reinitialize them
}
/**
* This method reinitializes an AI.
*
* @param ai determinate which AI should be reinitialized.
*/
private void reinitializeAi(Player ai) {
try {
Class<?> cl = loader.loadClass(ais.get(ai).getClass().getName());
ais.remove(ai);
SphereMiners2015 newAi = (SphereMiners2015) cl.newInstance();
newAi.setPlayer(ai);
newAi.setPhysics(physics);
newAi.setConstants(constants);
newAi.init();
ais.put(ai, newAi);
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e1) {
constants
.getLogger()
.log(
Level.SEVERE,
"AI "
+ ai.getInternalName()
+ " could not be reinitialized, ai is removed from the game");
ais.remove(ai);
throw new Error("Reinitialization of " + ais.get(ai).getClass().getName() + " FAILED!");
}
}
public enum LoadingStatus {
INVALID_LOCATION,
INITIALIZING_FAILED,
LOADED;
}
}