328 lines
11 KiB
Java
328 lines
11 KiB
Java
// Class CritterModel keeps track of the state of the critter simulation.
|
|
|
|
import java.util.*;
|
|
import java.awt.Point;
|
|
import java.awt.Color;
|
|
import java.lang.reflect.*;
|
|
|
|
public class CritterModel {
|
|
private int height;
|
|
private int width;
|
|
private Critter[][] grid;
|
|
private Map<Critter, PrivateData> info;
|
|
private SortedMap<String, Integer>critterCount;
|
|
private boolean debugView;
|
|
private int simulationCount;
|
|
private static boolean created;
|
|
|
|
public CritterModel(int width, int height) {
|
|
// this prevents someone from trying to create their own copy of
|
|
// the GUI components
|
|
if (created)
|
|
throw new RuntimeException("Only one world allowed");
|
|
created = true;
|
|
|
|
this.width = width;
|
|
this.height = height;
|
|
grid = new Critter[width][height];
|
|
info = new HashMap<Critter, PrivateData>();
|
|
critterCount = new TreeMap<String, Integer>();
|
|
this.debugView = false;
|
|
}
|
|
|
|
public Iterator<Critter> iterator() {
|
|
return info.keySet().iterator();
|
|
}
|
|
|
|
public Point getPoint(Critter c) {
|
|
return info.get(c).p;
|
|
}
|
|
|
|
public Color getColor(Critter c) {
|
|
return info.get(c).color;
|
|
}
|
|
|
|
public String getString(Critter c) {
|
|
return info.get(c).string;
|
|
}
|
|
|
|
public void add(int number, Class<? extends Critter> critter) {
|
|
Random r = new Random();
|
|
Critter.Direction[] directions = Critter.Direction.values();
|
|
if (info.size() + number > width * height)
|
|
throw new RuntimeException("adding too many critters");
|
|
for (int i = 0; i < number; i++) {
|
|
Critter next;
|
|
try {
|
|
next = makeCritter(critter);
|
|
} catch (IllegalArgumentException e) {
|
|
System.out.println("ERROR: " + critter + " does not have" +
|
|
" the appropriate constructor.");
|
|
System.exit(1);
|
|
return;
|
|
} catch (Exception e) {
|
|
System.out.println("ERROR: " + critter + " threw an " +
|
|
" exception in its constructor.");
|
|
System.exit(1);
|
|
return;
|
|
}
|
|
int x, y;
|
|
do {
|
|
x = r.nextInt(width);
|
|
y = r.nextInt(height);
|
|
} while (grid[x][y] != null);
|
|
grid[x][y] = next;
|
|
|
|
Critter.Direction d = directions[r.nextInt(directions.length)];
|
|
info.put(next, new PrivateData(new Point(x, y), d));
|
|
}
|
|
String name = critter.getName();
|
|
if (!critterCount.containsKey(name))
|
|
critterCount.put(name, number);
|
|
else
|
|
critterCount.put(name, critterCount.get(name) + number);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private Critter makeCritter(Class critter) throws Exception {
|
|
Constructor c = critter.getConstructors()[0];
|
|
if (critter.toString().equals("class Bear")) {
|
|
// flip a coin
|
|
boolean b = Math.random() < 0.5;
|
|
return (Critter) c.newInstance(new Object[] {b});
|
|
} else {
|
|
return (Critter) c.newInstance();
|
|
}
|
|
}
|
|
|
|
public int getWidth() {
|
|
return width;
|
|
}
|
|
|
|
public int getHeight() {
|
|
return height;
|
|
}
|
|
|
|
public String getAppearance(Critter c) {
|
|
// Override specified toString if debug flag is true
|
|
if (!debugView)
|
|
return info.get(c).string;
|
|
else {
|
|
PrivateData data = info.get(c);
|
|
if (data.direction == Critter.Direction.NORTH) return "^";
|
|
else if (data.direction == Critter.Direction.SOUTH) return "v";
|
|
else if (data.direction == Critter.Direction.EAST) return ">";
|
|
else return "<";
|
|
}
|
|
}
|
|
|
|
public void toggleDebug() {
|
|
this.debugView = !this.debugView;
|
|
}
|
|
|
|
private boolean inBounds(int x, int y) {
|
|
return (x >= 0 && x < width && y >= 0 && y < height);
|
|
}
|
|
|
|
private boolean inBounds(Point p) {
|
|
return inBounds(p.x, p.y);
|
|
}
|
|
|
|
// returns the result of rotating the given direction clockwise
|
|
private Critter.Direction rotate(Critter.Direction d) {
|
|
if (d == Critter.Direction.NORTH) return Critter.Direction.EAST;
|
|
else if (d == Critter.Direction.SOUTH) return Critter.Direction.WEST;
|
|
else if (d == Critter.Direction.EAST) return Critter.Direction.SOUTH;
|
|
else return Critter.Direction.NORTH;
|
|
}
|
|
|
|
private Point pointAt(Point p, Critter.Direction d) {
|
|
if (d == Critter.Direction.NORTH) return new Point(p.x, p.y - 1);
|
|
else if (d == Critter.Direction.SOUTH) return new Point(p.x, p.y + 1);
|
|
else if (d == Critter.Direction.EAST) return new Point(p.x + 1, p.y);
|
|
else return new Point(p.x - 1, p.y);
|
|
}
|
|
|
|
private Info getInfo(PrivateData data, Class original) {
|
|
Critter.Neighbor[] neighbors = new Critter.Neighbor[4];
|
|
Critter.Direction d = data.direction;
|
|
Critter.Direction[] neighborDirections = new Critter.Direction[4];
|
|
for (int i = 0; i < 4; i++) {
|
|
neighbors[i] = getStatus(pointAt(data.p, d), original);
|
|
if (neighbors[i] == Critter.Neighbor.OTHER ||
|
|
neighbors[i] == Critter.Neighbor.SAME){
|
|
Point p = pointAt(data.p, d);
|
|
PrivateData oldData = info.get(grid[p.x][p.y]);
|
|
neighborDirections[i] = oldData.direction;
|
|
} else {
|
|
neighborDirections[i] = Critter.Direction.NORTH;
|
|
}
|
|
d = rotate(d);
|
|
}
|
|
return new Info(neighbors, data.direction, neighborDirections);
|
|
}
|
|
|
|
private Critter.Neighbor getStatus(Point p, Class original) {
|
|
if (!inBounds(p))
|
|
return Critter.Neighbor.WALL;
|
|
else if (grid[p.x][p.y] == null)
|
|
return Critter.Neighbor.EMPTY;
|
|
else if (grid[p.x][p.y].getClass() == original)
|
|
return Critter.Neighbor.SAME;
|
|
else
|
|
return Critter.Neighbor.OTHER;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public void update() {
|
|
simulationCount++;
|
|
Object[] list = info.keySet().toArray();
|
|
Collections.shuffle(Arrays.asList(list));
|
|
|
|
// This keeps track of critters that are locked and cannot be
|
|
// infected this turn. The happens when:
|
|
// * a Critter is infected
|
|
// * a Critter hops
|
|
Set<Critter> locked = new HashSet<Critter>();
|
|
|
|
for (int i = 0; i < list.length; i++) {
|
|
Critter next = (Critter)list[i];
|
|
PrivateData data = info.get(next);
|
|
if (data == null) {
|
|
// happens when creature was infected earlier in this round
|
|
continue;
|
|
}
|
|
Point p = data.p;
|
|
Point p2 = pointAt(p, data.direction);
|
|
|
|
|
|
// try to perform the critter's action
|
|
Critter.Action move = next.getMove(getInfo(data, next.getClass()));
|
|
if (move == Critter.Action.LEFT)
|
|
data.direction = rotate(rotate(rotate(data.direction)));
|
|
else if (move == Critter.Action.RIGHT)
|
|
data.direction = rotate(data.direction);
|
|
else if (move == Critter.Action.HOP) {
|
|
if (inBounds(p2) && grid[p2.x][p2.y] == null) {
|
|
grid[p2.x][p2.y] = grid[p.x][p.y];
|
|
grid[p.x][p.y] = null;
|
|
data.p = p2;
|
|
locked.add(next); //successful hop locks a critter from
|
|
// being infected for the rest of the
|
|
// turn
|
|
}
|
|
} else if (move == Critter.Action.INFECT) {
|
|
if (inBounds(p2) && grid[p2.x][p2.y] != null
|
|
&& grid[p2.x][p2.y].getClass() != next.getClass()
|
|
&& !locked.contains(grid[p2.x][p2.y])) {
|
|
Critter other = grid[p2.x][p2.y];
|
|
// remember the old critter's private data
|
|
PrivateData oldData = info.get(other);
|
|
// then remove that old critter
|
|
String c1 = other.getClass().getName();
|
|
critterCount.put(c1, critterCount.get(c1) - 1);
|
|
String c2 = next.getClass().getName();
|
|
critterCount.put(c2, critterCount.get(c2) + 1);
|
|
info.remove(other);
|
|
// and add a new one to the grid
|
|
try {
|
|
grid[p2.x][p2.y] = makeCritter(next.getClass());
|
|
// This critter has been infected and is now locked
|
|
// for the rest of this turn
|
|
locked.add(grid[p2.x][p2.y]);
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("" + e);
|
|
}
|
|
// and add to the map
|
|
info.put(grid[p2.x][p2.y], oldData);
|
|
}
|
|
}
|
|
}
|
|
updateColorString();
|
|
}
|
|
|
|
// calling this method causes each critter to update the stored color and
|
|
// text for toString; should be called each time update is performed and
|
|
// once before the simulation begins
|
|
public void updateColorString() {
|
|
for (Critter next : info.keySet()) {
|
|
info.get(next).color = next.getColor();
|
|
info.get(next).string = next.toString();
|
|
}
|
|
}
|
|
|
|
public Set<Map.Entry<String, Integer>> getCounts() {
|
|
return Collections.unmodifiableSet(critterCount.entrySet());
|
|
}
|
|
|
|
public int getSimulationCount() {
|
|
return simulationCount;
|
|
}
|
|
|
|
private class PrivateData {
|
|
public Point p;
|
|
public Critter.Direction direction;
|
|
public Color color;
|
|
public String string;
|
|
|
|
public PrivateData(Point p, Critter.Direction d) {
|
|
this.p = p;
|
|
this.direction = d;
|
|
}
|
|
|
|
public String toString() {
|
|
return p + " " + direction;
|
|
}
|
|
}
|
|
|
|
// an object used to query a critter's state (neighbors, direction)
|
|
private static class Info implements CritterInfo {
|
|
private Critter.Neighbor[] neighbors;
|
|
private Critter.Direction direction;
|
|
private Critter.Direction[] neighborDirections;
|
|
|
|
public Info(Critter.Neighbor[] neighbors, Critter.Direction d,
|
|
Critter.Direction[] neighborDirections) {
|
|
this.neighbors = neighbors;
|
|
this.direction = d;
|
|
this.neighborDirections = neighborDirections;
|
|
}
|
|
|
|
public Critter.Neighbor getFront() {
|
|
return neighbors[0];
|
|
}
|
|
|
|
public Critter.Neighbor getBack() {
|
|
return neighbors[2];
|
|
}
|
|
|
|
public Critter.Neighbor getLeft() {
|
|
return neighbors[3];
|
|
}
|
|
|
|
public Critter.Neighbor getRight() {
|
|
return neighbors[1];
|
|
}
|
|
|
|
public Critter.Direction getDirection() {
|
|
return direction;
|
|
}
|
|
|
|
public Critter.Direction getFrontDirection() {
|
|
return neighborDirections[0];
|
|
}
|
|
|
|
public Critter.Direction getBackDirection() {
|
|
return neighborDirections[2];
|
|
}
|
|
|
|
public Critter.Direction getLeftDirection() {
|
|
return neighborDirections[3];
|
|
}
|
|
|
|
public Critter.Direction getRightDirection() {
|
|
return neighborDirections[1];
|
|
}
|
|
}
|
|
}
|