SimStation exemplifies the multi-agent
or agent-based architecture. It is a framework
for creating agent-based simulations. A typical simulation might involve a
population of agents (birds, ants, shoppers, cars, etc.) moving around an
environment and interacting with random neighbors (fighting, bargaining,
mating, etc.)
For example, the following screenshot shows a flock of birds (the red dots). Initially the birds are flying in random directions and at random speeds. A bird interacts with another bird by copying its speed and direction. Eventually the entire flock is flying in the same direction and at the same speed. (Flocking behavior is an example of an emergent phenomenon. No single bird is directing the flock. Instead, flocking emerges from thousands of simpler interactions between pairs of birds. What are some examples of flocking behavior among humans?)
Notice that Sim Station provides
buttons to start, suspend, resume, and stop the simulation. These same commands
also appear on the Edit menu.
The Stats button displays a dialog
box containing the time, number of active agents, and total number of agents
(birds):
The File and Help menu contain
their usual commands.
SimStation is built on top of the mvc framework. (So a framework on top of a framework!) Plague, greed, flocking, random walks, and prisoners dilemma are SimStation customizations.
Here's the business layer for SimStation (not all methods shown):
Notes
· World is the base class for all agent environments. It provides a list of agents. It's startAgents, stopAgents, pauseAgents, and resumeAgents methods call the corresponding start, stop, suspend, and resume methods of its agents.
· Populate is an abstract method that will be specified in subclasses. It's called by startAgents and populates the simulation.
· An agent is an active object. It runs in its own thread (myThread).
· The run method repeatedly calls the abstract update method.
· An agent has a location in the world (0 <= xc, yc <= World.SIZE) and a name (agentName).
· As in Agent Lab, two flags are used to suspend and stop agents.
· A mobile agent has a heading (N, E, S, W) that can be changed using its turn method.
· A mobile agent's location can be changed by calling its move method. (Note that an agent wraps around the world if it's location goes beyond the border.
· The world has a special agent called observer of type ObserverAgent. It's update method calls the world's updateStatistics method. The default implementation of this method increments the world clock and the alive attribute, which is the number of agents that are still active.
The presentation layer is a straightforward mvc customization:
The simulation view's paintComponent method calls drawAgent(a, gc) for each agent a. The default implementation simply draws a diameter 10 red filled oval at the a's location, but this can easily be overridden in a subclass.
WorldPanel creates a thread control subpanel (threadPanel) and adds it to the northern region of the control panel (which is an AppPanel field.) Subclasses can add additional controls to the other regions pof the control panel.
Of course there's a command hierarchy:
· At strategic places Agent.run calls empty methods: onStart, onInterrupted, and OnExit. These can be overridden in subclasses if needed.
·
Move should call world.changed. In my version an agent moves one step
at a time for n steps, calling world.changed
after each step. We can also call world.changed(name, oldPoint, newPoint) where oldPoint is the former location of the agent and newPoint is the new location. In this case the view's propertyChanged method calls repaint(x,
y, h, w) where (x, y) is the upper corner of a rectangle of height h and width
w that contains oldPoint and newPoint.
This version of repaint only paints the "dirty" part of the
component. (It didn't seem to improve performance that much, though.)
· I found that a 20 msec sleep time gave the smoothest graphics.
· The main service the simulation provides to agents is getNeighbor. An agent seeking a random nearby partner to interact with might call:
Agent partner = world.getNeighbor(this, 10); // try to find a
random agent (not me) within 10 steps
if (partner != null) { /* interact with partner */ }
· An efficient implementation of getNeighbor picks a random location in the agents list. Starting at this location it visits each agent in order (wrapping around to the start if necessary) until it either finds a suitable neighbor or until it loops back to the starting point and returns null.
· There needs to be some restriction on when a simulation gets saved to a file. Threads are not serializable, so the myThread field in the Agent class needs to be declared transient:
transient protected Thread myThread;
This means that an agent’s thread won’t be written to a file when the agent is. So when a simulation and its agents are read back from a file, the threads won’t be there. This will be a problem if the agents were in their running or suspended state when they were saved. A simple fix is to insist that the agents be in their ready state (i.e., they have not been started) if and when they are saved.
With this assumption in place you can override the setModel method inherited from AppPanel in WorldPanel. Recall that this method is called when the model changes (which happens if the user selects New or Open from the Filer menu.)
In Random Walk agents are drunks. They update themselves by selecting a random heading and random number of steps (less than some global speed, maybe 10), then they call the move function. (Drunk walks were used to determine the probability that setting off the first A-bomb might cause a chain reaction in the world's atmosphere!)
Visually, Random Walks is not so interesting. We see the drunks as red dots moving in a jittery fashion.
Here's the design:
Here's my implementation, which can be used to test SimStation: RandomWalk.java.
In Flocking agents are birds. Each bird has a heading and a speed. These are initially random. A bird updates itself by asking its world for a random nearby neighbor. The bird copies the speed and heading of the neighbor, then calls its move function with steps = speed. (You can experiment with different values for radius.) After many cycles you will see all of the birds moving in the same direction.
Here's the design:
The plague simulator models the spread of a virus through a population. Red agents (called hosts) are infected and green ones aren't:
The Stats box includes the percentage of infected agents:
Constants in the simulation allow users to experiment with different types of viruses:
public class PlagueSimulation extends Simulation {
public
static int VIRULENCE = 50; // % chance of infection
public
static int RESISTANCE = 2; // % chance of resisting infection
// etc.
}
In Prisoner's Dilemma Tournament agents are prisoners. Each prisoner has a
fitness score, which is initially zero. A prisoner updates itself by playing a
game of prisoner's dilemma with a random neighbor. In this game each prisoner
decides to cooperate or cheat:
class
Prisoner extends MobileAgent {
private int fitness = 0;
private boolean partnerCheated = false;
public boolean cooperate()
{ ??? }
// etc.
}
Fitness scores are updated according to the following table:
Prisoner 2 |
|||
cooperate |
cheat |
||
Prisoner 1 |
cooperate |
3, 3 |
0, 5 |
cheat |
5, 0 |
1, 1 |
In other words, if Prisoner 1 decides to cheat but
Prisoner 2 cooperates, then Prisoner 1's fitness is incremented by 5 points,
while Prisoner 2's fitness remains the same.
The cooperate method uses the Strategy pattern. There are currently four strategies available: always cheat, always cooperate, randomly cooperate, cooperate only if the last opponent cooperated (tit-for-tat sort of). More interesting and involved strategies are possible. Initially, one quarter of the agents should be endowed with each strategy. So 10 cheaters, 10 cooperators, etc.
Here's my design:
The statistics box should show the average fitness for each strategy.
Prisoner's Dilemma Tournaments are used to study the evolution of altruistic behavior in social networks.
Sliders fire change events instead of action events and therefore require different types of listeners. Here’s an explanation: Handling Change Events.
In Greed the world is a meadow populated by mobile agents called cows and stationary agents called patches (of grass). Cows and patches have an integer energy attribute (0 <= energy <= 100). Every cow stands on a patch. A cow needs to eat the grass (energy) of its patch to maintain its energy. If its energy ever reaches 0, it dies (and turns white and stops moving).
Eating its grass reduces the energy of a patch. Fortunately, the grass will grow back at a rate determined by the user.
The amount of energy (grass) a cow eats is determined by its greediness, which is also set by the user. If a cow's patch w does not yet have sufficient energy, the cow randomly moves to another patch, if it has enough energy to move. This is called move-energy and is also set by the user.
Otherwise, the cow must wait for the grass to grow back. As a cow waits, its energy slowly diminishes. The cow may share its patch with other waiting cows, so it can be a long wait and the cow may eventually starve to death. (Cows that share a patch have to take turns eating.)
For a given grow-back rate and a given move-energy cost, what is the maximum greediness the cows can maintain without becoming extinct?
Complete the implementation of SimStation.
Implement Plague as a SimStation customization.
Implement Prisoners Dilemma Tournament as a SimStation customization.
Implement Greed as a SimStation customization.