@@ -1,16 +1,36 @@
package com.teamquixote.ai.actions;

import com.teamquixote.ai.GameState;
import com.teamquixote.ai.io.GameStateData;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

public abstract class Action {
public abstract void execute();
public abstract String getType();

public abstract void execute(GameStateData gameStateData);

public double getActionCost(GameState state){
return 1.0;
public JSONObject toJSON() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("type", getType());

return jsonObject;
}

public int getUpdatedHeroPosition(GameState state) {
//implementing classes can override this if their actions cause a change in hero location
return state.heroPosition;
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;
}
}

This file was deleted.

@@ -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);
}
}

This file was deleted.

@@ -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);
}

This file was deleted.

This file was deleted.

@@ -1,8 +1,7 @@
package com.teamquixote.ai.agents;

import com.teamquixote.ai.AiAgent;
import com.teamquixote.ai.GameState;
import com.teamquixote.ai.actions.Action;
import com.teamquixote.ai.io.GameStateData;

import java.util.List;
import java.util.Random;
@@ -15,8 +14,8 @@ public class Randy extends AiAgent {
private Random random = new Random();

@Override
protected Action makeDecision(GameState state) {
List<Action> actions = state.getActions();
public Action makeDecision(GameStateData state) {
List<Action> actions = Action.getValidActions(state);
return actions.get(random.nextInt(actions.size()));
}
}

This file was deleted.

@@ -1,6 +1,8 @@
package com.teamquixote.ai;
package com.teamquixote.ai.dungeons;

import com.teamquixote.ai.actions.Action;
import com.teamquixote.ai.agents.AiAgent;
import com.teamquixote.ai.io.GameStateData;
import com.watabou.noosa.Game;
import com.watabou.pixeldungeon.Dungeon;
import com.watabou.pixeldungeon.PixelDungeon;
@@ -14,8 +16,14 @@
import com.watabou.pixeldungeon.windows.WndStory;
import com.watabou.utils.PDPlatformSupport;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class AiPixelDungeon extends PixelDungeon {
private final AiAgent ai;
protected final AiAgent ai;

private GameStateData currentState = new GameStateData();
private static final boolean showPerformanceStats = true;
Long startTime;
int totalActionsPlayed = 0;
@@ -48,9 +56,18 @@ protected void update() {
if (canAct()) {
if (startTime == null)
startTime = System.currentTimeMillis();
GameState state = new GameState();
Action a = ai.makeDecision(state);
a.execute();

try {
Dungeon.saveAll();
stateChanged(currentState.copy());
} catch (IOException e) {
e.printStackTrace();
}

Action a = ai.makeDecision(currentState);
currentState = currentState.createChild(a);
a.execute(currentState);

totalActionsPlayed++;
long elapsed = System.currentTimeMillis() - startTime;
if (showPerformanceStats) {
@@ -60,10 +77,15 @@ protected void update() {
}
}
} else {
heroDied();
Game.instance.finish();
}
}

protected void stateChanged(GameStateData state){}

protected void heroDied(){ }

private void clearChasm() {
WndOptions options = (WndOptions) scene.findFirstMember(WndOptions.class);
if (options != null) {
@@ -88,4 +110,18 @@ private boolean canAct() {

return Dungeon.hero.ready && scene.active && scene.alive && isGameScene;
}

@Override
public InputStream openFileInput(String fileName) throws IOException {
if (currentState == null)
throw new IOException("File " + fileName + " does not exist");
return currentState.loadSection(fileName);
}

@Override
public OutputStream openFileOutput(String fileName) {
if (currentState == null)
return null;
return currentState.saveSection(fileName);
}
}
@@ -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));
}
}

Large diffs are not rendered by default.

@@ -1,4 +1,4 @@
package com.teamquixote.ai;
package com.teamquixote.ai.launchers;

import com.watabou.input.NoosaInputProcessor;

@@ -1,13 +1,12 @@
package com.teamquixote.ai;
package com.teamquixote.ai.launchers;

import com.badlogic.gdx.Files;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.badlogic.gdx.backends.lwjgl.LwjglPreferences;
import com.badlogic.gdx.utils.SharedLibraryLoader;
import com.teamquixote.ai.agents.AngryFrontiersman;
import com.teamquixote.ai.agents.Frontiersman;
import com.teamquixote.ai.agents.Spelunker;
import com.teamquixote.ai.agents.Randy;
import com.teamquixote.ai.dungeons.AiPixelDungeon;
import com.watabou.input.NoosaInputProcessor;
import com.watabou.pixeldungeon.Preferences;
import com.watabou.utils.PDPlatformSupport;
@@ -45,7 +44,7 @@ public static void main (String[] arg) {
DesktopSupport platformSupport = new DesktopSupport(version, config.preferencesDirectory, new AiInputProcessor());
// TODO: It have to be pulled from build.gradle, but I don't know how it can be done
config.title = "Pixel Dungeon";
new LwjglApplication(new AiPixelDungeon(platformSupport, new Spelunker()), config);
new LwjglApplication(new AiPixelDungeon(platformSupport, new Randy()), config);
}

private static class DesktopSupport extends PDPlatformSupport {
@@ -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());
}
}
@@ -1,12 +1,12 @@
package com.teamquixote.ai;
package com.teamquixote.ai.launchers;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.headless.HeadlessApplication;
import com.badlogic.gdx.backends.headless.HeadlessApplicationConfiguration;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.utils.SharedLibraryLoader;
import com.teamquixote.ai.agents.Randy;
import com.teamquixote.ai.agents.Spelunker;
import com.teamquixote.ai.dungeons.AiPixelDungeon;
import com.watabou.utils.PDPlatformSupport;

import static org.mockito.Mockito.mock;
@@ -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());
}
}