Skip to content

Instantly share code, notes, and snippets.

@LyndonArmitage
Created July 30, 2014 18:16
Show Gist options
  • Save LyndonArmitage/660c80f94c7a1a60cbb8 to your computer and use it in GitHub Desktop.
Save LyndonArmitage/660c80f94c7a1a60cbb8 to your computer and use it in GitHub Desktop.
Reddit DailyProgrammer Challenge 173: Langtons Ant
package com.lyndonarmitage.daily.langton;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* Grid class containing all data on the Ants world
* @author Lyndon Armitage
*/
public class Grid {
private Map<String,Point> points;
private ArrayList<Color> colours;
/**
* Create an empty grid
* @param maxColours the maximum amount of different colours that a point can be
*/
public Grid(int maxColours) {
points = new HashMap<String, Point>();
colours = getDefaultColours(maxColours);
}
/**
* Preallocate points to the grid
* @param width width of the grid
* @param height height of the grid
* @param maxColours the maximum amount of different colours that a point can be
*/
public Grid(int width, int height, int maxColours) {
points = new HashMap<String, Point>(width * height);
for(int x = 0; x < width; x ++) {
for(int y = 0; y < height; y ++) {
Point p = new Point(x, y, (byte) 0);
points.put(x + "," + y, p);
}
}
colours = getDefaultColours(maxColours);
}
public static ArrayList<Color>getDefaultColours(int maxColours) {
// an array of preselected colours
final Color colourArray[] = {
Color.black,
Color.white,
Color.red,
Color.green,
Color.blue,
Color.yellow,
Color.pink,
};
ArrayList<Color> colours = new ArrayList<Color>(maxColours);
int i;
for(i = 0; i < maxColours && i < colourArray.length; i ++) {
colours.add(colourArray[i]);
}
if(i < maxColours) {
// Fill extra colours with random colours
// Doesn't check for duplicates or similar shades
Random rnd = new Random();
while (i < maxColours) {
colours.add(new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)));
i ++;
}
}
return colours;
}
public ArrayList<Color> getColours() {
return colours;
}
public void flipPoint(int x, int y, boolean reverse) {
Point p = getPoint(x, y);
addColour(p, reverse);
}
private void addColour(Point point, boolean subtract) {
byte newColour = (byte) (subtract ? point.colour-1 : point.colour+1);
if(newColour >= colours.size()) {
point.colour = 0;
}
else if(newColour < 0){
point.colour = (byte) (colours.size()-1);
}
else {
point.colour = newColour;
}
// point.traversed = true;
}
public Point getPoint(int x, int y) {
Point point = points.get(x+","+y);
if(point != null) {
return point;
}
else {
Point newPoint = new Point(x, y, (byte) 0);
points.put(x+","+y, newPoint);
return newPoint;
}
}
public Point getLowestPoint() {
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
for(String s : points.keySet()) {
Point p = points.get(s);
if(p.x < minX) {
minX = p.x;
}
if(p.y < minY) {
minY = p.y;
}
}
return getPoint(minX, minY);
}
public Point getHighestPoint() {
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
for(String s : points.keySet()) {
Point p = points.get(s);
if(p.x > maxX) {
maxX = p.x;
}
if(p.y > maxY) {
maxY = p.y;
}
}
return getPoint(maxX, maxY);
}
public void saveGridImage() {
// Saving the grid requires us to align the points so they are all positive
Point lowest = getLowestPoint();
Point highest = getHighestPoint();
int width, height;
int adjustX, adjustY;
if(lowest.getX() < 0) {
adjustX = Math.abs(lowest.getX());
}
else {
adjustX = -lowest.getX();
}
if(lowest.getY() < 0) {
adjustY = Math.abs(lowest.getY());
}
else {
adjustY = -lowest.getY();
}
// the width and height of the resulting image
width = highest.getX() + adjustX + 1;
height = highest.getY() + adjustY + 1;
// Get the colours the grid has been told to use
ArrayList<Color> colours = getColours();
// Create an image ready to be drawn on
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// Fills the image with the default colour
Graphics2D g2 = image.createGraphics();
g2.setColor(colours.get(0));
g2.fillRect(0,0, width-1, height-1);
g2.dispose();
// Set the pixel data for each point
for(String s : points.keySet()) {
Point p = points.get(s);
int x, y;
x = p.getX() + adjustX;
y = p.getY() + adjustY;
image.setRGB(x, y, colours.get(p.getColour()).getRGB());
}
// Saving image logic below
JFileChooser chooser = new JFileChooser("."); // make a chooser in the current directory
// setup an appropriate file filter
chooser.setFileFilter(new FileFilter() {
@Override
public boolean accept(File f) {
return f.isDirectory() || f.getName().toLowerCase().endsWith(".png");
}
@Override
public String getDescription() {
return "Save PNG";
}
});
// Display the dialog
if(chooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
File saveFile = chooser.getSelectedFile();
// Check to make sure the file has png at the end of it's name and if not append it
if(!saveFile.getName().toLowerCase().endsWith(".png")) {
saveFile = new File(saveFile.getParent(), saveFile.getName() + ".png");
}
// Try to save the image out
try {
ImageIO.write(image, "png", saveFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Inner class to represent a point on the grid
*/
public class Point {
private int x;
private int y;
private byte colour;
/**
* Create a new Point
* @param x the x position on the grid
* @param y the y position on the grid
* @param colour the colour index of the new point
*/
public Point(int x, int y, byte colour) {
this.x = x;
this.y = y;
this.colour = colour;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public byte getColour() {
return colour;
}
}
}
package com.lyndonarmitage.daily.langton;
import java.util.ArrayList;
/**
* Langton's technicolor ant
* This contains all the ants code, and the main method to run the example with.
* @author Lyndon Armitage
*/
public class LangtonsAnt {
public static final int GRID_INITIAL_WIDTH = 100, GRID_INITIAL_HEIGHT = 100;
public static final int DEFAULT_ITERATIONS = 250000;
public static final String DEFAULT_RULES = "LRLRR";
private Grid grid;
private int x;
private int y;
private Direction dir = Direction.EAST;
private ArrayList<Boolean> rules;
/**
* The main run code
* @param args first argument should be the rules as a string of Ls and Rs,
* second argument should be an amount of iterations
* @throws UnknownRuleException
*/
public static void main(String args[]) throws UnknownRuleException {
String rules;
int iterations;
if(args.length > 0) {
rules = args[0];
if(args.length > 1) {
iterations = Integer.parseInt(args[1]);
}
else {
iterations = DEFAULT_ITERATIONS;
}
}
else {
rules = DEFAULT_RULES;
iterations = DEFAULT_ITERATIONS;
}
System.out.println("Running langtons ant with rules: " + rules + " for " + iterations + " iterations");
Grid grid = new Grid(GRID_INITIAL_WIDTH, GRID_INITIAL_HEIGHT, rules.length());
LangtonsAnt ant = new LangtonsAnt(grid, GRID_INITIAL_WIDTH / 2, GRID_INITIAL_HEIGHT / 2, rules);
long startTime = System.currentTimeMillis();
for(int i = 0; i < iterations; i ++) {
// Technically speaking I could have multiple ants running on the same grid one after the other with ease.
ant.iterate();
}
long endTime = System.currentTimeMillis();
float secondsTaken = (float)(endTime - startTime) / 1000 ;
System.out.println("Took " + secondsTaken + " seconds");
grid.saveGridImage();
}
/**
* Constructor for the ant
* @param grid the grid the ant will occupy
* @param startX the x position on the grid where the ant will start
* @param startY the y position on the grid where the ant will start
* @param rules a set of rules as a string for each different colour e.g. "LLRR"
* @throws UnknownRuleException
*/
public LangtonsAnt(Grid grid, int startX, int startY, String rules) throws UnknownRuleException {
this.grid = grid;
this.x = startX;
this.y = startY;
this.rules = new ArrayList<Boolean>(Math.min(rules.length(), grid.getColours().size()));
rules = rules.toLowerCase();
for(int i = 0; i < rules.length() && i < grid.getColours().size(); i ++) {
char rule = rules.charAt(i);
if(rule == 'l') {
this.rules.add(true);
}
else if(rule == 'r') {
this.rules.add(false);
}
else {
throw new UnknownRuleException("Unknown rule: " + rule);
}
}
}
/**
* Perform a step
*/
public void iterate() {
Grid.Point current = grid.getPoint(x, y);
boolean counterClockwise = rules.get(current.getColour());
turn(counterClockwise);
grid.flipPoint(x, y, false);
moveForward();
}
/**
* Move the ant forward in its current direction
*/
private void moveForward() {
switch (dir) {
case NORTH:
y ++;
break;
case EAST:
x ++;
break;
case SOUTH:
y --;
break;
case WEST:
x --;
break;
}
}
/**
* Turn the ant
* @param counterClockwise whether we should turn the ant counterclockwise/anticlockwise or clockwise
*/
private void turn(boolean counterClockwise) {
int newOrdinal = counterClockwise ? dir.ordinal()-1 : dir.ordinal() + 1;
if(newOrdinal >= Direction.values().length) {
newOrdinal = 0;
}
else if(newOrdinal < 0) {
newOrdinal = Direction.values().length-1;
}
dir = Direction.values()[newOrdinal];
}
public enum Direction {
NORTH, EAST, SOUTH, WEST
}
/**
* An exception for when a weird argument is given
*/
public class UnknownRuleException extends Exception {
public UnknownRuleException(String message) {
super(message);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment