/* * NanoPond.java * * Copyright (C) 2007 Thomas Abeel * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * This program was based on the Nanopond 1.9 C program by Adam Ierymenko * http://www.greythumb.org/wiki/Nanopond * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110 USA * */ package be.ppareit.nanopond; import android.util.Log; import static be.ppareit.android.Utils.sleepIgnoreInterrupt; public class NanoPond { private static final String TAG = NanoPond.class.getSimpleName(); /* All available instructions */ String[] names = {"ZERO", "FWD", "BACK", "INC", "DEC", "READG", "WRITEG", "READB", "WRITEB", "LOOP", "REP", "TURN", "XCHG", "KILL", "SHARE", "STOP"}; /* * Frequency of comprehensive reports-- lower values will provide more info while * slowing down the simulation. Higher values will give less frequent updates. This is * also the frequency of screen refreshes if SDL is enabled. */ public static final int REPORT_FREQUENCY = 100000; /* * How frequently should random cells / energy be introduced? Making this too high * makes things very chaotic. Making it too low might not introduce enough energy. */ public static final int INFLOW_FREQUENCY = 100; /* Base amount of energy to introduce per INFLOW_FREQUENCY ticks */ public static final int INFLOW_RATE_BASE = 4000; /* * A random amount of energy between 0 and this is added to INFLOW_RATE_BASE when * energy is introduced. Comment this out for no variation in inflow rate. */ public static final int INFLOW_RATE_VARIATION = 8000; public static final int POND_SIZE_X = 160; public static final int POND_SIZE_Y = 120; /* * Maximum genome size. */ public static final int POND_DEPTH = 64; /* * This is the divisor that determines how much energy is taken from cells when they * try to KILL a viable cell neighbor and fail. Higher numbers mean lower penalties. */ public static final int FAILED_KILL_PENALTY = 2; byte[] startBuffer = new byte[POND_DEPTH]; enum Direction { LEFT, RIGHT, UP, DOWN; public static Direction getDirection(int i) { switch (i) { case 0: return LEFT; case 1: return RIGHT; case 2: return UP; case 3: return DOWN; default: throw new RuntimeException("Unknown direction requested: " + i); } } } public class Cell { long generation; long ID; long parentID; long lineage; int energy; byte[] genome; public Cell() { this.ID = 0; this.parentID = 0; this.lineage = 0; this.generation = 0; this.energy = 0; this.genome = new byte[POND_DEPTH]; System.arraycopy(startBuffer, 0, genome, 0, POND_DEPTH); } /** * Fill genome of the cell with random instruction */ public void setRandomGenome() { for (int i = 0; i < POND_DEPTH; i++) { genome[i] = (byte) rg.nextInt(16); } } public String getHexa() { return hexa(genome); } public void setGenome(String hex) { System.arraycopy(startBuffer, 0, genome, 0, POND_DEPTH); for (int i = 0; i < hex.length(); ++i) { char ch = hex.charAt(i); if ('0' <= ch && ch <= '9') { genome[i] = (byte) (ch - '0'); } else if ('a' <= ch && ch <= 'f') { genome[i] = (byte) (ch - 'a' + 10); } else if ('A' <= ch && ch <= 'F') { genome[i] = (byte) (ch - 'A' + 10); } else { Log.e(TAG, "Failed to parse hex string for the genome"); setRandomGenome(); } } } } /* The pond is a 2D array of cells */ Cell[][] pond = new Cell[POND_SIZE_X][POND_SIZE_Y]; private static MTRandom rg = new MTRandom(); /** * Class for keeping some running tally type statistics */ class PerReportStatCounters { long[] instructionExecutions = new long[16]; long cellExecutions = 0; long viableCellsReplaced = 0; long viableCellsKilled = 0; long viableCellShares = 0; public void reset() { // instructionExecutions = new long[16]; for (int i = 0; i < instructionExecutions.length; ++i) { instructionExecutions[i] = 0; } cellExecutions = 0; viableCellsReplaced = 0; viableCellsKilled = 0; viableCellShares = 0; } } /* Global statistics counters */ PerReportStatCounters statCounters = new PerReportStatCounters(); boolean replicatorMessage = false; static class Report { long year; long energy; long maxGeneration; long activeCells; long viableReplicators; long kills; long replaced; long shares; } static private Report report = new Report(); public Report getReport() { report.year = clock; long totalActiveCells = 0; long totalEnergy = 0; long totalViableReplicators = 0; long maxGeneration = 0; for (int i = 0; i < POND_SIZE_X; i++) { for (int j = 0; j < POND_SIZE_Y; j++) { Cell c = pond[i][j]; if (c.energy > 0) { totalActiveCells++; totalEnergy += c.energy; if (c.generation > 2) { totalViableReplicators++; } if (c.generation > maxGeneration) { maxGeneration = c.generation; } } } } if (maxGeneration > 2 && !replicatorMessage) { replicatorMessage = true; System.out.println("[EVENT] Replicators have evolved in the year " + clock); } if (maxGeneration <= 2 && replicatorMessage) { replicatorMessage = false; System.out.println("[EVENT] Replicators have gone extinct in the year " + clock); } report.energy = totalEnergy; report.maxGeneration = maxGeneration; report.activeCells = totalActiveCells; report.viableReplicators = totalViableReplicators; report.kills = statCounters.viableCellsKilled; report.replaced = statCounters.viableCellsReplaced; report.shares = statCounters.viableCellShares; statCounters.reset(); return report; } /** * Get a neighbor in the pond * * @param x Starting X position * @param y Starting Y position * @param dir Direction to get neighbor from * @return neighboring cell */ public Cell getNeighbor(int x, int y, Direction dir) { switch (dir) { case LEFT: return x != 0 ? pond[x - 1][y] : pond[POND_SIZE_X - 1][y]; case RIGHT: return (x < (POND_SIZE_X - 1)) ? pond[x + 1][y] : pond[0][y]; case UP: return y != 0 ? pond[x][y - 1] : pond[x][POND_SIZE_Y - 1]; case DOWN: return (y < (POND_SIZE_Y - 1)) ? pond[x][y + 1] : pond[x][0]; default: throw new RuntimeException("Unknown direction!"); } } int[] BITS = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}; /** * Determines whether neighbor cell is accessible by the cell with the register value. * * @param reg register value of the attacking cell * @param positiveInteraction kill & replace : false / share : true * @param neighbor the neighboring cell * @return true if access to the neighbor is allowed, false in other cases */ private boolean accessAllowed(Cell neighbor, byte reg, boolean positiveInteraction) { if (neighbor.parentID == 0) { return true; } if (positiveInteraction) { return BITS[neighbor.genome[0] ^ reg] <= rg.nextInt(4); } else { return BITS[neighbor.genome[0] ^ reg] >= rg.nextInt(4); } } /** * Constructor of the world : fill all genomes with 512 STOP's */ public NanoPond() { for (int i = 0; i < POND_DEPTH; i++) { startBuffer[i] = (byte) 0xf; /* STOP instruction */ } for (int i = 0; i < POND_SIZE_X; i++) { for (int j = 0; j < POND_SIZE_Y; j++) { pond[i][j] = new Cell(); } } } volatile boolean isRunning = false; Thread thread = null; public void run() { if (thread == null) { isRunning = true; thread = new Thread(() -> { int threadCounter = 0; while (isRunning) { if (threadCounter % 1013 == 0) { sleepIgnoreInterrupt(1); } singleStep(); ++threadCounter; } }); thread.start(); } } public void pauze() { if (thread != null) { isRunning = false; boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { // swallow } } thread = null; } } /* Clock is incremented on each core loop */ private long clock = -1; /* This is used to generate unique cell IDs */ private long cellIdCounter = 0; private final double MUTATION_RATE = 0.000005; /* Buffer used for execution output of candidate offspring */ private final byte[] outputBuf = new byte[POND_DEPTH]; /* Virtual machine loop/rep stack */ private final int loopStackPointer[] = new int[POND_DEPTH]; /* Main loop */ public void singleStep() { Direction facing = Direction.getDirection(rg.nextInt(4)); /* increment clock */ clock++; /* * Introduce a random cell somewhere with a given energy level. This is called * seeding and introduces both energy and entropy into the substrate. This happens * every INFLOW_FREQUENCY clock ticks. */ if (clock % INFLOW_FREQUENCY == 0) { int x = rg.nextInt(POND_SIZE_X); int y = rg.nextInt(POND_SIZE_Y); pond[x][y].ID = cellIdCounter; pond[x][y].parentID = 0; pond[x][y].lineage = cellIdCounter; pond[x][y].generation = 0; pond[x][y].energy = INFLOW_RATE_BASE + (int) (rg.nextDouble() * INFLOW_RATE_VARIATION); pond[x][y].setRandomGenome(); cellIdCounter++; } /* Pick a random cell to execute */ int x = rg.nextInt(POND_SIZE_X); int y = rg.nextInt(POND_SIZE_Y); Cell c = pond[x][y]; /* Reset VM */ System.arraycopy(startBuffer, 0, outputBuf, 0, POND_DEPTH); byte reg = 0; int pointer = 0; int loopStackPtr = 0; int falseLoopDepth = 0; boolean stop = false; /* Keep track of how many cells have been executed */ statCounters.cellExecutions++; /* Core execution loop */ int instructionIndex = 0;// the current instruction index while (c.energy > 0 && !stop) { /* * Randomly frob either the instruction or the register with a probability * defined by MUTATION_RATE. This introduces variation, and since the * variation is introduced into the state of the VM it can have all manner of * different effects on the end result. */ // This is faulty execution by duplicating or skipping instructions if (rg.nextDouble() < MUTATION_RATE) { int type = rg.nextInt(4); switch (type) { /* replacement */ case 0: c.genome[instructionIndex] = (byte) rg.nextInt(16); break; /* change register */ case 1: reg = (byte) rg.nextInt(16); break; /* duplicate instruction execution */ case 2: if (instructionIndex == 0) { instructionIndex = POND_DEPTH; } instructionIndex--; break; /* skip instruction */ case 3: instructionIndex++; instructionIndex %= POND_DEPTH; break; } } /* Each instruction processed costs one unit of energy */ c.energy--; /* Execute the instruction */ if (falseLoopDepth > 0) { /* * Skip forward to matching REP if we're in a false loop. */ if (c.genome[instructionIndex] == 9) { falseLoopDepth++; } /* * Decrement on REP */ else if (c.genome[instructionIndex] == 10) { falseLoopDepth--; } } else { /* * Keep track of execution frequencies for each instruction */ statCounters.instructionExecutions[c.genome[instructionIndex]]++; switch (c.genome[instructionIndex]) { case 0x0: /* ZERO: Zero VM state registers */ reg = 0; pointer = 0; break; case 0x1: /* FWD: Increment the pointer (wrap at end) */ pointer++; pointer %= POND_DEPTH; break; case 0x2: /* BACK: Decrement the pointer (wrap at beginning) */ if (pointer == 0) { pointer = POND_DEPTH; } pointer--; break; case 0x3: /* INC: Increment the register */ reg++; reg %= 16; break; case 0x4: /* DEC: Decrement the register */ if (reg == 0) { reg = 16; } reg--; break; case 0x5: /* READG: Read into the register from genome */ reg = c.genome[pointer]; break; case 0x6: /* WRITEG: Write out from the register to genome */ c.genome[pointer] = reg; break; case 0x7: /* READB: Read into the register from buffer */ reg = outputBuf[pointer]; break; case 0x8: /* WRITEB: Write out from the register to buffer */ outputBuf[pointer] = reg; break; case 0x9: /* * LOOP: Jump forward to matching REP if register is zero */ if (reg > 0) { if (loopStackPtr >= POND_DEPTH) /* Stack overflow ends execution */ { stop = true; } else { loopStackPointer[loopStackPtr] = instructionIndex; loopStackPtr++; } } else { falseLoopDepth = 1; } break; case 0xa: /* * REP: Jump back to matching LOOP if register is nonzero */ if (loopStackPtr > 0) { loopStackPtr--; if (reg > 0) { instructionIndex = loopStackPointer[loopStackPtr]; /* * This ensures that the LOOP is rerun and that the * instruction pointer has not yet changed. */ continue; } } break; case 0xb: /* * TURN: Turn in the direction specified by register */ facing = Direction.getDirection(Math.abs(reg) % 4); break; case 0xc: /* * XCHG: Skip next instruction and exchange value of reg with it */ instructionIndex++; instructionIndex %= POND_DEPTH; byte tmp = reg; reg = c.genome[instructionIndex]; c.genome[instructionIndex] = tmp; break; case 0xd: /* * KILL: Blow away neighboring cell if allowed with penalty on * failure */ Cell neighborKill = getNeighbor(x, y, facing); if (accessAllowed(neighborKill, reg, false)) { if (neighborKill.generation > 2) { statCounters.viableCellsKilled++; } /* * putting a STOP instruction as first instruction will kill the * neighboring cell */ neighborKill.genome[0] = 15; neighborKill.ID = cellIdCounter; neighborKill.parentID = 0; neighborKill.lineage = cellIdCounter; neighborKill.generation = 0; cellIdCounter++; } else if (neighborKill.generation > 2) { c.energy /= FAILED_KILL_PENALTY; } break; case 0xe: /* * SHARE: Equalize energy between self and neighbor if allowed */ Cell neighborShare = getNeighbor(x, y, facing); if (accessAllowed(neighborShare, reg, true)) { if (neighborShare.generation > 2) { statCounters.viableCellShares++; } int newEnergy = (c.energy + neighborShare.energy) / 2; c.energy = newEnergy; neighborShare.energy = newEnergy; } break; case 0xf: /* STOP: End execution */ stop = true; break; } } /* * Increase instruction pointer and loop at the end of the genome */ instructionIndex++; instructionIndex %= POND_DEPTH; } /* * Copy outputBuf into neighbor if access is permitted and there is energy there * to make something happen. There is no need to copy to a cell with no energy, * since anything copied there would never be executed and then would be replaced * with random junk eventually. See the seeding code in the main loop above. */ if (outputBuf[0] != 15) { Cell neighbor = getNeighbor(x, y, facing); if (neighbor.energy > 0 && accessAllowed(neighbor, reg, false)) { /* Log it if we're replacing a viable cell */ if (neighbor.generation > 0) { statCounters.viableCellsReplaced++; } neighbor.ID = cellIdCounter++; neighbor.parentID = c.ID; /* * Lineage is copied in offspring */ neighbor.lineage = c.lineage; neighbor.generation = c.generation + 1; // This is a 'faulty' copy mechanism that allows minor // mutations to enter when copying the cell // alternative non faulty: // System.arraycopy(outputBuf, 0, neighbor.genome, 0, POND_DEPTH); int i = 0, j = 0; while (i < POND_DEPTH && j < POND_DEPTH) { if (rg.nextDouble() < MUTATION_RATE) { int type = rg.nextInt(3); switch (type) { /* replacement */ case 0: neighbor.genome[i++] = (byte) rg.nextInt(16); j++; break; /* duplicate instruction execution */ case 1: neighbor.genome[i++] = outputBuf[j]; i %= POND_DEPTH; neighbor.genome[i++] = outputBuf[j++]; break; /* skip instruction */ case 2: i++; j++; break; } } else { /* no mutation */ neighbor.genome[i++] = outputBuf[j++]; } } } } } private String hexa(byte[] genome) { StringBuilder out = new StringBuilder(); for (byte aGenome : genome) { out.append(Integer.toHexString(aGenome)); } return out.substring(0, out.indexOf("ff") + 1); } /* Used for unique seed ids */ long seedingID = -1; public boolean seed(int x, int y, byte[] genome) { Cell c = pond[x][y]; c.generation = 5; c.energy = 10000; c.parentID = seedingID; c.ID = seedingID; c.lineage = seedingID; seedingID--; System.arraycopy(genome, 0, c.genome, 0, POND_DEPTH); return true; } }