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(); } }