package biosim;
import java.util.*;
import java.awt.Dimension;
/**
* The Ecosystem maintains a list of Organisms, manages their
* behavior, and acts as an intermediary to facilitate communication
* between them. In order to accomplish this, the Ecosystem
* maintains a queue of events and a clock. With each "tick"
* of the clock, the Ecosystem examines the queue to see if
* there is an event scheduled. If so, it responds to the event,
* perhaps sending another event as a message to one or more of the
* Organisms. The simulation runs in its own thread.
*/
public class Ecosystem extends Observable implements Runnable {
/**
* The list of Organisms that live in this EcoSystem.
*/
public Vector organisms;
/**
* The queue of pending events. An Organism can call
* the Ecosystem's schedule() method to tell the Ecosystem
* to add an EcoEvent to the queue.
*/
protected EventQueue eq;
/**
* The clock is incremented one step at a time in order
* to run the simulation.
*/
protected int clock;
/**
* The size of the 2D space embodied by the Ecosystem.
*/
protected Dimension dim;
/**
* How long to sleep in between iterations. This can
* be manipulated via setSleep() to speed up or slow down
* the simulation.
*/
protected int sleepValue;
/**
* A thread for the simulation.
*/
Thread simThread;
/**
* True if the simulation is running, false if it is stopped.
*/
boolean running = false;
/**
* The default constructor creates an Ecosystem 300 units
* in width, 300 units in height, with a sleep value of 250
* milliseconds.
*/
public Ecosystem() {
this(300, 300, 250);
}
/**
* Create an Ecosystem with the specified dimensions and
* sleep value.
*/
public Ecosystem(int width, int height, int sleep) {
organisms = new Vector();
eq = new EventQueue();
clock = 0;
dim = new Dimension(width, height);
sleepValue = sleep;
}
/**
* Alter the sleep value that determines the speed of the
* simulation. A higher sleep value slows the simulation down;
* a lower value speeds it up. Negative values do not make sense,
* so they are converted to their absolute value.
*/
synchronized public void setSleep(int newValue) {
sleepValue = Math.abs(newValue); // make sure it's positive
}
/**
* Returns true if the simulation is running, false if stopped.
*/
public boolean isRunning() {
return running;
}
/**
* Start the simulation. This method is used to start
* the simulation for the first time. Also, if the simulation
* is stopped, it may be started again with this method.
*/
public void start() {
simThread = new Thread(this);
simThread.start(); // asynchronously calls this.run()
}
/**
* This method runs the simulation in its own thread. Should
* not be called directly, but rather it is called by the system
* when the Ecosystem's start() method calls simThread.start().
*/
public void run() {
Organism org;
running = true;
while (running) {
// run one cycle of the simulation
tick();
// Yield the processor to other threads for a while
try {
simThread.sleep(sleepValue);
} catch (InterruptedException e) {
return;
}
}
}
/**
* Stop the simulation. It may be started again by
* calling start().
*/
public void stop() {
running = false;
simThread = null;
}
/**
* Checks for collisions between organisms. If one is detected,
* both organisms are sent a COLLISION event, to occur at the
* next cycle of the Ecosystem's clock.
*/
void checkForCollisions() {
int i, j;
Organism org1, org2;
synchronized (organisms) {
// For each organism, look at all others to see if
// any are touching it
for (i = 0; i < organisms.size(); i++) {
org1 = (Organism) organisms.elementAt(i);
for (j = i + 1; j < organisms.size(); j++) {
org2 = (Organism) organisms.elementAt(j);
if (org1.isTouching(org2)) {
// Send both organisms a COLLISION event
// Note that we pass in a copy of the velocity.
// The velocity of each Organism will change,
// but each needs to know the velocity of the
// other at the moment of collision.
EcoEvent e1 = new EcoEvent(EcoEvent.COLLISION,
clock + 1, org1, org2, org2.getVelocity());
EcoEvent e2 = new EcoEvent(EcoEvent.COLLISION,
clock + 1, org2, org1, org1.getVelocity());
schedule(e1);
schedule(e2);
}
}
}
}
}
/**
* Schedule an EcoEvent (by adding it to the EventQueue eq). If
* the time field of the EcoEvent field was set to -1, then schedule
* it as soon as possible (i.e., the next clock tick).
*/
public void schedule(EcoEvent e) {
if (e.time == -1) {
e.time = clock + 1;
}
eq.addEvent(e);
}
/**
* Looks at the EventQueue eq to see if there are any events scheduled for
* this time step, and handles them one by one.
*/
void handleCurrentEvents() {
EcoEvent theEvent;
Organism theReceiver;
while (eq.hasMoreElements() && eq.check().time <= clock) {
theEvent = eq.nextEvent();
theReceiver = theEvent.to;
if (theReceiver == null) {
// This is an event directed to the Ecosystem.
// We need to deal with it here.
switch (theEvent.id) {
case EcoEvent.BIRTH:
// One or more new organisms must be added to the list
if (theEvent.attachment instanceof Vector) {
Vector offspring = (Vector) theEvent.attachment;
// Add each of the new offspring to the organism list
for (int i = 0; i < offspring.size(); i++) {
organisms.addElement(offspring.elementAt(i));
}
} else if (theEvent.attachment instanceof Organism) {
// Add the new offspring to the organism list
organisms.addElement(theEvent.attachment);
}
break;
case EcoEvent.DEATH:
// An organism must be removed from the list
Organism deadBeef = theEvent.from;
organisms.removeElement(deadBeef);
// The attachment might contain one or more Organisms
// to be notified of the death.
if (theEvent.attachment != null) {
Organism org;
EcoEvent death;
if (theEvent.attachment instanceof Vector) {
Vector spectators = (Vector) theEvent.attachment;
// Notify each spectator of the death
// We do this by sending a DEATH even to that Organism,
// from the one that died. Because the event is not
// addressed to the EcoSystem, it does not cause a loop.
for (int i = 0; i < spectators.size(); i++) {
org = (Organism) spectators.elementAt(i);
death = new EcoEvent(EcoEvent.DEATH,
EcoEvent.NOW, org, deadBeef, null);
schedule(death);
}
} else if (theEvent.attachment instanceof Organism) {
// Notify the Organism that this Organism died.
org = (Organism) theEvent.attachment;
death = new EcoEvent(EcoEvent.DEATH,
EcoEvent.NOW, org, theEvent.from, null);
schedule(death);
}
}
break;
default:
System.err.println("Unknown EcoEvent ID: " +
theEvent.id);
System.err.println("\t(Sent to Ecosystem.)");
break;
}
} else {
// This is an event directed to an Organism.
// Tell that Organism to deal with the event.
// (But only if that Organism is actually alive to receive it.)
if (organisms.contains(theReceiver)) {
theReceiver.react(theEvent);
}
}
}
}
/**
* This is the method where the real work of the Ecosystem
* is done. First the Ecosystem handles any EcoEvents scheduled
* for this time step. Then, it calls each Organism's advance()
* method. Next, it checks for collisions between Organisms.
* finally, it advances the clock, and notifies any Observers
* that the state of the system has changed.
*/
synchronized public void tick() {
// Handle any current events
handleCurrentEvents();
// Give each Organism a chance to move, or to do
// whatever else its life-pattern requires of it
for (int i = 0; i < organisms.size(); i++) {
Organism org = (Organism) organisms.elementAt(i);
org.advance();
}
// Handle collisions between Organisms
checkForCollisions();
// Advance the clock
clock++;
// Notify any Observers that the state
// of the Ecosystem has changed
setChanged();
notifyObservers();
}
/**
* This method allows an external controller to add one Organism
* to the simulation from the outside, bypassing the Event
* queue. No BIRTH events are generated. The Organism
* just appears. This method calls notifyObservers() after
* adding the Organism.
*/
synchronized public void addOrganism(Organism org) {
organisms.addElement(org);
// Notify any Observers that the state
// of the Ecosystem has changed
setChanged();
notifyObservers();
}
/**
* This method allows an external controller to add a list of
* Organisms to the simulation from the outside, bypassing the Event
* queue. No BIRTH events are generated. The Organisms
* just appear. This method calls notifyObservers() after
* adding the Organisms.
*/
synchronized public void addOrganisms(Vector orgs) {
for (int i = 0; i < orgs.size(); i++) {
organisms.addElement(orgs.elementAt(i));
}
// Notify any Observers that the state
// of the Ecosystem has changed
setChanged();
notifyObservers();
}
/**
* This method allows an external controller to delete
* an Organism from the outside, bypassing the Event queue.
* No DEATH events are generated. The Organism just disappears.
* This method calls notifyObservers() after deleting the
* Organism.
*/
synchronized public void removeOrganism(Organism org) {
organisms.removeElement(org);
// Notify any Observers that the state
// of the Ecosystem has changed
setChanged();
notifyObservers();
}
/**
* This method resets the simulation, removing all Organisms and
* Events, and resetting the clock to zero.
*/
synchronized public void reset() {
organisms = new Vector();
eq = new EventQueue();
clock = 0;
// Notify any Observers that the state
// of the Ecosystem has changed
setChanged();
notifyObservers();
}
/**
* Calls tick() the specified number of times.
*/
synchronized public void tick(int times) {
for (int i = 0; i < times; i++) {
tick();
}
}
/**
* Resize the 2D space of the simulation.
*/
synchronized public void resize(int width, int height) {
dim = new Dimension(width, height);
}
/**
* Resize the 2D space of the simulation.
*/
synchronized public void resize(Dimension d) {
dim = new Dimension(d);
}
/**
* Report the size of the 2D space for the simulation.
*/
public Dimension size() {
return new Dimension(dim);
}
}
/**
* The EventQueue is kept in sorted order by the time field of
* the EcoEvents it contains. An EcoEvents is added to the queue
* by passing it into the addEvent() method. When this occurs, they
* automatically propogate into their sorted location. To get
* EcoEvents from the queue, you call nextEvent() (or, if you are
* using the EventQueue as a generic Enumeration, nextElement()).
* To get a handle to the first element in the queue without
* popping it off, you can call check().
*/
class EventQueue implements Enumeration {
/**
* The Vector that actually holds the EcoEvent objects.
*/
protected Vector q = new Vector();
/**
* Construct an empty EventQueue.
*/
EventQueue() {
q = new Vector();
}
/**
* Place an event in the event queue, to occur at the time
* step specified in the given EcoEvent object.
*/
synchronized public void addEvent(EcoEvent evt) {
int n = q.size();
int i = 0;
// Find the proper place in the queue
looking: for (i = 0; i < n; i++ ) {
if (((EcoEvent) q.elementAt(i)).time > evt.time) {
break looking;
}
}
// Either we've found a place within the queue
// to insert the element, or we're at the end.
q.insertElementAt(evt, i);
}
/**
* Implements Enumeration.hasMoreElements()
* Returns true if there are any elements in the EventQueue.
*/
public boolean hasMoreElements() {
return (q.size() > 0);
}
/**
* Implements Enumeration.nextElement()
* Returns the next EcoEvent in the EventQueue (as a generic Object),
* removing it from the EventQueue. The returned value must be
* cast to an EcoEvent before it is used as one.
*/
public synchronized Object nextElement() {
if (hasMoreElements()) {
Object target = q.firstElement();
q.removeElementAt(0);
return target;
} else {
return null;
}
}
/**
* Returns the next EcoEvent in the EventQueue. This
* saves the user from having to cast the result to
* an EcoEvent.
*/
public EcoEvent nextEvent() {
return (EcoEvent) nextElement();
}
/**
* Returns the next EcoEvent in the EventQueue, without
* removing it.
*/
public EcoEvent check() {
return (EcoEvent) q.firstElement();
}
}