initial commit
[applet-bots.git] / src / appletbots / World.java
blobd354ba852ddfa05f0017359b81580aec27e12be4
1 /*
2 * Copyright (c) 2002 Erik Rasmussen - All Rights Reserved
3 */
4 package appletbots;
6 import appletbots.geometry.Point;
7 import appletbots.geometry.Vector;
9 import javax.swing.*;
10 import java.awt.*;
11 import java.util.ArrayList;
12 import java.util.Collections;
13 import java.util.Comparator;
14 import java.util.HashMap;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Map;
19 /**
20 * This class represents a world in which multiple agents can exist and
21 * interact with each other or with objects in the world. The positions and
22 * velocities of objects and agents are kept internally. The positions cannot
23 * be accessed by the agents or the objects themselves. This keeps all agent
24 * algorithms completely vector-based and, therefore, scalable.
26 * @author Erik Rasmussen
28 public class World extends JPanel
30 /**
31 * The maximum number of attempts (1000) that should be made trying to add
32 * an object randomly to the world. This is to prevent infinite loops if
33 * there is no space for the object.
35 protected static final int MAX_ADD_OBJECT_ATTEMPTS = 1000;
36 /**
37 * A hashtable mapping each object to its location and velocity datum
39 protected Map objectsTable = new HashMap();
40 /**
41 * The number of milliseconds to sleep in between time cycles
43 protected int delay = 500;
44 /**
45 * The thread driving the world
47 protected WorldThread thread;
48 /**
49 * The number of time cycles that have passed
51 protected long time = 0;
52 /**
53 * Registered listeners
55 protected List listeners = new ArrayList();
56 /**
57 * The object that is currently selected
59 protected WorldObject selectedObject;
60 /**
61 * The color to paint the object that is selected
63 protected Color selectedObjectColor = Color.yellow;
64 /**
65 * The color in which to paint the "sight circle" of the selected object
67 protected Color selectedSight;
69 /**
70 * Constructs a new world with the given dimensions
72 * @param width The width of the world in pixels
73 * @param height The height of the world in pixels
75 public World(final int width, final int height)
77 setMinimumSize(new Dimension(width, height));
78 setMaximumSize(new Dimension(width, height));
79 setBackground(Color.black);
82 /**
83 * Paints the world (invoked by Swing)
85 * @param g The graphics object on which to paint the world
87 public void paint(final Graphics g)
89 super.paint(g);
90 paintObjects(g);
93 /**
94 * Paints the objects in the world
96 * @param g The graphics object on which to paint the objects
98 protected void paintObjects(final Graphics g)
100 if (selectedSight != null && selectedObject instanceof Agent)
102 g.setColor(selectedSight);
103 final int sight = ((Agent) selectedObject).getSight();
104 final WorldObjectData selectedObjectData = getData(selectedObject);
105 g.drawOval((int) Math.round(selectedObjectData.getLocation().x) - selectedObject.getSize() - sight,
106 (int) Math.round(selectedObjectData.getLocation().y) - selectedObject.getSize() - sight,
107 2 * (selectedObject.getSize() + sight),
108 2 * (selectedObject.getSize() + sight));
110 synchronized (objectsTable)
112 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
114 final WorldObject object = (WorldObject) iterator.next();
115 // don't paint carried objects
116 if (!(object instanceof CarriableObject && ((CarriableObject) object).getCarriedBy() != null))
117 paintObject(object, g);
123 * Paints an individual object in the world
125 * @param object The object to paint
126 * @param g The graphics object on which to paint the object
128 protected void paintObject(final WorldObject object, final Graphics g)
130 final WorldObjectData data = getData(object);
131 if (object.equals(selectedObject))
132 g.setColor(selectedObjectColor);
133 else
134 g.setColor(object.getColor());
136 // paint object
137 g.fillOval((int) Math.round(data.getLocation().x) - object.getSize(),
138 (int) Math.round(data.getLocation().y) - object.getSize(),
139 object.getSize() * 2,
140 object.getSize() * 2);
142 // paint any objects the current object is carrying
143 if (object instanceof CarrierAgent)
145 final CarriableObject[] carriedItems = ((CarrierAgent) object).getInventory();
146 for (int i = 0; i < carriedItems.length; i++)
147 paintObject(carriedItems[i], g);
150 if (object instanceof Agent)
152 final Agent agent = (Agent) object;
153 final VectorToDraw[] vectorsToDraw = agent.getVectorsToDraw();
154 for (int i = 0; i < vectorsToDraw.length; i++)
156 g.setColor(vectorsToDraw[i].color);
157 final Point endPoint = data.getLocation().add(vectorsToDraw[i].vector);
158 g.drawLine((int) Math.round(data.getLocation().x),
159 (int) Math.round(data.getLocation().y),
160 (int) Math.round(endPoint.x),
161 (int) Math.round(endPoint.y));
167 * Adds an object to a random position in the world. The object will have
168 * an initial speed of 0.
170 * @param object The object to add
171 * @throws CollisionException Thrown if the object cannot be added randomly
172 * to the world in MAX_ADD_OBJECT_ATTEMPTS
174 public void addObject(final WorldObject object) throws CollisionException
176 addObject(object, new Vector(0, 0));
180 * Adds an object to a random position in the world with the given initial
181 * velocity
183 * @param object The object to add
184 * @param velocity The initial velocity
185 * @throws CollisionException Thrown if the object cannot be added randomly
186 * to the world in MAX_ADD_OBJECT_ATTEMPTS
188 public void addObject(final WorldObject object, final Vector velocity) throws CollisionException
190 for (int i = 0; i < MAX_ADD_OBJECT_ATTEMPTS; i++)
194 addObject(object, getRandomPoint(object.getSize()), velocity);
195 return;
197 catch (CollisionException e)
200 catch (OutOfThisWorldException e)
204 throw new CollisionException("Tried " + MAX_ADD_OBJECT_ATTEMPTS + " times to add an object randomly and failed.");
208 * Adds an object to the world at the given position with the given initial
209 * velocity
211 * @param object The object to add
212 * @param location The location to add the object
213 * @param velocity The initial velocity
214 * @throws CollisionException Thrown if the location given is at least
215 * partially occupied by another object
216 * @throws OutOfThisWorldException Thrown if the location given will place
217 * at least part of the object outside the
218 * boundaries of the world
220 public void addObject(final WorldObject object, final Point location, final Vector velocity) throws CollisionException, OutOfThisWorldException
222 if (objectsTable.containsKey(object))
223 return; // already have this object
224 if (!inWorld(location, object.getSize()))
225 throw new OutOfThisWorldException();
226 final WorldObjectData data = new WorldObjectData(location, velocity, object);
227 synchronized (objectsTable)
229 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
231 final WorldObject otherObject = (WorldObject) iterator.next();
232 final WorldObjectData otherObjectData = getData(otherObject);
233 final double distance = location.distance(otherObjectData.getLocation());
234 if (distance < object.getSize() + otherObject.getSize())
235 throw new CollisionException(object, otherObject);
237 objectsTable.put(object, data);
238 if (object instanceof Agent)
239 ((Agent) object).setWorld(this);
244 * Returns a random point in the world
246 * @return A random point in the world
248 protected Point getRandomPoint()
250 return getRandomPoint(0);
254 * Returns a random point in the world within a given distance from the
255 * boundaries
257 * @param distance The minimum distance from the boundaries that the point
258 * can be
259 * @return A random point in the world within a given distance from the
260 * boundaries
262 protected Point getRandomPoint(final int distance)
264 return new Point(Math.random() * (getWorldWidth() - distance - distance) + distance,
265 Math.random() * (getWorldHeight() - distance - distance) + distance);
269 * Removes an object from the world
271 * @param object The object to remove
273 public void removeObject(final WorldObject object)
275 synchronized (objectsTable)
277 objectsTable.remove(object);
282 * Returns the location and velocity data for the given object
284 * @param object The object to get the data for
285 * @return The location and velocity data for the given object
287 protected WorldObjectData getData(final WorldObject object)
289 if (object instanceof CarriableObject && ((CarriableObject) object).getCarriedBy() != null)
290 return getData(((CarriableObject) object).getCarriedBy());
291 else
292 return (WorldObjectData) objectsTable.get(object);
296 * Returns the velocity for the given object
298 * @param object The object to get the velocity of
299 * @return The velocity for the given object
301 public Vector getVelocity(final WorldObject object)
303 return getData(object).getVelocity();
307 * Moves an object
309 * @param object The object to move
311 private void moveObject(final WorldObject object)
313 final WorldObjectData data = getData(object);
315 // don't move carried objects
316 if (object instanceof CarriableObject && ((CarriableObject) object).getCarriedBy() != null)
317 return;
319 if (object instanceof Agent)
321 final Agent agent = (Agent) object;
322 data.setVelocity(data.getVelocity().add(agent.getAcceleration()));
324 if (data.getVelocity().getLength() == 0)
325 return;
326 final Point newLocation = data.getLocation().add(data.getVelocity());
327 // check that new location is in the world
328 if (!inWorld(newLocation, object.getSize()))
330 bounceOffWall(object);
331 return;
333 else
335 // check for collisions
336 boolean collision = false;
337 final WorldObject aObject = object;
338 final WorldObjectData a = data;
339 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
341 final WorldObject bObject = (WorldObject) iterator.next();
342 final WorldObjectData b = getData(bObject);
343 // Define C as the vector from the center of A to the center of B
344 final Vector c = new Vector(a.getLocation(), b.getLocation());
345 final double lengthOfC = c.getLength();
347 // Make sure A is going to travel far enough to hit B
348 final double aVelocityLength = a.getVelocity().getLength();
349 final double radiiSum = aObject.getSize() + bObject.getSize();
350 if (aVelocityLength < lengthOfC - radiiSum)
351 continue;
352 // Make sure A is going towards B
353 if (a.getVelocity().dotProduct(c) <= 0)
354 continue;
355 // Normalize A's velocity vector and call it N
356 final Vector n = a.getVelocity();
357 n.normalize();
358 // Get dot product of N and C. This is the point along A's path
359 // where it will be closest to B. We'll call it D.
360 final double d = n.dotProduct(c);
361 // Use the pythagorean theorem to get the value for the square
362 // of the closest distance on V to B, and call it F.
363 // (ie. the shortest distance from V to B is sqrt(F)
364 final double f = lengthOfC * lengthOfC - d * d;
365 // Define T such that "the longest distance A can travel without
366 // hitting B" = D - sqrt(T). Again by the pythagorean theorem
367 final double radiiSumSquared = radiiSum * radiiSum;
368 final double t = radiiSumSquared - f;
369 // Check if closest point is within the two radii of B. To save
370 // time, rather than taking the sqrt of F, we'll just square the
371 // other side of the equation
372 if (t < 0)
373 continue;
374 // if A won't travel far enough to collide with B, there's no
375 // collision
376 final double distanceToCollision = d - Math.sqrt(t);
377 if (aVelocityLength < distanceToCollision)
378 continue;
379 collision = true;
380 collide(aObject, bObject);
382 if (!collision)
383 data.setLocation(newLocation);
388 * Called by the WorldThread. This method does three things: it allows all
389 * the agents to observe the world, moves all the objects, and repaints the
390 * world.
392 public void incrementTime()
394 time++;
395 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
397 final WorldObject object = (WorldObject) iterator.next();
398 if (object instanceof Agent)
399 ((Agent) object).observeWorld();
400 moveObject(object);
401 // notify listeners
402 for (int i = 0; i < listeners.size(); i++)
403 ((WorldListener) listeners.get(i)).timeIncremented(time);
405 repaint();
409 * Returns the number of milliseconds to wait between time cycles
411 * @return The number of milliseconds to wait between time cycles
413 public int getDelay()
415 return delay;
419 * Sets the number of milliseconds to wait between time cycles
421 * @param delay The number of milliseconds to wait between time cycles
423 public void setDelay(final int delay)
425 this.delay = delay;
429 * Starts the world thread
431 public void startThread()
433 if (thread == null)
435 thread = new WorldThread(this);
436 thread.start();
437 // notify listeners
438 for (int i = 0; i < listeners.size(); i++)
439 ((WorldListener) listeners.get(i)).threadStarted();
444 * Stops the world thread
446 public void stopThread()
448 if (thread != null)
450 thread.setRunning(false);
451 thread = null;
452 // notify listeners
453 for (int i = 0; i < listeners.size(); i++)
454 ((WorldListener) listeners.get(i)).threadStopped();
459 * Returns whether or not the world thread is running
461 * @return Whether or not the world thread is running
463 public boolean isRunning()
465 return thread != null && thread.getRunning();
469 * Returns all the objects that can be seen by the given agent, normally
470 * called by the given agent himself.
471 * <br><br>
472 * THIS METHOD WILL NOT RETURN OTHER AGENTS! To get seen agents, use
473 * getNeighbors().
475 * @param agent The agent for which to get the seen objects
476 * @return All the objects the given agent can see
478 public List getSeenObjects(final Agent agent)
480 final List seenObjects = new ArrayList();
481 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
483 final WorldObject object = (WorldObject) iterator.next();
484 if (!(object instanceof Agent) && getDistanceBetweenObjects(agent, object) <= agent.getSight())
485 seenObjects.add(object);
487 return seenObjects;
491 * Returns all the other agents that can be seen by the given agent,
492 * normally called by the given agent himself.
494 * @param agent The agent for which to return the seen neighbors
495 * @return All the agents that the given agent can see
497 public List getNeighbors(final Agent agent)
499 final List neighbors = new ArrayList();
500 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
502 final WorldObject object = (WorldObject) iterator.next();
503 if (object instanceof Agent && !object.equals(agent) && getDistanceBetweenObjects(agent, object) <= agent.getSight())
504 neighbors.add(object);
506 return neighbors;
510 * Returns a vector from the first object to the second object. This is
511 * useful when an agent wants to move towards an object. The returned
512 * vector is the direction in which the agent should move.
513 * <br><br>
514 * Note: The vector returned is not from the center of the first object to the
515 * center of the second object! The vector returned is the vector that, if added
516 * to the first object's location, would place the object so that it was touching
517 * the second object. In other words, it's the vector from the center of the first
518 * object to the center of the object <b>minus</b> the radii of the both objects.
520 * @param a The first object
521 * @param b The second object
522 * @return A vector from the first object to the second object
524 public Vector getVectorToObject(final WorldObject a, final WorldObject b)
526 Vector v = new Vector(getData(a).getLocation(), getData(b).getLocation());
527 v = v.setLength(v.getLength() - a.getSize() - b.getSize());
528 return v;
532 * Returns the distance between the two objects
534 * @param a An object
535 * @param b Another object
536 * @return The distance between the two objects
538 protected double getDistanceBetweenObjects(final WorldObject a, final WorldObject b)
540 return getVectorToObject(a, b).getLength();
544 * Returns the width (in pixels) of the world
546 * @return The width (in pixels) of the world
548 public int getWorldWidth()
550 return getMinimumSize().width;
554 * Returns the height (in pixels) of the world
556 * @return The height (in pixels) of the world
558 public int getWorldHeight()
560 return getMinimumSize().height;
564 * Returns the number of elapsed time cycles
566 * @return The number of elapsed time cycles
568 public long getTime()
570 return time;
574 * Imports all the objects from another world into this one
576 * @param world The world to import the objects from
578 public void importObjects(final World world)
580 objectsTable = world.objectsTable;
581 delay = world.delay;
582 selectedObject = null;
583 selectedObjectColor = null;
584 thread = null;
585 time = 0;
589 * Returns whether or not an object with the given size at the given
590 * location would be complete inside the boundaries of the world
592 * @param location The location of the hypothetical object
593 * @param size The size of the hypothetical object
594 * @return Whether or not an object with the given size at the given
595 * location would be complete inside the boundaries of the world
597 protected boolean inWorld(final Point location, final int size)
599 return location.x >= size &&
600 location.x < getWorldWidth() - size &&
601 location.y >= size &&
602 location.y < getWorldHeight() - size;
606 * Collides two objects. Using the objects' masses, locations, and
607 * velocities it calculates the objects' velocities after a perfectly
608 * elastic collision (i.e. no momentum or kinetic energy is lost).
610 * @param a An object
611 * @param b Another object
613 private void collide(final WorldObject a, final WorldObject b)
615 final WorldObjectData aData = getData(a);
616 final WorldObjectData bData = getData(b);
618 // First, find the normalized vector n from the center of
619 // A to the center of B
620 final Vector n = new Vector(aData.getLocation(), bData.getLocation());
621 n.normalize();
623 // Find the length of the component of each of the velocities along n.
624 // a1 = v1 . n
625 // a2 = v2 . n
626 final double a1 = aData.getVelocity().dotProduct(n);
627 final double a2 = bData.getVelocity().dotProduct(n);
629 // Using the optimized version,
630 // optimizedP = Ê2(a1 - a2)
631 // ÊÊÊÊÊÊÊÊÊÊÊÊÊ-----------
632 // ÊÊÊÊÊÊÊÊÊÊÊÊÊÊÊm1 + m2
633 final double optimizedP = (2.0 * (a1 - a2)) / (a.getMass() + b.getMass());
635 // Calculate final velocity for A
636 // va' = va - optimizedP * mb * n
637 aData.setVelocity(aData.getVelocity().subtract(n.multiply(optimizedP * b.getMass())));
639 // Calculate final velocity for B
640 // vb' = vb + optimizedP * ma * n
641 bData.setVelocity(bData.getVelocity().add(n.multiply(optimizedP * a.getMass())));
643 // Notify objects of collision
644 a.collidedWith(b);
648 * Bounces the object off the wall. Performs a calculation based on the
649 * object's location and velocity and modifies the object's velocity
650 * appropriately. The collision is perfectly elastic (i.e. no momentum or
651 * kinetic energy is lost)
653 * @param object The object to bounce
655 private void bounceOffWall(final WorldObject object)
657 final WorldObjectData data = getData(object);
658 final Point newLocation = data.getLocation().add(data.getVelocity());
660 // change velocity depending on which wall we've hit
661 if (newLocation.x < object.getSize() || newLocation.x > getWorldWidth() - object.getSize())
663 final Vector velocity = data.getVelocity();
664 velocity.x *= -1;
665 data.setVelocity(velocity);
667 if (newLocation.y < object.getSize() || newLocation.y > getWorldHeight() - object.getSize())
669 final Vector velocity = data.getVelocity();
670 velocity.y *= -1;
671 data.setVelocity(velocity);
676 * Returns the object at the given x and y coordinates. Used for user
677 * interaction and mouse-clicks.
679 * @param x The x coordinate
680 * @param y The y coordinate
681 * @return The object at the given location
683 protected WorldObject getObjectAt(final int x, final int y)
685 final Point point = new Point(x, y);
686 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
688 final WorldObject object = (WorldObject) iterator.next();
689 final WorldObjectData data = getData(object);
690 if (data.getLocation().distance(point) <= object.getSize())
691 return object;
693 return null;
697 * Selects the object at the given location
699 * @param x The x coordinate
700 * @param y The y coordinate
702 public void selectObjectAt(final int x, final int y)
704 selectObject(getObjectAt(x, y));
708 * Selects the given object
710 * @param object The object to select
712 public void selectObject(final WorldObject object)
714 selectedObject = object;
715 repaint();
719 * Sets the color to paint the selected object
721 * @param selectedObjectColor The color to paint the selected object
723 public void setSelectedObjectColor(final Color selectedObjectColor)
725 this.selectedObjectColor = selectedObjectColor;
729 * Sets the color with which to paint the selected object's "sight circle"
731 * @param selectedSight The color with which to paint the selected object's
732 * "sight circle"
734 public void setSelectedSight(final Color selectedSight)
736 this.selectedSight = selectedSight;
740 * Drops an item from a CarrierAgent
742 * @param agent The agent dropping the item
743 * @param item The object beind dropped
744 * @throws CollisionException Thrown if the item cannot be placed within the "pickup distance"
746 public void dropItem(final CarrierAgent agent, final CarriableObject item) throws CollisionException
748 int collisions = 0;
749 final WorldObjectData agentData = getData(agent);
750 Vector dropVector = agentData.getVelocity();
751 dropVector = dropVector.setLength(agent.getSize() + item.getSize());
752 synchronized (objectsTable)
754 while (true)
756 final Point dropPoint = agentData.getLocation().add(dropVector);
757 boolean collided = false;
758 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
760 final WorldObject otherObject = (WorldObject) iterator.next();
761 final WorldObjectData otherObjectData = getData(otherObject);
762 final double distance = dropPoint.distance(otherObjectData.getLocation());
763 if (distance < item.getSize() + otherObject.getSize() || !inWorld(dropPoint, item.getSize()))
765 collisions++;
766 collided = true;
767 if (collisions > MAX_ADD_OBJECT_ATTEMPTS)
768 throw new CollisionException(item, otherObject);
771 if (!collided)
773 final WorldObjectData itemData = getData(item);
774 itemData.setLocation(dropPoint);
775 itemData.setVelocity(agentData.getVelocity());
776 return;
778 else
779 dropVector = Vector.getRandom(agent.getSize() + item.getSize());
785 * Registers a listener for this world
787 public void addListener(final WorldListener listener)
789 listeners.add(listener);
794 * Returns the closest object of the given type to the given agent that
795 * is seen by the agent
797 * @param agent The agent to get an object near
798 * @param type The type of objects to search for
799 * @return The closest seen object of the given type to the given agent
801 public WorldObject getClosestObjectOfType(final Agent agent, final Class type)
803 final List objectsOfCorrectType = new ArrayList();
804 for (Iterator iterator = objectsTable.keySet().iterator(); iterator.hasNext();)
806 final WorldObject object = (WorldObject) iterator.next();
807 if (type.isInstance(object) && getDistanceBetweenObjects(agent, object) <= agent.getSight())
808 objectsOfCorrectType.add(object);
810 if (objectsOfCorrectType.isEmpty())
811 return null;
812 else
814 Collections.sort(objectsOfCorrectType, objectDistanceComparator(agent));
815 return (WorldObject) objectsOfCorrectType.iterator().next();
820 * Returns a comparator to sort other objects by their distance to the given object
822 * @param object The object to compare distances to
823 * @return A comparator to sort other objects by their distance to the given object
825 public Comparator objectDistanceComparator(final WorldObject object)
827 return new Comparator()
829 public int compare(final Object o1, final Object o2)
831 final Vector v1 = getVectorToObject(object, (WorldObject) o1);
832 final Vector v2 = getVectorToObject(object, (WorldObject) o2);
833 return v1.compareTo(v2);