diff --git a/.idea/misc.xml b/.idea/misc.xml index 2ac9e35931b9ef45775c1e4d81e72f9e331a2135..2f57a3fb3ab327fb51536ae1f2d469f4652a1cad 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="MavenProjectsManager"> @@ -8,7 +7,7 @@ </list> </option> </component> - <component name="ProjectRootManager" version="2" languageLevel="JDK_20" project-jdk-name="temurin-20" project-jdk-type="JavaSDK"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_19" project-jdk-name="temurin-17" project-jdk-type="JavaSDK"> <output url="file://$PROJECT_DIR$/out" /> </component> </project> \ No newline at end of file diff --git a/src/main/java/de/fossag/hackatron/AlgConfig.java b/src/main/java/de/fossag/hackatron/AlgConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..9e4d1778e5dace47de43229bcbd405ca2c87021e --- /dev/null +++ b/src/main/java/de/fossag/hackatron/AlgConfig.java @@ -0,0 +1,8 @@ +package de.fossag.hackatron; + +public class AlgConfig { + + public static final int maxDepth = 2; // How far we look into the future + public static final int dfsBestScore = 23; // Which score is considered "best" to stop searching early + +} diff --git a/src/main/java/de/fossag/hackatron/GameEngine.java b/src/main/java/de/fossag/hackatron/GameEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..25bedb83a2d23d9e4b2cf3e13c119cedb8d75dc2 --- /dev/null +++ b/src/main/java/de/fossag/hackatron/GameEngine.java @@ -0,0 +1,67 @@ +package de.fossag.hackatron; + +import java.util.*; + +public class GameEngine { + public enum Move { + Up("up"), Down("down"), Left("left"), Right("right"); + private final String name; + + Move(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + public static Move generateNextMove(GameState gameState) { + ArrayList<Map.Entry<Move, Integer>> bestMoves = new ArrayList<>(AlgConfig.maxDepth); + double[] weightedScores = new double[AlgConfig.maxDepth]; + for (int i = AlgConfig.maxDepth-1; i >= 0; i--) { + Map.Entry<Move, Integer> currentMove = dfs(gameState.ownX, gameState.ownY, gameState.getMap(), 0); + + bestMoves.add(0, currentMove); + weightedScores[i] = currentMove.getValue()/(Math.min(Math.pow(2,i+1),AlgConfig.dfsBestScore)); + + gameState.cleanupMap(-i-2); + } + + int highestScoreIndex = weightedScores.length-1; + for (int i = weightedScores.length - 2; i >= 0; i--) { + if (weightedScores[i] > weightedScores[highestScoreIndex]) { + highestScoreIndex = i; + } + } + + return bestMoves.get(highestScoreIndex).getKey(); + } + + public static Map.Entry<Move, Integer> dfs(int x, int y, Integer[][] map, int depth) { + int bestMoveScore = 0; + Move bestMove = Move.Up; + Move[] moves = Move.values(); + if(depth == 0) { + List<Move> listOfMoves = Arrays.asList(moves); + Collections.shuffle(listOfMoves); + moves = listOfMoves.toArray(new Move[0]); + } + for (Move m : moves) { + int[] nextPos = GameUtils.getNextPosition(map, x, y, m); + if (map[nextPos[0]][nextPos[1]] != null) continue; + map[nextPos[0]][nextPos[1]] = -1; + int score = dfs(nextPos[0], nextPos[1], map, depth+1).getValue() + 1; + map[nextPos[0]][nextPos[1]] = null; + if (score > bestMoveScore) { + if (score > AlgConfig.dfsBestScore) { + return Map.entry(m, score); + } + bestMoveScore = score; + bestMove = m; + } + } + return Map.entry(bestMove, bestMoveScore); + } +} diff --git a/src/main/java/de/fossag/hackatron/GameState.java b/src/main/java/de/fossag/hackatron/GameState.java new file mode 100644 index 0000000000000000000000000000000000000000..f2f0b79777e9df0a22772cee06f345295270c8c1 --- /dev/null +++ b/src/main/java/de/fossag/hackatron/GameState.java @@ -0,0 +1,93 @@ +package de.fossag.hackatron; + +import java.util.HashMap; + +public class GameState { + public int ownID, ownX, ownY; + public GameEngine.Move lastMove = GameEngine.Move.Up; + private int mapWidth, mapHeight; + private final Integer[][] map; //[width][height] + private final HashMap<Integer, String> playerNames; + + public GameState(int mapWidth, int mapHeight, int ownID) { + playerNames = new HashMap<>(); + this.ownID = ownID; + map = new Integer[mapWidth][mapHeight]; + + this.mapWidth = mapWidth; + this.mapHeight = mapHeight; + } + + public Integer[][] getMap() { + return map; + } + + public void cleanupMap(int untilPriority) { + for (int x = 0; x < mapWidth; x++) { + for (int y = 0; y < mapHeight; y++) { + if (map[x][y] != null && map[x][y] <= untilPriority) map[x][y] = null; + } + } + } + + public void addPos(int id, int x, int y) { + if (x < 0 | y < 0 | x >= mapWidth || y >= mapHeight) { + throw new UnsupportedOperationException("Position: (" + x + "," + y + ") out of bounds"); + } + map[x][y] = id; + + if (id == ownID) return; + addLookaheadPosRec(x, y, 1); + } + + public void addLookaheadPosRec(int x, int y, int depth) { + if (depth >= AlgConfig.maxDepth) return; + + for (GameEngine.Move m : GameEngine.Move.values()) { + int[] nextPos = GameUtils.getNextPosition(getMap(), x, y, m); + int newX = nextPos[0]; + int newY = nextPos[1]; + Integer currentCell = map[newX][newY]; + + if (currentCell == null || currentCell < -1 - depth) { + map[newX][newY] = -1 - depth; + addLookaheadPosRec(newX, newY, depth + 1); + } + } + } + + public void setPlayerInfo(int playerId, String name) { + playerNames.put(playerId, name); + } + + public void printMap() { + System.out.println("------------"); + for (int y = 0; y < mapHeight; y++) { + System.out.print("|"); + for (int x = 0; x < mapWidth; x++) { + Integer currentPixel = map[x][y]; + if(currentPixel == null) { + System.out.print(" "); + } else if(currentPixel == ownID) { + System.out.print("X"); + } else { + System.out.print((char) (map[x][y] + 64+1)); + } + } + System.out.println("|"); + } + System.out.println("------------"); + } + + public void playerDies(int playerId) { + playerNames.remove(playerId); + for (int i = 0; i < mapWidth; i++) { + for (int j = 0; j < mapHeight; j++) { + Integer val = map[i][j]; + if (val != null && val == playerId) { + map[i][j] = null; + } + } + } + } +} diff --git a/src/main/java/de/fossag/hackatron/GameUtils.java b/src/main/java/de/fossag/hackatron/GameUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..5609737fee565c08fb3a48e660a5eb7e2c87107b --- /dev/null +++ b/src/main/java/de/fossag/hackatron/GameUtils.java @@ -0,0 +1,21 @@ +package de.fossag.hackatron; + +public class GameUtils { + + public static int[] getNextPosition(Integer[][] map, int x, int y, GameEngine.Move m) { + int mapWidth = map.length; + int mapHeight = map[0].length; + + int newX = x; + int newY = y; + switch (m) { + case Up -> newY = (y - 1 + mapHeight) % mapHeight; + case Left -> newX = (x - 1 + mapWidth) % mapWidth; + case Right -> newX = (x + 1) % mapWidth; + case Down -> newY = (y + 1) % mapHeight; + } + + return new int[]{newX, newY}; + } + +} diff --git a/src/main/java/de/fossag/hackatron/HackatronClient.java b/src/main/java/de/fossag/hackatron/HackatronClient.java index 9989ba40fd1daab4959b14492857f8ddc1224d8c..9661283ac6b53bab852f6f478da61831b9d5a0e6 100644 --- a/src/main/java/de/fossag/hackatron/HackatronClient.java +++ b/src/main/java/de/fossag/hackatron/HackatronClient.java @@ -2,30 +2,81 @@ package de.fossag.hackatron; public class HackatronClient implements IHackatronClient { - private IMessageSender messageSender; - private static final String CLIENT_NAME = "dummyclient"; - private static final String CLIENT_SECRET = "changeme"; + private IMessageSender messageSender; + private static final String CLIENT_NAME = "Schatten"; + private static final String CLIENT_SECRET = "ThisIsASuperSecretClientSecretToMakeSureNoOneStealsOurClient"; + private int losses = 0; + private int wins = 0; - @Override - public void setMessageSender(IMessageSender messageSender) { - this.messageSender = messageSender; - } + GameState game; - @Override - public void onMessage(String message) { - String[] parts = message.split("\\|"); - String messageType = parts[0]; + @Override + public void setMessageSender(IMessageSender messageSender) { + this.messageSender = messageSender; + } + + long lastTick = System.nanoTime(); + int curTick = 0; + @Override + public void onMessage(String message) { + String[] parts = message.split("\\|"); + String messageType = parts[0]; - switch (messageType) { - case "motd": - messageSender.send("join|" + CLIENT_NAME + "|" + CLIENT_SECRET); - break; - case "tick": - messageSender.send("move|up"); - break; - default: - System.out.println("Unknown message type :("); - System.exit(1); + switch (messageType) { + case "motd": + messageSender.send("join|" + CLIENT_NAME + "|" + CLIENT_SECRET); + break; + case "tick": + long timeStart = System.nanoTime(); + game.lastMove = GameEngine.generateNextMove(game); + long timeEnd = System.nanoTime(); + //System.out.println("Time spent: " + (timeEnd - timeStart) / 1_000_000.0 + "ms, going " + game.lastMove); + messageSender.send("move|" + game.lastMove); + game.cleanupMap(-2); + curTick++; + //System.out.println("Last tick time: "+(timeStart-lastTick)/1_000_000.0); + //System.out.println("Expected Tick: "+(1/(Math.ceil(curTick/10.d)))); + lastTick = timeStart; + //if(game != null) game.printMap(); + break; + case "game": + if (wins + losses > 0) + System.out.printf("Starting a new game, current stats: %d-%d, %d\n", wins, losses, wins / (wins + losses)); + game = new GameState(Integer.parseInt(parts[1]), Integer.parseInt(parts[2]), Integer.parseInt(parts[3])); + curTick = 0; + break; + case "pos": + int id = Integer.parseInt(parts[1]); + int x = Integer.parseInt(parts[2]); + int y = Integer.parseInt(parts[3]); + game.addPos(id, x, y); + if (id == game.ownID) { + game.ownX = x; + game.ownY = y; + } + break; + case "player": + game.setPlayerInfo(Integer.parseInt(parts[1]), parts[2]); + break; + case "die": + System.out.println(parts[1] + " has died"); + game.playerDies(Integer.parseInt(parts[1])); + break; + case "lose": + System.out.printf("LOST! Died at %d|%d, last move: %s\n", game.ownX, game.ownY, game.lastMove); + game.printMap(); + losses++; + break; + case "win": + System.out.println("WON!"); + wins++; + break; + case "error": + System.out.println("SERVER ERROR: " + parts[1]); + break; + default: + System.out.println("Unknown message type :("); + System.exit(1); + } } - } } diff --git a/src/main/java/de/fossag/hackatron/HackatronWrapper.java b/src/main/java/de/fossag/hackatron/HackatronWrapper.java index 7adf7f33badc183bcb887d0a5cbdab2e47aa172f..9b42df5f55e5ffd9dc0b42217737283be15ebcb2 100644 --- a/src/main/java/de/fossag/hackatron/HackatronWrapper.java +++ b/src/main/java/de/fossag/hackatron/HackatronWrapper.java @@ -14,46 +14,44 @@ import java.net.Socket; */ public class HackatronWrapper { - private String host; - private int port; - - private IHackatronClient hackatronClient; - - public HackatronWrapper(String host, int port, IHackatronClient hackatronClient) { - this.host = host; - this.port = port; - this.hackatronClient = hackatronClient; - } - - public void run() throws IOException { - System.out.println("Connecting to " + host + ":" + port); - - // Set up socket and wire up streams - Socket socket = new Socket(host, port); - OutputStream out = socket.getOutputStream(); - PrintWriter writer = new PrintWriter(out, true); - InputStream in = socket.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); - - System.out.println("Connection successful"); - - // The message sender logs the outgoing message and stuffs it into the output stream - hackatronClient.setMessageSender(s -> { - System.out.println("OUT MSG: " + s); - writer.println(s); - }); - - while (true) { - if (socket.isClosed()) { - return; - } - String line = reader.readLine(); - if (line == null) { - return; - } - System.out.println("IN MSG: " + line); - hackatronClient.onMessage(line); + private String host; + private int port; + + private IHackatronClient hackatronClient; + + public HackatronWrapper(String host, int port, IHackatronClient hackatronClient) { + this.host = host; + this.port = port; + this.hackatronClient = hackatronClient; } - } + public void run() throws IOException { + System.out.println("Connecting to " + host + ":" + port); + + // Set up socket and wire up streams + Socket socket = new Socket(host, port); + OutputStream out = socket.getOutputStream(); + PrintWriter writer = new PrintWriter(out, true); + InputStream in = socket.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + + System.out.println("Connection successful"); + + // The message sender logs the outgoing message and stuffs it into the output stream + hackatronClient.setMessageSender(s -> { + writer.println(s); + }); + + while (true) { + if (socket.isClosed()) { + return; + } + String line = reader.readLine(); + if (line == null) { + return; + } + hackatronClient.onMessage(line); + } + + } } diff --git a/src/main/java/de/fossag/hackatron/IHackatronClient.java b/src/main/java/de/fossag/hackatron/IHackatronClient.java index 82a8589c0939335280ba4ac6bd6ad621820f5704..e4fba652ea8776e6860a0a632746f6a94d4bdf87 100644 --- a/src/main/java/de/fossag/hackatron/IHackatronClient.java +++ b/src/main/java/de/fossag/hackatron/IHackatronClient.java @@ -2,20 +2,20 @@ package de.fossag.hackatron; public interface IHackatronClient { - /** - * Gets called one time after instance creation to set the {@link IMessageSender} instance. You - * can use that to send reply messages back to the game server. - * - * @param messageSender IMessageSender instance - */ - void setMessageSender(IMessageSender messageSender); + /** + * Gets called one time after instance creation to set the {@link IMessageSender} instance. You + * can use that to send reply messages back to the game server. + * + * @param messageSender IMessageSender instance + */ + void setMessageSender(IMessageSender messageSender); - /** - * Gets called every time a new message from the game server arrives. - * - * See PROTOCOL.md for details - * - * @param message Message string - */ - void onMessage(String message); + /** + * Gets called every time a new message from the game server arrives. + * <p> + * See PROTOCOL.md for details + * + * @param message Message string + */ + void onMessage(String message); } diff --git a/src/main/java/de/fossag/hackatron/IMessageSender.java b/src/main/java/de/fossag/hackatron/IMessageSender.java index 1ff86423e6477931a31b48d88de58aeaa77bed92..5e9fec30191c958efc16f5485ac15c1d95bf066d 100644 --- a/src/main/java/de/fossag/hackatron/IMessageSender.java +++ b/src/main/java/de/fossag/hackatron/IMessageSender.java @@ -2,5 +2,5 @@ package de.fossag.hackatron; public interface IMessageSender { - void send(String message); + void send(String message); } diff --git a/src/main/java/de/fossag/hackatron/Main.java b/src/main/java/de/fossag/hackatron/Main.java index a69d6420dd0b0d06392328737cced5585f83aca4..b6f878a51a1a0338fde3795c5545349d1b65ee27 100644 --- a/src/main/java/de/fossag/hackatron/Main.java +++ b/src/main/java/de/fossag/hackatron/Main.java @@ -4,11 +4,11 @@ import java.io.IOException; public class Main { - public static void main(String[] args) throws IOException { - HackatronWrapper client = new HackatronWrapper( - "game.hackatron.de", - 4000, - new HackatronClient()); - client.run(); - } + public static void main(String[] args) throws IOException { + HackatronWrapper client = new HackatronWrapper( + "game.hackatron.de", + 4000, + new HackatronClient()); + client.run(); + } } \ No newline at end of file