diff --git a/core/src/com/mygdx/game/Game.java b/core/src/com/mygdx/game/Game.java index 12b4936..01f442f 100644 --- a/core/src/com/mygdx/game/Game.java +++ b/core/src/com/mygdx/game/Game.java @@ -1,105 +1,127 @@ -package com.mygdx.game; - -import com.badlogic.gdx.ApplicationAdapter; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Input; -import com.badlogic.gdx.graphics.GL20; -import com.badlogic.gdx.graphics.g2d.BitmapFont; -import com.badlogic.gdx.graphics.glutils.ShapeRenderer; -import java.util.ConcurrentModificationException; -import logic.Element; -import logic.World; - -public class Game extends ApplicationAdapter { - - private static Game game; - ShapeRenderer shaper; - private World world; - private float cameraSpeed = 15; - private BitmapFont font; - private boolean paused = false; - - @Override - public void create() { - game = this; - shaper = new ShapeRenderer(); - shaper.setAutoShapeType(true); - font = new BitmapFont(); - Thread worldThread = new Thread(world); - worldThread.setName("Worker"); - worldThread.setPriority(Thread.MAX_PRIORITY); - worldThread.start(); - } - - public Game() { - world = new World(2500, 2500); - } - - @Override - public void render() { - // Controls - if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { - world.newGen(false); - } - if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { - shaper.translate(-cameraSpeed, 0, 0); - } - if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { - shaper.translate(cameraSpeed, 0, 0); - } - if (Gdx.input.isKeyPressed(Input.Keys.UP)) { - shaper.translate(0, -cameraSpeed, 0); - } - if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { - shaper.translate(0, cameraSpeed, 0); - } - if (Gdx.input.isKeyJustPressed(Input.Keys.PLUS)) { - shaper.scale(0.5f, 0.5f, 1); - } - if (Gdx.input.isKeyJustPressed(Input.Keys.MINUS)) { - shaper.scale(1.5f, 1.5f, 1); - } - if (Gdx.input.isKeyJustPressed(Input.Keys.P)) { - paused = !paused; - } - if (Gdx.input.isKeyJustPressed(Input.Keys.L)) { - if (world.getFpsLimit() == 60) { - world.setFpsLimit(0); - } else { - world.setFpsLimit(60); - } - } - // Draw - Gdx.gl.glClearColor(0, 0, 0, 1); - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); - shaper.setColor(1, 1, 1, 1); - shaper.begin(ShapeRenderer.ShapeType.Line); - try { - for (Element e : world.getElements()) { - try { - e.render(shaper); - } catch (ArrayIndexOutOfBoundsException ex) { - // No idea why it happens, but it's rendering so meh - //Log.log(Log.ERROR, ex+""); - } - } - } catch (ConcurrentModificationException ex) { - } - shaper.setColor(0.3f, 0.3f, 0.3f, 1); - // draw borders - shaper.rect(0, 0, world.getWidth(), world.getHeight()); - shaper.end(); - } - - public World getWorld() { - return world; - } - - public static Game get() { - return game; - } - - public boolean isPaused() { - return paused; - } -} +package com.mygdx.game; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import java.util.ConcurrentModificationException; +import logic.Creature; +import logic.Element; +import logic.World; + +public class Game extends ApplicationAdapter { + + private static Game game; + ShapeRenderer renderer, overlayRenderer; + private World world; + private float cameraSpeed = 15; + private BitmapFont font; + private boolean paused = false; + + @Override + public void create() { + game = this; + renderer = new ShapeRenderer(); + renderer.setAutoShapeType(true); + overlayRenderer = new ShapeRenderer(); + overlayRenderer.setAutoShapeType(true); + font = new BitmapFont(); + Thread worldThread = new Thread(world); + worldThread.setName("Worker"); + worldThread.setPriority(Thread.MAX_PRIORITY); + worldThread.start(); + } + + public Game() { + world = new World(2500, 2500); + } + + @Override + public void render() { + // Controls + if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { + world.newGen(false); + } + if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { + renderer.translate(-cameraSpeed, 0, 0); + } + if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { + renderer.translate(cameraSpeed, 0, 0); + } + if (Gdx.input.isKeyPressed(Input.Keys.UP)) { + renderer.translate(0, -cameraSpeed, 0); + } + if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { + renderer.translate(0, cameraSpeed, 0); + } + if (Gdx.input.isKeyJustPressed(Input.Keys.PLUS)) { + renderer.scale(0.5f, 0.5f, 1); + } + if (Gdx.input.isKeyJustPressed(Input.Keys.MINUS)) { + renderer.scale(1.5f, 1.5f, 1); + } + if (Gdx.input.isKeyJustPressed(Input.Keys.P)) { + paused = !paused; + } + if (Gdx.input.isKeyJustPressed(Input.Keys.L)) { + if (world.getFpsLimit() == 60) { + world.setFpsLimit(0); + } else { + world.setFpsLimit(60); + } + } + if (Gdx.input.isButtonPressed(Input.Buttons.RIGHT)) { + renderer.translate(Gdx.input.getDeltaX(), Gdx.input.getDeltaY() * -1, 0); + } + /* + // Broken for now + if(Gdx.input.isButtonPressed(Input.Buttons.LEFT)){ + // TODO: project coordinates to world + world.selectCreatureAt(Gdx.input.getX(), Gdx.input.getY()); + }*/ + // Draw + Gdx.gl.glClearColor(0, 0, 0, 1); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + renderer.begin(ShapeRenderer.ShapeType.Line); + try { + for (Element e : world.getElements()) { + try { + e.render(renderer); + } catch (ArrayIndexOutOfBoundsException ex) { + // No idea why it happens, but it's rendering so meh + //Log.log(Log.ERROR, ex+""); + } + } + } catch (ConcurrentModificationException ex) { + } + if (world.getSelectedCreature() != null) { + // There is a selection + Creature c = world.getSelectedCreature(); + renderer.setColor(1, 1, 1, 1); + // Draw selection rectangle + renderer.rect(c.getX() - c.getSize() / 2, c.getY() - c.getSize() / 2, c.getX() + c.getSize() / 2, c.getY() + c.getSize() / 2); + // Draw brain + overlayRenderer.begin(); + c.getBrain().render(overlayRenderer); + overlayRenderer.end(); + } + renderer.setColor(0.3f, 0.3f, 0.3f, 1); + // draw borders + renderer.rect(0, 0, world.getWidth(), world.getHeight()); + renderer.end(); + } + + public World getWorld() { + return world; + } + + public static Game get() { + return game; + } + + public boolean isPaused() { + return paused; + } +} diff --git a/core/src/logic/Vegetable.java b/core/src/logic/Vegetable.java index 54e57e9..6f6f6bb 100644 --- a/core/src/logic/Vegetable.java +++ b/core/src/logic/Vegetable.java @@ -1,45 +1,45 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package logic; - -import com.badlogic.gdx.graphics.glutils.ShapeRenderer; -import com.mygdx.game.Game; - -/** - * - * @author fazo - */ -public class Vegetable extends Element { - - public static final int default_radius = 5; - private float decayRate = 0; - - public Vegetable(float x, float y) { - super(x, y, default_radius); - } - - @Override - public void update() { - setSize(getSize()-decayRate); - if (getSize() <= 0) { - Game.get().getWorld().getDeadPlants().add(this); - } - } - - @Override - public void render(ShapeRenderer s) { - s.setColor(1, 1, 1, 1); - s.circle(getX(), getY(), getSize()); - } - - public float getDecayRate() { - return decayRate; - } - - public void setDecayRate(float decayRate) { - this.decayRate = decayRate; - } -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package logic; + +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.mygdx.game.Game; + +/** + * + * @author fazo + */ +public class Vegetable extends Element { + + public static final int default_radius = 5; + private float decayRate = 0; + + public Vegetable(float x, float y) { + super(x, y, default_radius); + } + + @Override + public void update() { + setSize(getSize()-decayRate); + if (getSize() <= 2) { + Game.get().getWorld().getDeadPlants().add(this); + } + } + + @Override + public void render(ShapeRenderer s) { + s.setColor(1, 1, 1, 1); + s.circle(getX(), getY(), getSize()); + } + + public float getDecayRate() { + return decayRate; + } + + public void setDecayRate(float decayRate) { + this.decayRate = decayRate; + } +} diff --git a/core/src/logic/World.java b/core/src/logic/World.java index c759179..05df2ba 100644 --- a/core/src/logic/World.java +++ b/core/src/logic/World.java @@ -1,279 +1,311 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package logic; - -import com.mygdx.game.Game; -import com.mygdx.game.Log; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Date; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * - * @author fazo - */ -public class World implements Runnable { - - private final int width, height, nPlants, creatPerGen; - private int generation = 1; - private int fpsLimit = 60, fps = 0; - private final ArrayList elements; - private final ArrayList toAdd; - private final ArrayList creatures; - private final ArrayList graveyard; - private final ArrayList plants; - private final ArrayList deadPlants; - private final ArrayList fpsListeners; - - public World(int width, int height) { - this.width = width; - this.height = height; - elements = new ArrayList(); - creatures = new ArrayList(); - toAdd = new ArrayList(); - creatPerGen = Math.min(Math.round(width * height / 20000), 50); - nPlants = Math.round(width * height / 5500); - plants = new ArrayList(); - deadPlants = new ArrayList(); - graveyard = new ArrayList(); - fpsListeners = new ArrayList(); - newGen(true); - } - - @Override - public void run() { - Date d, timekeeper = new Date(); - long time; - int target, frames = 0; - for (;;) { - if (!Game.get().isPaused()) { - d = new Date(); - update(); - frames++; - Date now = new Date(); - if (now.getTime() - timekeeper.getTime() > 1000) { - fps = frames; - frames = 0; - for (FpsListener f : fpsListeners) { - f.fpsChanged(fps); - } - timekeeper = new Date(); - } - if (fpsLimit > 0) { - time = now.getTime() - d.getTime(); - target = 1000 / fpsLimit; - if (time < target) { - try { - Thread.sleep((long) (target - time)); - } catch (InterruptedException ex) { - Logger.getLogger(World.class.getName()).log(Level.SEVERE, null, ex); - } - } - } - } else { - try { - Thread.sleep(100); - } catch (InterruptedException ex) { - Logger.getLogger(World.class.getName()).log(Level.SEVERE, null, ex); - } - } - } - } - - public void update() { - for (Element e : toAdd) { - elements.add(e); - if (e instanceof Creature) { - creatures.add((Creature) e); - } else if (e instanceof Vegetable) { - plants.add((Vegetable) e); - } - } - toAdd.clear(); - elements.removeAll(graveyard); - elements.removeAll(deadPlants); - plants.removeAll(deadPlants); - deadPlants.clear(); - creatures.removeAll(graveyard); - if (creatures.isEmpty()) { - // All dead, next gen - newGen(false); - } - while (plants.size() < nPlants) { - spawnVegetable(); - } - for (Element e : elements) { - e.update(); - } - } - - public void newGen(boolean restart) { - elements.removeAll(creatures); - graveyard.addAll(creatures); - creatures.clear(); - Comparator creatureComp = new Comparator() { - - @Override - public int compare(Creature t, Creature t1) { - // put the highest fitness first (sort in reverse) - return (int) (t1.getFitness() - t.getFitness()); - } - }; - if (graveyard.isEmpty() || restart) { // First gen - generation = 1; - Log.log(Log.INFO, "Starting from generation 1: spawning " + creatPerGen + " creatures."); - for (int i = 0; i < creatPerGen; i++) { - spawnCreature(); - } - } else { // Evolve previous gen - graveyard.sort(creatureComp); // sort by fitness - // Prepare best agent list - int topSize = (int) Math.round(graveyard.size() * 0.05f); - Creature[] top = new Creature[topSize]; - // Calculate avg fitness and prepare best agent list - float avgFitness = 0; - for (int i = 0; i < graveyard.size(); i++) { - Creature c = graveyard.get(i); - if (i < topSize) { - top[i] = graveyard.get(i); - Log.log(Log.INFO, "Gen " + generation + " Top " + (i + 1) + ": " + c.getFitness()); - } - avgFitness += c.getFitness(); - } - avgFitness = avgFitness / graveyard.size(); - Log.log(Log.INFO, "Gen " + generation + " done. Avg fitness: " + avgFitness); - // Generate children - for (Creature c : graveyard) { - int first = (int) Math.floor(Math.random() * topSize); - int sec = first; - while (sec == first) { - sec = (int) Math.floor(Math.random() * topSize); - } - float[][][] n = null; - try { - n = top[first].getBrain().breed(top[sec].getBrain().getMap()); - } catch (Exception ex) { - // Should not happen - Logger.getLogger(World.class.getName()).log(Level.SEVERE, null, ex); - } - Creature ne = spawnCreature(n); - ne.getBrain().mutate(0.05f); // mutate children - } - graveyard.clear(); - generation++; - } - } - - private Element spawn(boolean isCreature, float[][][] brainMap) { - int x, y, r; - boolean overlaps = false; - if (isCreature) { - r = Creature.default_radius; - } else { - r = Vegetable.default_radius; - } - do { - overlaps = false; - x = (int) (Math.random() * width); - y = (int) (Math.random() * height); - for (Element e : elements) { - if (e.overlaps(x, y, r)) { - overlaps = true; - } - } - } while (overlaps); - if (isCreature) { - Log.log(Log.DEBUG, "New Creat: " + x + " " + y); - Creature c = new Creature(x, y); - if (brainMap != null) { - c.getBrain().remap(brainMap); - } - //add(c); - elements.add(c); - creatures.add(c); - return c; - } else { - Log.log(Log.DEBUG, "New Veg: " + x + " " + y); - Vegetable v = new Vegetable(x, y); - //add(v); - elements.add(v); - plants.add(v); - return v; - } - } - - public interface FpsListener { - - public abstract void fpsChanged(int newValue); - } - - private void spawnVegetable() { - spawn(false, null); - } - - private Creature spawnCreature() { - return (Creature) spawn(true, null); - } - - private Creature spawnCreature(float[][][] b) { - return (Creature) spawn(true, b); - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int getGeneration() { - return generation; - } - - public void addFpsListener(FpsListener f) { - fpsListeners.add(f); - } - - public void add(Element e) { - toAdd.add(e); - } - - public ArrayList getElements() { - return elements; - } - - public ArrayList getGraveyard() { - return graveyard; - } - - public ArrayList getDeadPlants() { - return deadPlants; - } - - public ArrayList getCreatures() { - return creatures; - } - - public ArrayList getPlants() { - return plants; - } - - public float getFpsLimit() { - return fpsLimit; - } - - public void setFpsLimit(int fpsLimit) { - this.fpsLimit = fpsLimit; - } - - public float getFps() { - return fps; - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package logic; + +import com.mygdx.game.Game; +import com.mygdx.game.Log; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author fazo + */ +public class World implements Runnable { + + private final int width, height, nPlants, creatPerGen; + private int generation = 1; + private int fpsLimit = 60, fps = 0; + private Creature selected; + private final ArrayList elements; + private final ArrayList toAdd; + private final ArrayList creatures; + private final ArrayList graveyard; + private final ArrayList plants; + private final ArrayList deadPlants; + private final ArrayList fpsListeners; + + public World(int width, int height) { + this.width = width; + this.height = height; + elements = new ArrayList(); + creatures = new ArrayList(); + toAdd = new ArrayList(); + creatPerGen = Math.min(Math.round(width * height / 20000), 50); + nPlants = Math.round(width * height / 5500); + plants = new ArrayList(); + deadPlants = new ArrayList(); + graveyard = new ArrayList(); + fpsListeners = new ArrayList(); + selected = null; + newGen(true); + } + + @Override + public void run() { + Date d, timekeeper = new Date(); + long time; + int target, frames = 0; + for (;;) { + if (!Game.get().isPaused()) { + d = new Date(); + update(); + frames++; + Date now = new Date(); + if (now.getTime() - timekeeper.getTime() > 1000) { + fps = frames; + frames = 0; + for (FpsListener f : fpsListeners) { + f.fpsChanged(fps); + } + timekeeper = new Date(); + } + if (fpsLimit > 0) { + time = now.getTime() - d.getTime(); + target = 1000 / fpsLimit; + if (time < target) { + try { + Thread.sleep((long) (target - time)); + } catch (InterruptedException ex) { + Logger.getLogger(World.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } else { + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + Logger.getLogger(World.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } + + public void update() { + for (Element e : toAdd) { + elements.add(e); + if (e instanceof Creature) { + creatures.add((Creature) e); + } else if (e instanceof Vegetable) { + plants.add((Vegetable) e); + } + } + toAdd.clear(); + elements.removeAll(graveyard); + if (selected != null && graveyard.contains(selected)) { + selected = null; + Log.log(Log.INFO, "Cleared selection"); + } + elements.removeAll(deadPlants); + plants.removeAll(deadPlants); + deadPlants.clear(); + creatures.removeAll(graveyard); + if (creatures.isEmpty()) { + // All dead, next gen + newGen(false); + } + while (plants.size() < nPlants) { + spawnVegetable(); + } + for (Element e : elements) { + e.update(); + } + } + + public void newGen(boolean restart) { + elements.removeAll(creatures); + graveyard.addAll(creatures); + creatures.clear(); + if (selected != null) { + selected = null; + Log.log(Log.INFO, "Cleared selection"); + } + Comparator creatureComp = new Comparator() { + + @Override + public int compare(Creature t, Creature t1) { + // put the highest fitness first (sort in reverse) + return (int) (t1.getFitness() - t.getFitness()); + } + }; + if (graveyard.isEmpty() || restart) { // First gen + generation = 1; + Log.log(Log.INFO, "Starting from generation 1: spawning " + creatPerGen + " creatures."); + for (int i = 0; i < creatPerGen; i++) { + spawnCreature(); + } + } else { // Evolve previous gen + graveyard.sort(creatureComp); // sort by fitness + // Prepare best agent list + int topSize = (int) Math.round(graveyard.size() * 0.05f); + Creature[] top = new Creature[topSize]; + // Calculate avg fitness and prepare best agent list + float avgFitness = 0; + for (int i = 0; i < graveyard.size(); i++) { + Creature c = graveyard.get(i); + if (i < topSize) { + top[i] = graveyard.get(i); + Log.log(Log.INFO, "Gen " + generation + " Top " + (i + 1) + ": " + c.getFitness()); + } + avgFitness += c.getFitness(); + } + avgFitness = avgFitness / graveyard.size(); + Log.log(Log.INFO, "Gen " + generation + " done. Avg fitness: " + avgFitness); + // Generate children + for (Creature c : graveyard) { + int first = (int) Math.floor(Math.random() * topSize); + int sec = first; + while (sec == first) { + sec = (int) Math.floor(Math.random() * topSize); + } + float[][][] n = null; + try { + n = top[first].getBrain().breed(top[sec].getBrain().getMap()); + } catch (Exception ex) { + // Should not happen + Logger.getLogger(World.class.getName()).log(Level.SEVERE, null, ex); + } + Creature ne = spawnCreature(n); + ne.getBrain().mutate(0.05f); // mutate children + } + graveyard.clear(); + generation++; + } + } + + private Element spawn(boolean isCreature, float[][][] brainMap) { + int x, y, r; + boolean overlaps = false; + if (isCreature) { + r = Creature.default_radius; + } else { + r = Vegetable.default_radius; + } + do { + overlaps = false; + x = (int) (Math.random() * width); + y = (int) (Math.random() * height); + for (Element e : elements) { + if (e.overlaps(x, y, r)) { + overlaps = true; + } + } + } while (overlaps); + if (isCreature) { + Log.log(Log.DEBUG, "New Creat: " + x + " " + y); + Creature c = new Creature(x, y); + if (brainMap != null) { + c.getBrain().remap(brainMap); + } + //add(c); + elements.add(c); + creatures.add(c); + return c; + } else { + Log.log(Log.DEBUG, "New Veg: " + x + " " + y); + Vegetable v = new Vegetable(x, y); + //add(v); + elements.add(v); + plants.add(v); + return v; + } + } + + public interface FpsListener { + + public abstract void fpsChanged(int newValue); + } + + public void selectCreatureAt(int x, int y) { + selected = null; // Clear selection + try { + for (Creature c : creatures) { + if (c.overlaps(x, y)) { + selected = c; + Log.log(Log.INFO, "Selected a creature"); + } + } + } catch (ConcurrentModificationException ex) { + } + } + + private void spawnVegetable() { + spawn(false, null); + } + + private Creature spawnCreature() { + return (Creature) spawn(true, null); + } + + private Creature spawnCreature(float[][][] b) { + return (Creature) spawn(true, b); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getGeneration() { + return generation; + } + + public void addFpsListener(FpsListener f) { + fpsListeners.add(f); + } + + public void add(Element e) { + toAdd.add(e); + } + + public ArrayList getElements() { + return elements; + } + + public ArrayList getGraveyard() { + return graveyard; + } + + public ArrayList getDeadPlants() { + return deadPlants; + } + + public ArrayList getCreatures() { + return creatures; + } + + public ArrayList getPlants() { + return plants; + } + + public float getFpsLimit() { + return fpsLimit; + } + + public void setFpsLimit(int fpsLimit) { + this.fpsLimit = fpsLimit; + } + + public float getFps() { + return fps; + } + + public Creature getSelectedCreature() { + return selected; + } + + public void selectCreature(Creature selected) { + this.selected = selected; + } + +} diff --git a/core/src/logic/neural/Brain.java b/core/src/logic/neural/Brain.java index 2add9ad..ec56023 100644 --- a/core/src/logic/neural/Brain.java +++ b/core/src/logic/neural/Brain.java @@ -1,237 +1,271 @@ -package logic.neural; - -import com.mygdx.game.Log; - -/** - * Represents a virtual brain - * - * @author fazo - */ -public class Brain { - - public static final float bias = 0.5f; - private Neuron[][] neurons; - - /** - * Create a new brain with a random map (mind) with given number of neurons - * - * @param nInputs the number of input neurons (at least 1) - * @param nOutputs the number of output neurons (at least 1) - * @param hiddenLayers how many hidden layers of neurons (at least 1) - * @param neuronsPerHiddenLayer how many neurons per hidden layer (at least - * 1) - */ - public Brain(int nInputs, int nOutputs, int hiddenLayers, int neuronsPerHiddenLayer) { - // Prepare brain map - neurons = new Neuron[hiddenLayers + 2][]; - neurons[0] = new Neuron[nInputs]; - neurons[hiddenLayers + 1] = new Neuron[nOutputs]; - for (int i = 0; i < hiddenLayers; i++) { - neurons[i + 1] = new Neuron[neuronsPerHiddenLayer]; - } - // Randomize brain - initialize(); - } - - /** - * Create a new brain using given brain map (mind) - * - * @param brainMap the brain map (mind) to use - */ - public Brain(float[][][] brainMap) { - neurons = new Neuron[brainMap.length][]; - for (int i = 0; i < brainMap.length; i++) { // for each layer - neurons[i] = new Neuron[brainMap[i].length]; - for (int j = 0; j < brainMap[i].length; j++) { // for each neuron - neurons[i][j] = new Neuron(i, bias, this, brainMap[i][j]); - } - } - } - - /** - * Apply a new brain map (mind) to this brain - * - * @param brainMap the new brain map to apply - */ - public void remap(float[][][] brainMap) { - for (int i = 0; i < brainMap.length; i++) { // for each layer - for (int j = 0; j < brainMap[i].length; j++) { // for each neuron - // skip input layer - if (neurons[i + 1][j] == null) { - neurons[i + 1][j] = new Neuron(j, bias, this, brainMap[i][j]); - } else { - neurons[i + 1][j].setWeights(brainMap[i][j]); - } - } - } - } - - /** - * Populate the brain with brand new random neurons - */ - private void initialize() { - // init hidden layers - for (int i = 0; i < neurons.length; i++) { - for (int j = 0; j < neurons[i].length; j++) { - // create neuron - Neuron n = new Neuron(i, bias, this); - neurons[i][j] = n; - Log.log(Log.DEBUG, "Adding Layer " + (i + 1) + " Neuron " + (j + 1)); - } - } - } - - /** - * Give some input to the brain - * - * @param values the array of input. Its length must match the number of - * input neurons of this brain - * @throws Exception if the number of inputs given differs from the number - * of input neurons of this brain - */ - public void input(float[] values) throws Exception { - if (values.length != neurons[0].length) { - throw new Exception("Not enough or too many inputs"); - } - for (int i = 0; i < values.length; i++) { - neurons[0][i].setOutput(values[i]); - } - clearCache(); - } - - /** - * Compute output of the brain starting from given input - * - * @return an array as long as the number of output neurons, containing the - * result - */ - public float[] compute() { - //clearCache(); // unnecessary if already called when changing inputs - float[] res = new float[neurons[neurons.length - 1].length]; - for (int i = 0; i < neurons[neurons.length - 1].length; i++) { - res[i] = neurons[neurons.length - 1][i].compute(); - } - return res; - } - - /** - * Input some values (see input function) and then compute the results. - * - * @param values - * @return the results of the neural network - * @throws Exception if the number of inputs given differs from the number - * of input neurons of this brain - */ - public float[] compute(float[] values) throws Exception { - input(values); - return compute(); - } - - /** - * Get a brainMap that represents this brain's mind - * - * @return a tridimensional floating point number array representing a full - * mind - */ - public float[][][] getMap() { - float[][][] res = new float[neurons.length - 1][][]; - for (int i = 1; i < neurons.length; i++) // layers (skip input layer) - { - res[i - 1] = new float[neurons[i].length][]; - for (int j = 0; j < neurons[i].length; j++) // neurons per layer - { - res[i - 1][j] = neurons[i][j].getWeights(); - } - } - return res; - } - - /** - * Get a map of this brain's mind.. with a mutation - * - * @param mutationFactor the higher this number, the bigger the mutation - * @return a mutated brain map of this brain's mind - */ - public float[][][] getMutatedMap(float mutationFactor) { - float[][][] res = new float[neurons.length - 1][][]; - for (int i = 1; i < neurons.length; i++) // layers (skip input layer) - { - res[i - 1] = new float[neurons[i].length][]; - for (int j = 0; j < neurons[i].length; j++) // neurons per layer - { - res[i - 1][j] = neurons[i][j].mutate(mutationFactor); - } - } - return res; - } - - /** - * Apply a mutation to this brain - * - * @param mutationFactor the higher this number, the bigger the mutation - */ - public void mutate(float mutationFactor) { - for (int i = 1; i < neurons.length; i++) // layers (skip input layer) - { - for (int j = 0; j < neurons[i].length; j++) // neurons per layer - { - neurons[i][j].setWeights(neurons[i][j].mutate(mutationFactor)); - } - } - } - - public float[][][] breed(float[][][] map) throws Exception { - float[][][] res = new float[neurons.length - 1][][]; - if (map.length != neurons.length - 1) { - throw new Exception("incompatible brains"); - } - for (int i = 1; i < neurons.length; i++) // layers (skip input layer) - { - res[i - 1] = new float[neurons[i].length][]; - if (map[i - 1].length != neurons[i].length) { - throw new Exception("incompatible brains"); - } - for (int j = 0; j < neurons[i].length; j++) // neurons per layer - { - // j = 8 not valid for neurons[i][j]. investigate why. - //System.out.println(i+" "+j+" | "+neurons[i].length+" "+res[i-1].length+" "+neurons[i][j].getWeights().length); - //System.out.println(neurons[i].length +" has to be > "+j); - res[i - 1][j] = new float[neurons[i][j].getWeights().length]; - if (map[i - 1][j].length != neurons[i][j].getWeights().length) { - throw new Exception("incompatible brains"); - } - for (int z = 0; z < neurons[i][j].getWeights().length; z++) // each weight - { - // Combine the two weights - if (Math.random() < 0.5) { - res[i - 1][j][z] = map[i - 1][j][z]; - } else { - res[i - 1][j][z] = neurons[i][j].getWeights()[z]; - } - } - } - } - return res; - } - - /** - * Empties the neurons' cache. Needs to be called after changing brain - * inputs or before computing the result. - */ - private void clearCache() { - for (int i = 1; i < neurons.length; i++) { - for (int j = 0; j < neurons[i].length; j++) { - neurons[i][j].clearCache(); - } - } - } - - /** - * Returns an array with pointers to all this brain's neurons. - * - * @return bidimensional array with first index representing the layer. - */ - public Neuron[][] getNeurons() { - return neurons; - } -} +package logic.neural; + +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.mygdx.game.Log; + +/** + * Represents a virtual brain + * + * @author fazo + */ +public class Brain { + + public static final float bias = 0.5f; + private Neuron[][] neurons; + + /** + * Create a new brain with a random map (mind) with given number of neurons + * + * @param nInputs the number of input neurons (at least 1) + * @param nOutputs the number of output neurons (at least 1) + * @param hiddenLayers how many hidden layers of neurons (at least 1) + * @param neuronsPerHiddenLayer how many neurons per hidden layer (at least + * 1) + */ + public Brain(int nInputs, int nOutputs, int hiddenLayers, int neuronsPerHiddenLayer) { + // Prepare brain map + neurons = new Neuron[hiddenLayers + 2][]; + neurons[0] = new Neuron[nInputs]; + neurons[hiddenLayers + 1] = new Neuron[nOutputs]; + for (int i = 0; i < hiddenLayers; i++) { + neurons[i + 1] = new Neuron[neuronsPerHiddenLayer]; + } + // Randomize brain + initialize(); + } + + /** + * Create a new brain using given brain map (mind) + * + * @param brainMap the brain map (mind) to use + */ + public Brain(float[][][] brainMap) { + neurons = new Neuron[brainMap.length][]; + for (int i = 0; i < brainMap.length; i++) { // for each layer + neurons[i] = new Neuron[brainMap[i].length]; + for (int j = 0; j < brainMap[i].length; j++) { // for each neuron + neurons[i][j] = new Neuron(i, bias, this, brainMap[i][j]); + } + } + } + + /** + * Apply a new brain map (mind) to this brain + * + * @param brainMap the new brain map to apply + */ + public void remap(float[][][] brainMap) { + for (int i = 0; i < brainMap.length; i++) { // for each layer + for (int j = 0; j < brainMap[i].length; j++) { // for each neuron + // skip input layer + if (neurons[i + 1][j] == null) { + neurons[i + 1][j] = new Neuron(j, bias, this, brainMap[i][j]); + } else { + neurons[i + 1][j].setWeights(brainMap[i][j]); + } + } + } + } + + /** + * Populate the brain with brand new random neurons + */ + private void initialize() { + // init hidden layers + for (int i = 0; i < neurons.length; i++) { + for (int j = 0; j < neurons[i].length; j++) { + // create neuron + Neuron n = new Neuron(i, bias, this); + neurons[i][j] = n; + Log.log(Log.DEBUG, "Adding Layer " + (i + 1) + " Neuron " + (j + 1)); + } + } + } + + /** + * Draw this brain's status. + * + * @param s the ShapeRenderer to use for the drawing + */ + public void render(ShapeRenderer s) { + s.set(ShapeRenderer.ShapeType.Filled); + int neuronHeight = 0; + for (Neuron[] ns : neurons) { + if (ns.length > neuronHeight) { + neuronHeight = ns.length; + } + } + s.rect(0, 0, neurons.length * 50, neuronHeight * 30); + for (int i = 0; i < neurons.length; i++) { + //s.set(ShapeRenderer.ShapeType.Line); + for (int j = 0; j < neurons[i].length; j++) { + // get neuron result first so cache system can kick in and save some calculations + float nr = neurons[i][j].compute(); + // Draw neuron links + float[] links = neurons[i][j].getInputs(); + for (int f = 0; f < links.length; f++) { + s.setColor(links[f], links[f], links[f], 1); + s.line(i * 50, j * 30, (i - 1) * 50, f * 30); + } + // Draw neuron + s.setColor(1 - nr, nr, 0, 1); + s.set(ShapeRenderer.ShapeType.Filled); + s.circle(i * 50, j * 30, 15); + } + } + } + + /** + * Give some input to the brain + * + * @param values the array of input. Its length must match the number of + * input neurons of this brain + * @throws Exception if the number of inputs given differs from the number + * of input neurons of this brain + */ + public void input(float[] values) throws Exception { + if (values.length != neurons[0].length) { + throw new Exception("Not enough or too many inputs"); + } + for (int i = 0; i < values.length; i++) { + neurons[0][i].setOutput(values[i]); + } + clearCache(); + } + + /** + * Compute output of the brain starting from given input + * + * @return an array as long as the number of output neurons, containing the + * result + */ + public float[] compute() { + //clearCache(); // unnecessary if already called when changing inputs + float[] res = new float[neurons[neurons.length - 1].length]; + for (int i = 0; i < neurons[neurons.length - 1].length; i++) { + res[i] = neurons[neurons.length - 1][i].compute(); + } + return res; + } + + /** + * Input some values (see input function) and then compute the results. + * + * @param values + * @return the results of the neural network + * @throws Exception if the number of inputs given differs from the number + * of input neurons of this brain + */ + public float[] compute(float[] values) throws Exception { + input(values); + return compute(); + } + + /** + * Get a brainMap that represents this brain's mind + * + * @return a tridimensional floating point number array representing a full + * mind + */ + public float[][][] getMap() { + float[][][] res = new float[neurons.length - 1][][]; + for (int i = 1; i < neurons.length; i++) // layers (skip input layer) + { + res[i - 1] = new float[neurons[i].length][]; + for (int j = 0; j < neurons[i].length; j++) // neurons per layer + { + res[i - 1][j] = neurons[i][j].getWeights(); + } + } + return res; + } + + /** + * Get a map of this brain's mind.. with a mutation + * + * @param mutationFactor the higher this number, the bigger the mutation + * @return a mutated brain map of this brain's mind + */ + public float[][][] getMutatedMap(float mutationFactor) { + float[][][] res = new float[neurons.length - 1][][]; + for (int i = 1; i < neurons.length; i++) // layers (skip input layer) + { + res[i - 1] = new float[neurons[i].length][]; + for (int j = 0; j < neurons[i].length; j++) // neurons per layer + { + res[i - 1][j] = neurons[i][j].mutate(mutationFactor); + } + } + return res; + } + + /** + * Apply a mutation to this brain + * + * @param mutationFactor the higher this number, the bigger the mutation + */ + public void mutate(float mutationFactor) { + for (int i = 1; i < neurons.length; i++) // layers (skip input layer) + { + for (int j = 0; j < neurons[i].length; j++) // neurons per layer + { + neurons[i][j].setWeights(neurons[i][j].mutate(mutationFactor)); + } + } + } + + public float[][][] breed(float[][][] map) throws Exception { + float[][][] res = new float[neurons.length - 1][][]; + if (map.length != neurons.length - 1) { + throw new Exception("incompatible brains"); + } + for (int i = 1; i < neurons.length; i++) // layers (skip input layer) + { + res[i - 1] = new float[neurons[i].length][]; + if (map[i - 1].length != neurons[i].length) { + throw new Exception("incompatible brains"); + } + for (int j = 0; j < neurons[i].length; j++) // neurons per layer + { + // j = 8 not valid for neurons[i][j]. investigate why. + //System.out.println(i+" "+j+" | "+neurons[i].length+" "+res[i-1].length+" "+neurons[i][j].getWeights().length); + //System.out.println(neurons[i].length +" has to be > "+j); + res[i - 1][j] = new float[neurons[i][j].getWeights().length]; + if (map[i - 1][j].length != neurons[i][j].getWeights().length) { + throw new Exception("incompatible brains"); + } + for (int z = 0; z < neurons[i][j].getWeights().length; z++) // each weight + { + // Combine the two weights + if (Math.random() < 0.5) { + res[i - 1][j][z] = map[i - 1][j][z]; + } else { + res[i - 1][j][z] = neurons[i][j].getWeights()[z]; + } + } + } + } + return res; + } + + /** + * Empties the neurons' cache. Needs to be called after changing brain + * inputs or before computing the result. + */ + private void clearCache() { + for (int i = 1; i < neurons.length; i++) { + for (int j = 0; j < neurons[i].length; j++) { + neurons[i][j].clearCache(); + } + } + } + + /** + * Returns an array with pointers to all this brain's neurons. + * + * @return bidimensional array with first index representing the layer. + */ + public Neuron[][] getNeurons() { + return neurons; + } +} diff --git a/core/src/logic/neural/Neuron.java b/core/src/logic/neural/Neuron.java index 3e4a44b..cf6c5b6 100644 --- a/core/src/logic/neural/Neuron.java +++ b/core/src/logic/neural/Neuron.java @@ -1,124 +1,142 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package logic.neural; - -import com.mygdx.game.Log; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * - * @author fazo - */ -public class Neuron { - - private float[] weights; - private NeuronCache cache; - private float bias, output; - private boolean isInputNeuron; - private int layer; - private Brain brain; - - public Neuron(int layer, float bias, Brain brain) { - this(layer, bias, brain, null); - } - - public Neuron(int layer, float bias, Brain brain, float[] weights) { - this.brain = brain; - this.layer = layer; - if (weights == null) { - scramble(); - } else { - this.weights = weights; - } - cache = new NeuronCache(this.weights.length); - } - - private void scramble() { - // init weights - if (layer > 0) { - weights = new float[brain.getNeurons()[layer - 1].length]; - } else { // layer 0 - isInputNeuron = true; - weights = new float[0]; - } - // Put random weights - for (int i = 0; i < weights.length; i++) { - weights[i] = (float) (Math.random() * 5 - 2.5f); - } - } - - public float compute() { - if (isInputNeuron) { - return output; - } - float a = bias * -1; // activation - for (int i = 0; i < weights.length; i++) { - if (cache.has(i)) { - try { - return cache.get(i); - } catch (Exception ex) { - // This should never happen - Logger.getLogger(Neuron.class.getName()).log(Level.SEVERE, null, ex); - } - } - Neuron n = brain.getNeurons()[layer - 1][i]; - a += n.compute() * weights[i]; - } - // sigmoid function - float res = (float) (1 / (1 + Math.pow(Math.E, a * -1))); - Log.log(Log.DEBUG, "Computed Value " + res + " for neuron"); - return res; - } - - public float[] mutate(float mutationFactor) { - float[] mutatedWeights = new float[weights.length]; - for (int i = 0; i < weights.length; i++) { - mutatedWeights[i] = weights[i] + mutationFactor - mutationFactor / 2; - } - return mutatedWeights; - } - - public void setOutput(float output) { - isInputNeuron = true; - this.output = output; - } - - public float getBias() { - return bias; - } - - public void setBias(float bias) { - this.bias = bias; - } - - public boolean isInputNeuron() { - return isInputNeuron; - } - - public int getLayer() { - return layer; - } - - public void setLayer(int layer) { - this.layer = layer; - } - - public float[] getWeights() { - return weights; - } - - public void setWeights(float[] weights) { - this.weights = weights; - // Changing the neuron makes the cache invalid - clearCache(); - } - - public void clearCache() { - cache.clear(); - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package logic.neural; + +import com.mygdx.game.Log; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author fazo + */ +public class Neuron { + + private float[] weights; + private NeuronCache cache; + private float bias, output; + private boolean isInputNeuron; + private int layer; + private Brain brain; + + public Neuron(int layer, float bias, Brain brain) { + this(layer, bias, brain, null); + } + + public Neuron(int layer, float bias, Brain brain, float[] weights) { + this.brain = brain; + this.layer = layer; + if (weights == null) { + scramble(); + } else { + this.weights = weights; + } + cache = new NeuronCache(this.weights.length); + } + + private void scramble() { + // init weights + if (layer > 0) { + weights = new float[brain.getNeurons()[layer - 1].length]; + } else { // layer 0 + isInputNeuron = true; + weights = new float[0]; + } + // Put random weights + for (int i = 0; i < weights.length; i++) { + weights[i] = (float) (Math.random() * 5 - 2.5f); + } + } + + public float compute() { + if (isInputNeuron) { + return output; + } + if(cache.hasCachedOutput()) return cache.getCachedOutput(); + float a = bias * -1; // activation + for (int i = 0; i < weights.length; i++) { + if (cache.has(i)) { + try { + a += cache.get(i); + } catch (Exception ex) { + // This should never happen + Logger.getLogger(Neuron.class.getName()).log(Level.SEVERE, null, ex); + } + } + Neuron n = brain.getNeurons()[layer - 1][i]; + a += n.compute() * weights[i]; + } + // sigmoid function + float res = (float) (1 / (1 + Math.pow(Math.E, a * -1))); + Log.log(Log.DEBUG, "Computed Value " + res + " for neuron"); + return res; + } + + public float[] mutate(float mutationFactor) { + float[] mutatedWeights = new float[weights.length]; + for (int i = 0; i < weights.length; i++) { + mutatedWeights[i] = weights[i] + mutationFactor - mutationFactor / 2; + } + return mutatedWeights; + } + + public float[] getInputs() { + float inputs[] = new float[weights.length]; + for (int i = 0; i < inputs.length; i++) { + if (cache.has(i)) { + try { + inputs[i] = cache.get(i); + } catch (Exception ex) { + // Shouldnt happen + Logger.getLogger(Neuron.class.getName()).log(Level.SEVERE, null, ex); + } + } else { + inputs[i] = 0; + } + } + return inputs; + } + + public void setOutput(float output) { + isInputNeuron = true; + this.output = output; + } + + public float getBias() { + return bias; + } + + public void setBias(float bias) { + this.bias = bias; + } + + public boolean isInputNeuron() { + return isInputNeuron; + } + + public int getLayer() { + return layer; + } + + public void setLayer(int layer) { + this.layer = layer; + } + + public float[] getWeights() { + return weights; + } + + public void setWeights(float[] weights) { + this.weights = weights; + // Changing the neuron makes the cache invalid + clearCache(); + } + + public void clearCache() { + cache.clear(); + } + +} diff --git a/core/src/logic/neural/NeuronCache.java b/core/src/logic/neural/NeuronCache.java index 4ba1454..07b4bd9 100644 --- a/core/src/logic/neural/NeuronCache.java +++ b/core/src/logic/neural/NeuronCache.java @@ -1,68 +1,84 @@ -package logic.neural; - -/** - * Used by neurons to cache inputs for faster NN evaluation performance. - * - * @author fazo - */ -public class NeuronCache { - - private float[] cache; - private boolean[] validity; - - /** - * Create a new empty input cache with given size. - * - * @param size how many inputs the requiring neuron has. - */ - public NeuronCache(int size) { - cache = new float[size]; - validity = new boolean[size]; - clear(); - } - - /** - * Put a value in the cache. - * - * @param index the index of the value - * @param value the value itself - */ - public void put(int index, float value) { - validity[index] = true; - cache[index] = value; - } - - /** - * Read a value from the cache. - * - * @param index the index of the value - * @return the value required - * @throws Exception if value not stored or declared invalid - */ - public float get(int index) throws Exception { - if (validity[index]) { - return cache[index]; - } else { - throw new Exception("Value not present"); - } - } - - /** - * Returns true if required value is present and valid in the cache. - * - * @param index which value to check - * @return true if has given value - */ - public boolean has(int index) { - return validity[index]; - } - - /** - * Clears cache. - */ - public void clear() { - for (int i = 0; i < cache.length; i++) { - validity[i] = false; - } - } -} +package logic.neural; + +/** + * Used by neurons to cache inputs for faster NN evaluation performance. + * + * @author fazo + */ +public class NeuronCache { + + private float[] cache; + private float cachedOutput; + private boolean cachedOutputValid; + private boolean[] validity; + + /** + * Create a new empty input cache with given size. + * + * @param size how many inputs the requiring neuron has. + */ + public NeuronCache(int size) { + cache = new float[size]; + validity = new boolean[size]; + clear(); + } + + /** + * Put a value in the cache. + * + * @param index the index of the value + * @param value the value itself + */ + public void put(int index, float value) { + validity[index] = true; + cache[index] = value; + } + + /** + * Read a value from the cache. + * + * @param index the index of the value + * @return the value required + * @throws Exception if value not stored or declared invalid + */ + public float get(int index) throws Exception { + if (validity[index]) { + return cache[index]; + } else { + throw new Exception("Value not present"); + } + } + + /** + * Returns true if required value is present and valid in the cache. + * + * @param index which value to check + * @return true if has given value + */ + public boolean has(int index) { + return validity[index]; + } + + public float getCachedOutput() { + return cachedOutput; + } + + public boolean hasCachedOutput() { + return cachedOutputValid; + } + + public void setCachedOutput(float cachedOutput) { + this.cachedOutput = cachedOutput; + } + + /** + * Clears cache. + */ + public void clear() { + for (int i = 0; i < cache.length; i++) { + validity[i] = false; + } + cachedOutputValid = false; + cachedOutput = 0; + } +}