| @@ -1,16 +1,36 @@ | ||
| package com.teamquixote.ai.actions; | ||
|
|
||
| import com.teamquixote.ai.io.GameStateData; | ||
| import org.json.JSONObject; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| public abstract class Action { | ||
| public abstract String getType(); | ||
|
|
||
| public abstract void execute(GameStateData gameStateData); | ||
|
|
||
| public JSONObject toJSON() { | ||
| JSONObject jsonObject = new JSONObject(); | ||
| jsonObject.put("type", getType()); | ||
|
|
||
| return jsonObject; | ||
| } | ||
|
|
||
| public static List<Action> getValidActions(GameStateData gameStateData) { | ||
| List<Action> validActions = new ArrayList<>(); | ||
|
|
||
| int heroPos = gameStateData.getHeroPosition(); | ||
|
|
||
| //if on an exit, there aren't any moves you can do until the next level loads | ||
| if (gameStateData.isPositionExit(heroPos)) | ||
| return validActions; | ||
|
|
||
| for (int adj : GameStateData.Utilities.getAdjacent(heroPos)) | ||
| if (gameStateData.isPositionPassable(adj)) | ||
| validActions.add(new Move(GameStateData.Utilities.getDx(heroPos, adj), GameStateData.Utilities.getDy(heroPos, adj))); | ||
|
|
||
| return validActions; | ||
| } | ||
| } |
| @@ -0,0 +1,40 @@ | ||
| package com.teamquixote.ai.actions; | ||
|
|
||
| import com.teamquixote.ai.io.GameStateData; | ||
| import com.watabou.pixeldungeon.Dungeon; | ||
| import org.json.JSONObject; | ||
|
|
||
| public class Move extends Action { | ||
| /** | ||
| * number of tiles offset relative to the hero's position (left and up are negative) | ||
| */ | ||
| public final int dX, dY; | ||
|
|
||
| public Move(int dX, int dY) { | ||
| this.dX = dX; | ||
| this.dY = dY; | ||
| } | ||
|
|
||
| @Override | ||
| public String getType() { | ||
| return "move"; | ||
| } | ||
|
|
||
| @Override | ||
| public void execute(GameStateData gameStateData) { | ||
| int target = gameStateData.getHeroPosition() + dX + (dY*GameStateData.Utilities.DUNGEON_WIDTH); | ||
| //stole this from GameScene.defaultCellListener | ||
| if (Dungeon.hero.handle(target)) { | ||
| Dungeon.hero.next(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public JSONObject toJSON() { | ||
| JSONObject jsonObject = super.toJSON(); | ||
| jsonObject.put("dX", dX); | ||
| jsonObject.put("dY", dY); | ||
|
|
||
| return jsonObject; | ||
| } | ||
| } |
| @@ -0,0 +1,16 @@ | ||
| package com.teamquixote.ai.actions; | ||
|
|
||
| import com.teamquixote.ai.io.GameStateData; | ||
| import com.watabou.pixeldungeon.Dungeon; | ||
|
|
||
| public class Wait extends Action { | ||
| @Override | ||
| public String getType() { | ||
| return "wait"; | ||
| } | ||
|
|
||
| @Override | ||
| public void execute(GameStateData gameStateData) { | ||
| Dungeon.hero.rest(false); | ||
| } | ||
| } |
| @@ -0,0 +1,8 @@ | ||
| package com.teamquixote.ai.agents; | ||
|
|
||
| import com.teamquixote.ai.actions.Action; | ||
| import com.teamquixote.ai.io.GameStateData; | ||
|
|
||
| public abstract class AiAgent { | ||
| public abstract Action makeDecision(GameStateData state); | ||
| } |
| @@ -0,0 +1,41 @@ | ||
| package com.teamquixote.ai.dungeons; | ||
|
|
||
| import com.teamquixote.ai.agents.AiAgent; | ||
| import com.teamquixote.ai.io.GameStateData; | ||
| import com.teamquixote.ai.launchers.EmptyPlatformSupport; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * saves all game states generated from the initial state | ||
| */ | ||
| public class PersistentDungeon extends AiPixelDungeon { | ||
| private final String saveDirectory; | ||
|
|
||
| private List<GameStateData> savedStates = new ArrayList<>(); | ||
|
|
||
| public PersistentDungeon(AiAgent ai, String saveDirectory, GameStateData initialState) { | ||
| super(new EmptyPlatformSupport(), ai); | ||
|
|
||
| // this.stateData = initialState == null ? new GameStateData() : initialState; | ||
| this.saveDirectory = saveDirectory + (saveDirectory.endsWith("\\") ? "" : "\\"); | ||
| } | ||
|
|
||
| @Override | ||
| protected void stateChanged(GameStateData state) { | ||
| savedStates.add(state); | ||
| } | ||
|
|
||
| @Override | ||
| protected void heroDied() { | ||
| for (GameStateData d : savedStates) { | ||
| try { | ||
| d.saveToDisk(saveDirectory + d.getId()); | ||
| } catch (IOException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,184 @@ | ||
| package com.teamquixote.ai.io; | ||
|
|
||
| import com.teamquixote.ai.actions.Action; | ||
| import com.watabou.pixeldungeon.levels.Terrain; | ||
| import org.json.JSONArray; | ||
| import org.json.JSONException; | ||
| import org.json.JSONObject; | ||
|
|
||
| import java.io.*; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Paths; | ||
| import java.util.UUID; | ||
|
|
||
| public class GameStateData { | ||
| private JSONObject data; | ||
|
|
||
| public GameStateData() { | ||
| data = new JSONObject(); | ||
| setId(UUID.randomUUID()); | ||
| } | ||
|
|
||
| public UUID getParentId() { | ||
| return (UUID) data.get("parentId"); | ||
| } | ||
|
|
||
| public void setParentId(UUID parentId) { | ||
| data.put("parentId", parentId); | ||
| } | ||
|
|
||
| public void setParentAction(Action action) { | ||
| data.put("parentAction", action.toJSON()); | ||
| } | ||
|
|
||
| public UUID getId() { | ||
| Object object = data.get("id"); | ||
| if (object instanceof UUID) { | ||
| return (UUID) object; | ||
| } | ||
|
|
||
| return UUID.fromString(object.toString()); | ||
| } | ||
|
|
||
| public void setId(UUID id) { | ||
| data.put("id", id); | ||
| } | ||
|
|
||
| public int getHeroPosition() { | ||
| return getHeroData().getInt("pos"); | ||
| } | ||
|
|
||
| public boolean isPositionExit(int pos) { | ||
| int posValue = getPositionValue(pos); | ||
| return posValue == Terrain.EXIT; | ||
| } | ||
|
|
||
| public boolean isPositionPassable(int pos) { | ||
| return (getPositionFlag(pos) & Terrain.PASSABLE) != 0; | ||
| } | ||
|
|
||
| /** | ||
| * TODO: parameterize this based on hero's class (if we ever get bored playing as the warrior" | ||
| */ | ||
| private static final String heroClassLabel = "warrior"; | ||
|
|
||
| private JSONObject getHeroData() { | ||
| return data.getJSONObject(heroClassLabel + ".dat").getJSONObject("hero"); | ||
| } | ||
|
|
||
| /** | ||
| * returns the equivalent of Dungeon.level.map[pos] | ||
| * | ||
| * @param pos | ||
| * @return | ||
| */ | ||
| private int getPositionValue(int pos) { | ||
| int currentLevel = getHeroData().getInt("lvl"); | ||
| return data.getJSONObject(heroClassLabel + currentLevel + ".dat") | ||
| .getJSONObject("level") | ||
| .getJSONArray("map") | ||
| .getInt(pos); | ||
| } | ||
|
|
||
| private int getPositionFlag(int pos) { | ||
| return Terrain.flags[getPositionValue(pos)]; | ||
| } | ||
|
|
||
| public GameStateData copy(){ | ||
| GameStateData copy = new GameStateData(); | ||
| copy.data = new JSONObject(data.toString()); | ||
|
|
||
| return copy; | ||
| } | ||
|
|
||
| public GameStateData createChild(Action action) { | ||
| GameStateData copy = copy(); | ||
| copy.setParentAction(action); | ||
| copy.setParentId(copy.getId()); | ||
| copy.setId(UUID.randomUUID()); | ||
|
|
||
| return copy; | ||
| } | ||
|
|
||
| public OutputStream saveSection(String sectionName) { | ||
| return new JsonObjectOutputStream(sectionName, data); | ||
| } | ||
|
|
||
| public InputStream loadSection(String sectionName) throws IOException { | ||
| try { | ||
| return new ByteArrayInputStream(data.getString(sectionName).getBytes()); | ||
| } catch (JSONException jsonE) { | ||
| throw new IOException("File " + sectionName + " doesn't exist"); | ||
| } | ||
| } | ||
|
|
||
| public void saveToDisk(String fileName) throws IOException { | ||
| try (FileWriter fw = new FileWriter(fileName)) { | ||
| fw.write(data.toString()); | ||
| } | ||
| } | ||
|
|
||
| public static GameStateData loadFromDisk(String fileName) throws IOException { | ||
| GameStateData gsd = new GameStateData(); | ||
| Path filePath = Paths.get(fileName); | ||
| byte[] bytes = Files.readAllBytes(filePath); | ||
| String fileString = new String(bytes); | ||
| gsd.data = new JSONObject(fileString); | ||
| return gsd; | ||
| } | ||
|
|
||
| private class JsonObjectOutputStream extends OutputStream implements AutoCloseable { | ||
|
|
||
| private final String key; | ||
| private final JSONObject data; | ||
|
|
||
| private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | ||
|
|
||
| public JsonObjectOutputStream(String key, JSONObject data) { | ||
| this.key = key; | ||
| this.data = data; | ||
| } | ||
|
|
||
| @Override | ||
| public void write(int b) throws IOException { | ||
| buffer.write(b); | ||
| } | ||
|
|
||
| @Override | ||
| public void flush() { | ||
| JSONObject flushed = new JSONObject(buffer.toString()); | ||
| data.put(key, flushed); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
| flush(); | ||
| } | ||
| } | ||
|
|
||
| public static class Utilities { | ||
| public static int DUNGEON_WIDTH = 32; | ||
|
|
||
| public static int[] getAdjacent(int position) { | ||
| return new int[]{ | ||
| position - DUNGEON_WIDTH, | ||
| position - DUNGEON_WIDTH + 1, | ||
| position + 1, | ||
| position + 1 + DUNGEON_WIDTH, | ||
| position + DUNGEON_WIDTH, | ||
| position - 1 + DUNGEON_WIDTH, | ||
| position - 1, | ||
| position - 1 - DUNGEON_WIDTH | ||
| }; | ||
| } | ||
|
|
||
| public static int getDx(int start, int end) { | ||
| return (end % DUNGEON_WIDTH)- (start % DUNGEON_WIDTH); | ||
| } | ||
|
|
||
| public static int getDy(int start, int end) { | ||
| return (end / DUNGEON_WIDTH) - (start / DUNGEON_WIDTH); | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,77 @@ | ||
| package com.teamquixote.ai.io; | ||
|
|
||
| import org.junit.Test; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import static org.junit.Assert.*; | ||
|
|
||
| public class GameStateDataTest { | ||
| private GameStateData getData(String filename) { | ||
| try { | ||
| //I don't know the "right" way to load resource files in Java... | ||
| String filePath = new File("src/com/teamquixote/ai/io/resources/" + filename).getAbsolutePath(); | ||
| return GameStateData.loadFromDisk(filePath); | ||
| } catch (IOException e) { | ||
| fail("error loading " + filename); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * some random game state from playing through | ||
| * @return | ||
| */ | ||
| private GameStateData getData1() { | ||
| return getData("data1.json"); | ||
| } | ||
|
|
||
| @Test | ||
| public void getHeroPosition() { | ||
| assertEquals(516, getData1().getHeroPosition()); | ||
| } | ||
|
|
||
| @Test | ||
| public void isPositionExit() { | ||
| GameStateData data = getData1(); | ||
| for (int i = 0; i < 1024; i++) | ||
| //there's only one exit, and it's at position 876 | ||
| assertTrue(i != 876 || data.isPositionExit(i)); | ||
| } | ||
|
|
||
| @Test | ||
| public void isPositionPassable(){ | ||
| GameStateData data = getData1(); | ||
| List<Integer> passables = new ArrayList<>(); | ||
| for (int i = 0; i < 1024; i++) { | ||
| if (data.isPositionPassable(i)) | ||
| passables.add(i); | ||
| } | ||
| /* | ||
| kinda lazy tests, but just saying that it'll return not an all-or-nothing value (which implies that it's kind of | ||
| at least doing something other than outright failure) A better test would show that a specific map value is | ||
| passable | ||
| */ | ||
| assertNotEquals(0, passables.size()); | ||
| assertNotEquals(1024, passables.size()); | ||
| } | ||
|
|
||
| @Test | ||
| public void getDx() { | ||
| assertEquals(-1, GameStateData.Utilities.getDx(50, 49)); | ||
| assertEquals(1, GameStateData.Utilities.getDx(50, 51)); | ||
| assertEquals(-1, GameStateData.Utilities.getDx(50, 17)); | ||
| assertEquals(1, GameStateData.Utilities.getDx(50, 83)); | ||
| } | ||
|
|
||
| @Test | ||
| public void getDy(){ | ||
| assertEquals(1, GameStateData.Utilities.getDy(10, 42)); | ||
| assertEquals(1, GameStateData.Utilities.getDy(10, 44)); | ||
| assertEquals(2, GameStateData.Utilities.getDy(10, 74)); | ||
| assertEquals(-1, GameStateData.Utilities.getDy(32, 0)); | ||
| } | ||
| } |
| @@ -1,4 +1,4 @@ | ||
| package com.teamquixote.ai.launchers; | ||
|
|
||
| import com.watabou.input.NoosaInputProcessor; | ||
|
|
||
| @@ -0,0 +1,12 @@ | ||
| package com.teamquixote.ai.launchers; | ||
|
|
||
| import com.watabou.utils.PDPlatformSupport; | ||
|
|
||
| /** | ||
| * dumb filler class to meet the PixelDungeon api requirement | ||
| */ | ||
| public class EmptyPlatformSupport extends PDPlatformSupport { | ||
| public EmptyPlatformSupport() { | ||
| super("", "", new AiInputProcessor()); | ||
| } | ||
| } |
| @@ -0,0 +1,18 @@ | ||
| package com.teamquixote.ai.launchers; | ||
|
|
||
| import com.badlogic.gdx.backends.lwjgl.LwjglApplication; | ||
| import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; | ||
| import com.teamquixote.ai.agents.Randy; | ||
| import com.teamquixote.ai.dungeons.PersistentDungeon; | ||
| import com.teamquixote.ai.io.GameStateData; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public class PersistentLauncher { | ||
| public static void main(String[] args) throws IOException { | ||
| String saveDirectory = args[0]; | ||
| GameStateData gameStateFile = args.length > 1 ? GameStateData.loadFromDisk(args[1]) : null; | ||
|
|
||
| new LwjglApplication(new PersistentDungeon(new Randy(), saveDirectory, gameStateFile), new LwjglApplicationConfiguration()); | ||
| } | ||
| } |