package org.pixelgaffer.turnierserver.gamelogic;
import java.io.IOException;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.pixelgaffer.turnierserver.Airbrake;
import org.pixelgaffer.turnierserver.GsonGzipParser;
import org.pixelgaffer.turnierserver.Logger;
import org.pixelgaffer.turnierserver.Parser;
import org.pixelgaffer.turnierserver.Parsers;
import org.pixelgaffer.turnierserver.gamelogic.interfaces.Ai;
import org.pixelgaffer.turnierserver.gamelogic.interfaces.AiObject;
import org.pixelgaffer.turnierserver.gamelogic.interfaces.Game;
import org.pixelgaffer.turnierserver.gamelogic.messages.GameFinished;
import org.pixelgaffer.turnierserver.gamelogic.messages.RenderData;
import lombok.Getter;
import lombok.Setter;
/**
* @param <E>
* Das AiObject
* @param <R>
* Die Antwort der Ai
*/
public abstract class GameLogic<E extends AiObject, R> {
public static final Logger logger = new Logger();
/**
* Gibt an, wie viele Runden gespielt werden. -1 bei unbegrenzt vielen Runden.
*/
protected int maxTurns = -1;
/**
* Gibt an, wie viele Runden schon gespielt wurden
*/
protected int playedRounds;
/**
* Das Spiel, welches von dieser GameLogic geleitet wird
*/
protected Game game;
/**
* Der Fortschritt, welcher angezeigt werden soll, -1, wenn keiner angezeigt werden soll
*/
protected double progress = -1;
/**
* Der Display-String, welcher angezeigt werden soll
*/
protected String display;
/**
* Gibt an ob das Spiel schon beendet worden ist
*/
protected boolean gameEnded = false;
protected DecimalFormat df = new DecimalFormat("#.##");
@Getter
@Setter
protected boolean started;
private List<Ai> crashed = new ArrayList<>();
public GameLogic() {
df.setRoundingMode(RoundingMode.HALF_UP);
}
/**
* Erstellt das Spiel, inseriert alle Keys in den Gamestate (falls dieser
* verwendet wird)
*/
protected abstract void setup();
/**
* Wird aufgerufen, wenn eine Nachricht empfangen wird
*
* @param response
* Die Antwort der AI
* @param ai
* Die AI, welche diese Antwort gesendet hat
* @param passedMikros
* Die vergangenen Mikros zwischen Antwort eingang und Antwort ausgang bei der KI
*/
protected abstract void receive(R response, Ai ai, int passedMikros);
/**
* Wird aufgerufen, wenn eine AI aufgegeben hat (oder aufgegeben wurde, z.B.
* aufgrund illegaler Aktionen oder wenn keine Rechenpunkte mehr übrig sind)
*
* @param ai
* Die AI, welche aufgegeben hat
*/
public abstract void lost(Ai ai);
/**
* Erstellt ein neues AIWrapper Objekt
*
* @param ai
* Die AI, für die das Objekt erstellt werden soll
* @return Das AI Objekt
*/
protected abstract E createUserObject(Ai ai);
/**
* Wird aufgerufen, wenn endGame() aufgerufen wird. Hier kann Score zeugs
* implementiert werden.
*/
protected abstract void gameFinished();
/**
* Wird in startGame() nach setup() aufgerufen, muss eine erste RenderData schicken
*/
protected abstract void sendFirstRenderData();
/**
* @return Das an isolate übergebene timeout, in Sekunden
*/
public abstract float aiTimeout();
/**
* Parsed die Antwort der Ki
*
* @param string Die Antwort der Ki
* @return Das geparste Objekt
*/
protected abstract R parse(String string) throws ParseException;
/**
* Castet das User Object der AI (Util-Methode)
*
* @param ai
* Die AI, deren Objekt gecastet werden soll
* @return Das gecastete User Object
*/
@SuppressWarnings("unchecked")
protected E getUserObject(Ai ai) {
return (E) ai.getObject();
}
/**
* Wird aufgerufen, wenn eine Nachricht empfangen wurde
*
* @param message
* Die Nachricht
* @param ai
* Die AI, von welcher die Nachricht kommt
*/
public synchronized void receiveMessage(byte[] message, Ai ai) {
// logger.debug("Nachricht empfangen: " + new String(message));
if(gameEnded) {
logger.warning("Game already ended!");
return;
}
if (getUserObject(ai).lost) {
logger.warning("Ai already lost!");
return;
}
String string = null;
try {
Parser parser = Parsers.getWorker();
if(parser instanceof GsonGzipParser) {
string = new String(((GsonGzipParser)parser).uncompress(message), "UTF-8");
}
else {
string = new String(message, "UTF-8");
}
} catch (IOException e1) {
Airbrake.log(e1).printStackTrace();
getUserObject(ai).loose("Die Nachricht der KI konnte nicht gelesen werden");
return;
}
//Wenn der erste Buchstabe eine Zahl ist, wird die Zahl ausgelesen und geparsed
int passedMikros = 0;
if(string.length() > 0 && Character.isDigit(string.charAt(0))) {
passedMikros = Integer.parseInt(string.substring(0, string.indexOf('|')));
string = string.substring(string.indexOf('|') + 1);
}
else {
string = string.substring(1);
}
if (string.equals("SURRENDER")) {
getUserObject(ai).loose("Die KI hat Aufgegeben");
return;
}
if (string.startsWith("CRASH ")) {
getUserObject(ai).loose("Die KI ist gecrashed: " + string.substring("CRASH ".length()).replace("\\\\", "\\").replace("\\n", "\n"));
return;
}
// logger.debug("Empfangen: " + string);
try {
receive(parse(string), ai, passedMikros);
} catch (ParseException e) {
getUserObject(ai).loose(Airbrake.log("Es trat ein Fehler beim parsen der Nachricht auf: " + string + "!"));
}
}
/**
* Sendet ein Objekt an das Frontend
*
* @param object
* Das Objekt, das gesendet werden soll
*/
public void sendToFronted(Object object) {
if(gameEnded) {
return;
}
try {
game.getFrontend().sendMessage(Parsers.getFrontend().parse(object, false));
} catch (IOException e) {
Airbrake.log(e).printStackTrace();
}
}
/**
* Das Renderupdate
*/
protected int update = 1;
/**
* Sendet die Daten zum rendern an das Frontend
*
* @param data
*/
public void sendRenderData(Object data) {
if(gameEnded) {
return;
}
RenderData renderData = new RenderData();
renderData.update = update;
update++;
renderData.data = data;
renderData.requestid = game.getFrontend().getRequestId();
renderData.progress = progress;
renderData.gameId = game.getUuid();
if (display != null) {
renderData.display = display;
}
renderData.calculationPoints = new HashMap<>();
renderData.points = new HashMap<>();
for(Ai ai : game.getAis()) {
renderData.calculationPoints.put(ai.getId(),getUserObject(ai).mikrosLeft / 1000f);
renderData.points.put(ai.getId(), getUserObject(ai).score);
}
sendToFronted(renderData);
}
/**
* Sendet ein Objekt an die AI
*
* @param object
* Das Objekt, das gesendet werden soll
* @param ai
* Die AI, der das Objekt gesendet werden soll
* @throws IOException
*/
public void sendToAi(Object object, Ai ai) throws IOException {
if(gameEnded) {
return;
}
ai.sendMessage((object + "\n").getBytes(StandardCharsets.UTF_8));
}
/**
* Beendet das Spiel (Die Scores müssen davor gesetzt werden!)
*/
public void endGame(String reason) {
if(gameEnded) {
return;
}
gameFinished();
GameFinished message = new GameFinished();
message.leftoverMillis = new HashMap<>();
message.scores = new HashMap<>();
message.position = new HashMap<>();
message.requestid = game.getFrontend().getRequestId();
message.reason = reason;
message.gameId = game.getUuid();
List<Integer> scores = new ArrayList<>();
for (Ai ai : game.getAis()) {
scores.add(getUserObject(ai).score);
}
scores = scores.stream().distinct().sorted(Collections.reverseOrder()).collect(Collectors.toList());
for (Ai ai : game.getAis()) {
message.leftoverMillis.put(ai.getId(), getUserObject(ai).mikrosLeft);
message.scores.put(ai.getId(), getUserObject(ai).score);
message.position.put(ai.getId(), scores.indexOf(getUserObject(ai).score) + 1);
}
sendToFronted(message);
gameEnded = true;
try {
game.finishGame();
} catch (IOException e) {
Airbrake.log(e).printStackTrace();
}
}
/**
* Startet das Spiel
*
* @param game
* Das Spiel, welches gestartet werden soll
*/
public void startGame(Game game) {
this.game = game;
for (Ai ai : game.getAis()) {
ai.setObject(createUserObject(ai));
ai.getObject().setAi(ai);
getUserObject(ai).setLogic(this);
getUserObject(ai).setAi(ai);
}
while(!crashed.isEmpty()) {
getUserObject(crashed.get(0)).loose("Die Ki hat sich am Anfang des Spieles beendet");
if(gameEnded) {
return;
}
crashed.remove(0);
}
setup();
sendFirstRenderData();
setStarted(true);
}
/**
* Erhöht playedRounds um 1
*/
public void round() {
playedRounds++;
}
/**
* Gibt zurück, ob die Anzahl an gespielten Runden der Anzahl an zu spielenden Runden ist
*
* @return True, wenn alle zu spielenden Runden gespielt wurden
*/
public boolean allRoundsPlayed() {
return playedRounds == maxTurns && maxTurns > 0;
}
/**
* @return Die Anzahl an Spielern, für welche dieses Spiel ausgelegt ist
*/
public int playerAmt() {
return 2;
}
/**
* Teilt der Spiellogik mit, wenn sich eine Ai disconnected
*
* @param ai Die Ai, die sich disconnected hat
*/
public void aiCrashed(Ai ai) {
if(getUserObject(ai) == null) {
crashed.add(ai);
return;
}
if(!getUserObject(ai).lost) {
getUserObject(ai).loose("Die Ki hat sich disconnected. Dies kann daran liegen, dass ein Crash nicht abgefangen wurde, oder dass die Ki ihre verfügbare Zeit verbraucht hat und von der Sandbox terminiert wurde.");
}
}
public Game getGame() {
return game;
}
}