The purpose of this week's lab was to actually make the plan from next week work. The assignment was to implement the Landscape class and then do a social simulation like from Lab 2. Since it was Halloween, I added zombies and dragons as suggested to make a fantasy simulation. I also tried to make pucks collide, but I haven't yet mastered that (they are currently shooting off at weird angles and often speeding up tremendously).
I actually implemented most of Landscape last week. I did, however, make a few changes. First, the private Iterator class is now of generic type so that I can make a random iterator of any of the ArrayLists. Its constructor receives the arrayList, and then makes an arrayList of indices in order, from which it promptly removes them randomly and places them in an array with class access. The next() method uses the array of scrambled indices to return an object from the provided ArrayList. My implementation last week had the next() method remove a random index from an ordered ArrayList. I decided to make this change since it would be necessary if I were to try to implement the remove method. Although, if I were to do that, I would probably make my array an arrayList.
I also implemented the getNeighbors(double x, double y, double vision). The social simulation and fantasy simulation use agents that want to identify neighbors by simply comparing the locations of their centers (x and y values). My collision simulation, however, wants to let pucks collide with large pucks before small ones, so a neighbor needs to be dependent on the radius of the nearby agents as well as the vision of the current one. I fixed this problem by creating an interface (it's always good to remind myself how they can be useful) Measurable. A measurable object must implement getRadius(). In my getNeighbors method, I cycle through all the other Agents and check if the distance from x,y to other.getX(), other.getY() is less than the vision. Each time through, I check to see if other is an instanceof Measurable, and if it is, I temporarily increase vision by other.getRadius().
The Landscape overloads getNeighbors by providing an alternate definition to get the n closest neighbors, where n is an integer. It is easy to get the n closest neighbors if there are only n agents on the Landscape; you simply return all the agents. Otherwise, I have the method create an ArrayList closest to which it adds the first n agents in order of nearness (I use an insertion sort by just putting the first one in the 0th location, and compare all the rest to see where to add them). Then it checks all the rest of the agents by comparing their distances from the given location with the distance of the furthest away in nearest. It does a reverse insertion sort to add any agent that is nearer.
In order to get the distances, there are two methods distance() in Landscape. One accepts two PositionedObjects, and it gets their x and y positions and calls the other, distance(double x1, double y1, double x2, double y2). It just uses the distance formula to return a double.
If SocAgents use this alternate method of getting neighbors (getting their 10 closest neighbors, counting themselves), then they form looser groups, and rarely hold formation of fewer than 10 of the same type.
Implementations of Simulation
This extends Simulation, so all it has is a constructor that supers the grid specifications and additionally recieves parameters for the total number of agents, number of types of SocAgent, and a vision for the agents. It stores these in private variables for use in its initialize method, which is its only method. initialize() is simply a loop that adds the specified number of SocAgents to the landscape in random locations with random types.
The SocAgent extends PositionedObject and implements Agent. It has the required draw methods, which, if the supplied dx and dy are 16, draws a stick figure in a color corrosponding to its type. I acheived this using g.fillPolygon(int xs, int ys, int numPoints). It has a getter and setter for type, and a static method to set the vision since I decided that all SocAgents have comparable eyesight. secondaryUpdate(Landscape scape) simply returns false since SocAgents do not die. The interesting method is primaryUpdate, which uses Landscape.getNeighbors(double x, double y, double vision) to find the nearby agents (I did temporarily use the other neighbor method, as mentioned above, but I usually used the vision method). The neighbor methods return an ArrayList that includes the current SocAgent, so its like counter starts at negative one and when comparing the like counter to the size of the list, the size looked at must be one less than the actual size. If the SocAgent is not happy (at least half of the neighbors are not like SocAgents) then it changes its position by adding a random change to its x and y positions using the Random's nextGaussian() method.
Here are the final states of two SocSims. The first used the vision method of getting neighbors and the second used the closest 10 method. The second is slow when you get lots of agents (200 isn't too bad, but 500 noticably lags). These used parameters 40X40, 200 agents, 4 types, vision of 2.5(only applicable for the first).
You can tell which is which since the pairs of blues on the left would not be happy in a closest simulation, and the outlying pinks, while happy on the right, would not be happy in a vision simulation.
This simulation has ZombieAgents and DragonAgents. ZombieAgents are a special sort of SocAgent (theyextend from it) that can have a type of -1, which indicates that it is a zombie. The FantasySim constructor only takes width, height, and scale, which it sends to Simulation. The number of agents is set in initialize() to have 500 non-zombie ZombieAgents, 50 zombies, and 12 DragonAgents. It puts the correct number of each on the Landscape at random locations.
ZombieAgents, as mentioned above, extend from SocAgent, and therefore have no required methods. It overrides primaryUpdate so that if the type==-1 there is a 25% chance of one of the neighbors being changed into the zombie if the unlucky neighbor is a ZombieAgent (dragons cannot become zombies). The change is achieved by simply setting the type to -1. Then the primaryUpdate for SocSim is called. It also overrides secondaryUpdate since zombies need to be able to be killed. Every ZombieAgent has a private boolean isKilled, which can be set to true using zombie.die(). It also overrides the draw methods so that zombies, if they have type==-1, are drawn gray with both arms extended in the same direction.
DragonAgents extend PositionedObject and implement Agent. primaryUpdate() simply finds the neighbors based on vision and has a 50% chance of killing each zombie (with type==-1). They also move with nextGaussian every turn. Additionally, the draw method makes a small picture of a red dragon if the dx==dy==16.
This shows 2000 simulations, updating every 40 iterations. I'm not sure why the background is black, but I just made the Zombies lightGray instead of darkGray, and everything shows up pretty well. It is a 60X60 grid. The ZombieAgents have a vision of 5 to determine happiness, but a contactDist of .5 for how close a live agent must be to risk being turned into a zombie.
As SocAgents, zombies like to cluster and sit; I guess they are graveyards or something. When a dragon comes in and eats most of them, the remaining one will sometimes run around and infect more -- in this simulation the cluster at the bottom is eaten and the last zombie does convert one other before both are eaten. The small clusters in the top corners are somewhat unusual since they do not get eaten in 200 iterations; usually the dragons are more efficient.
This week didn't require many changes to the structural aspects of the simulation. The method for getting the n closest neighbors was intersting to write, though I probably need to make the general agentList sorted in some way in order to make it more efficient and suitable for large simulations.