import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ImageObserver;
import java.awt.image.PixelGrabber;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.PropertyResourceBundle;
import java.util.Random;
import java.util.Vector;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
/**
* Evolution game
* @author Rick Townsend
*
*/
public class Evolution extends JFrame {
// constants
/** Serial version (just a GUID, really) */
private static final long serialVersionUID = -6397694215307966956L;
/** height of sprites */
private static final int spriteHeight = 8;
/** width of sprites */
private static final int spriteWidth = 10;
/** default background colour */
private static final int background = 0xFF101018;
/** height of text lines */
private static final int lineHeight = 15;
/** default zoom level for sprites on Stats screen */
private static int spriteStatsZoom = 2;
/** left offset of text lines */
private static final int textOffset = 10 + (spriteWidth * spriteStatsZoom) + 10;
// variables
/** game title */
public static String title = "EVOLUTION";
/** path to species definitions, using ResourceBundle naming (ie: "species\hunters\canines\" would be "species.hunters.canines.") */
public static String speciesPath = "species.";
/** path to hazards initialization file, which defines what hazards should be loaded where */
public static String hazardsFile = "hazards.ini";
/** path and filename of background image, using ResourceBundle naming (eg: "images\background\my.png" would be "images.background.my.jpg") */
public static String backgroundFile = "bg.jpg";
/** game width */
public static int width = 640;
/** game height (not height of the window!) */
public static int height = 480;
/** offset for size of window titlebar */
public static int titlebar = 29;
/** seed to use for initializing randomizer */
public static int gameSeed = (int)(Math.random() * Integer.MAX_VALUE);
/** random number generator (will be seeded with gameSeed) */
public static Random randomGen;
/** list of species to load in the initial spawn */
public static String initialSpeciesLoad = "rabbit:100,chicken:100,wolf:40";
/** list of hazards to load in the initial spawn (e.g.: Mud:50)*/
public static String initialHazardsLoad = "";
/** starting life for initial set of animals */
public static int baseLife = 300;
/** limit for the movement speed of animals */
public static int speedLimit = 20;
/** limit for the sight of animals */
public static int sightLimit = 100;
/** limit for the number of children that can be spawned at once */
public static int spawnChildrenLimit = 20;
/** initial number of plants to spawn */
public static int initialPlants = 40;
/** rate at which plants spawn */
public static int plantSpawnRate = 8;
/** range at which animals can kill each other */
public static int killRange = 5;
/** cost of living one turn */
public static double livingCost = 1.0;
/** cost of moving one pixel */
public static double movingCost = 0.5;
/** allow standoffs when both friends and enemies are greater than 3? */
public static boolean allowStandoffs = false;
/** interval that the main thread sleeps for during iterations */
public static int sleepTime = 10;
/** number of loops between reporting intervals (0 or negative means no reporting, 1 is continuous reporting) */
public static int reportCycles = 100;
/** whether the app should run in headless mode (can also be set by adding "headless" as a commandline switch) */
public static boolean headless = false;
/** tournament mode flag, indicates that the tournament settings should be honoured */
public static boolean tournamentMode = false;
/** tournament mode, stop when only one species remains (regardless of how it may have evolved.) */
public static boolean tournamentOneWinner = true;
/** tournament mode, stop when a specific number of cycles have passed */
public static long tournamentCycles = -1;
/** tournament mode, stop when a given amount of time (in milliseconds) has passed */
public static long tournamentTimeout = -1;
/** tournament mode, whether to take a screenshot when the tournament ends */
public static boolean tournamentFinalScreenshot = false;
/** limit to how fast plants can grow or die */
public static double growthLimit = 5;
/** starting life for all plants */
public static int basePlantLife = 100;
/** limit to the number of animals */
public static int animalLimit = Integer.MAX_VALUE;
/** directory in which to put screenshots */
public static String screenshotDirectory = "screenshots";
/** iteration to automatically take screenshots */
public static int screenshotInterval = 0;
/** counter for screenshots */
public static long lastScreenshotNum = 0;
/** image type for screenshots */
public static String screenshotType = "jpg";
/** image compression ratio for screenshots */
public static float screenshotCompressionRatio = 0.25f;
/** flag to indicate if Life-based kill rules should be applied */
public static boolean killRulesLifeBased = false;
/** threshold which, when passed by difference between enemy and friend Life totals, indicates the animal is killed */
public static int killRulesLifeThreshold = 500;
/** array of keys (to detect which ones are currently being held */
private static boolean[] keys = new boolean[65536];
/** mouse flags: X Coord, Y Coord,
* and what mouse button is currently being held */
private static int xMouse, yMouse, mouseButton;
/** Bundles (for loading species and initial environment specifications) */
private static Hashtable<String,PropertyResourceBundle> bundles = new Hashtable<String,PropertyResourceBundle>();
/** Handle to the single object instance */
private static Evolution frame;
// Graphics objects
/** main buffer */
private static BufferedImage buff;
/** stats buffer */
private static BufferedImage buffStats;
/** graphics holder */
private static Graphics gr;
/** when the screen was last painted */
private static long lastPaintTime = 0;
/** current frame number */
private static long currentFrame = 0;
/** pointer to the last frame used in a screenshot */
private static long lastScreenshotFrame = 0;
// Lists of objects
/** List of all active animals */
private static ArrayList<Animal> animals = new ArrayList<Animal>();
/** List of all active plants */
private static ArrayList<Plant> plants = new ArrayList<Plant>();
/** List of all active non-life objects */
private static ArrayList<Hazard> hazards = new ArrayList<Hazard>();
/** List of all animals the user left-clicked on */
private static ArrayList<Drawable> clicked = null;
/** List of all plants dropped during this iteration */
private static Vector<Plant> droppedPlants = new Vector<Plant>();
/** List of all hazards dropped during this iteration */
private static Vector<Hazard> droppedHazards = new Vector<Hazard>();
/** Statistics */
private static Hashtable<String, Hashtable<StatisticType, Integer>> speciesStats = new Hashtable<String, Hashtable<StatisticType, Integer>>();
/** Cached list of colours */
private static Hashtable<String, String> colours;
/** Cached list of sprites for NonLife objects */
private static Hashtable<HazardType,int[]> hazardSprites = new Hashtable<HazardType,int[]>();
// Pixel arrays
/** array of the screen pixels (ie: the game board) */
private static int[] pixels;
/** array of the pixels used as a background */
private static int[] backgroundPixels;
/** array of the pixels loaded as the original background */
private static int[] backgroundImagePixels;
// Flags
/** flag to indicate if the game is currently paused */
private static boolean paused = false;
/** flag to indicate if the escape button has been pushed (ie: exit flag) */
private static boolean escape = false;
/** flag to indicate if the screen should be painted,
* no reason to paint the screen if nothing is changing */
private static boolean doPaint = true;
/** flag to indicate if the details of the animals the user
* clicked-on should be displayed to the screen; if false then
* the game board should be displayed. */
private static boolean showClicked = false;
/** flag to indicate if Animal Details screen should move up one */
private static boolean moveUp = false;
/** flag to indicate if Animal Details screen should move down one */
private static boolean moveDown = false;
/** flag to indicate if the sprites should be wiped (undrawn) when they move or die */
private static boolean doWipe = true;
/** flag to indicate if the frame was resized since the last draw */
private static boolean doResize = false;
/** flag to indicate if the currently selected object should be killed */
private static boolean killCurrent = false;
/** List of all possible Animal actions */
private static enum Action {
Stop, /** position won't change (same as speed=0) */
Forward, /** no change */
Reverse, /** direction is reversed (direction + 180 degrees) */
Left, /** direction is rotated left (direction + 90 degrees) */
Right, /** direction is rotated right (direction - 90 degrees) */
North, /** direction is set to up */
NorthEast, /** direction is set to up-right */
East, /** direction is set to right */
SouthEast, /** direction is set to down-right */
South, /** direction is set to down */
SouthWest, /** direction is set to down-left */
West, /** direction is set to left */
NorthWest, /** direction is set to up-left */
FollowFriend, /** direction is set equal to a visible friend */
FollowEnemy, /** direction is set towards a visible enemy */
FollowFood, /** direction is set towards visible food */
FollowMouse, /** direction is set towards the mouse */
FollowHazard, /** direction is set towards a visible hazard */
FleeFriend, /** direction is set opposite that of a visible friend */
FleeEnemy, /** direction is set away from a visible enemy */
FleeFood, /** direction is set away from visible food */
FleeMouse, /** direction is set away from the mouse */
FleeHazard, /** direction is set away from a visible hazard */
Random /** direction is chosen randomly */
}
/** List of all possible reaction combinations (AnimalsXY: X = Friends, Y = Enemies */
private static enum ReactionEvent {
Animals00, /** 0 friends, 0 enemies */
Animals10, /** 1 friend, 0 enemies */
Animals20, /** 2 friends, 0 enemies */
Animals30, /** 3 friends, 0 enemies */
Animals01, /** 0 friends, 0 enemies */
Animals11, /** 1 friend, 0 enemies */
Animals21, /** 2 friends, 0 enemies */
Animals31, /** 3 friends, 0 enemies */
Animals02, /** 0 friends, 0 enemies */
Animals12, /** 1 friend, 0 enemies */
Animals22, /** 2 friends, 0 enemies */
Animals32, /** 3 friends, 0 enemies */
Animals03, /** 0 friends, 0 enemies */
Animals13, /** 1 friend, 0 enemies */
Animals23, /** 2 friends, 0 enemies */
Animals33, /** 3 friends, 0 enemies */
}
/** List of all available hazard types */
private static enum HazardType {
Fire,
Fog,
Glue,
Hole,
Ice,
Mud,
Paint,
Teleporter,
Wall
}
/** List of all available statistic types */
private static enum StatisticType {
SpeciesCount
}
/**
* Inner class for all drawable items to derive from.
* @author Townsend
*
*/
private abstract class Drawable {
/** A unique-enough id */
public int id = randomGen.nextInt();
/** Item's X Coordinate */
public double positionX = 0.0;
/** Item's Y Coordinate */
public double positionY = 0.0;
/** Item's sprite as an array of pixels */
public int [] sprite = new int[spriteHeight * spriteWidth];
/**
* Getter for the sprite (can be overridden)
* @return int array of the pixels for this sprite
*/
public int[] getSprite() {
return sprite;
}
/**
* Gets the sprite, magnified by magnificationFactor
* @param magnificationFactor Amount by which the sprite should be magnified
* @return int array of the pixels for this sprite, magnified
*/
public int[] getSpriteZoomed(int magnificationFactor) {
return this.getSpriteZoomed(this.sprite, magnificationFactor);
}
/**
* Gets the sprite, magnified by magnificationFactor
* @param sprite Sprite to be magnified
* @param magnificationFactor Amount by which the sprite should be magnified
* @return int array of the pixels for that sprite, magnified
*/
public int[] getSpriteZoomed(int[] sprite, int magnificationFactor) {
// sanity check - can't zoom out, only in
if (magnificationFactor < 1) return sprite;
int[] zoomedSprite = new int[spriteHeight * magnificationFactor * spriteWidth * magnificationFactor];
for (int i = 0; i < spriteHeight; i++) {
for (int j = 0; j < spriteWidth; j++){
for (int m = i * magnificationFactor; m < (i * magnificationFactor) + magnificationFactor; m++){
for (int n = j * magnificationFactor; n < (j * magnificationFactor) + magnificationFactor; n++){
zoomedSprite[m * (spriteWidth * magnificationFactor) + n] = sprite[i * spriteWidth + j];
}
}
}
}
return zoomedSprite;
}
/**
* Setter for the sprite (can be overridden)
* @param sprite int array of the pixels for this sprite
*/
public void setSprite(int[] sprite) {
this.sprite = sprite;
}
/**
* Overload of wipeSprite, using the default pixel arrays.
*/
public void wipeSprite(){
this.wipeSprite(pixels, backgroundPixels);
}
/**
* Wipe this sprite from the screen. Basically an "undraw"
* method, which sets the pixels this sprite occupied back
* to the default background colour.
*/
public void wipeSprite(int[] setImage, int[] fromImage){
for (int i = 0; i < spriteHeight && doWipe; i++) {
for (int j = 0; j < spriteWidth; j++){
if (this.sprite[i * spriteWidth + j] != background){
int index = (width * (this.getPositionY()+i)) + (this.getPositionX()+j);
setImage[index] = fromImage[index];
}
}
}
}
/**
* Overload for drawSprite, using the current position and default pixel array
*/
public void drawSprite() {
drawSprite(pixels);
}
/**
* Overload for drawSprite, using the current position
* @param image
*/
public void drawSprite(int[] image){
drawSprite(getPositionX(), getPositionY(), image);
}
/**
* Draw the sprite at the specified position. Only draws non-background
* pixels, which allows for sprites to overlap (and saves a bit of work.)
* @param x X Coordinate
* @param y Y Coordinate
*/
public void drawSprite(int x, int y, int[] image){
for (int i=0; i<spriteHeight; i++) {
for (int j=0; j<spriteWidth; j++){
// if this isn't a background pixel, draw it
if (this.sprite[i * spriteWidth + j] != background){
image[(width * (y+i)) + (x+j)] = this.sprite[i * spriteWidth + j];
}
}
}
}
/**
* INT safe get X coordinate
* @return int X position
*/
public int getPositionX() {
return (int)positionX;
}
/**
* INT safe set X coordinate
* @param positionX
*/
public void setPositionX(int positionX) {
this.positionX = positionX;
wrapPosition();
}
/**
* INT safe get Y coordinate
* @return int Y position
*/
public int getPositionY() {
return (int) positionY;
}
/**
* INT safe set Y coordinate
* @param positionY
*/
public void setPositionY(int positionY) {
this.positionY = positionY;
wrapPosition();
}
/**
* Wrap the X/Y coordinates so that they are within the boundaries of the game window.
*/
public void wrapPosition(){
positionX = (positionX + (width - spriteWidth)) % (width - spriteWidth);
positionY = (positionY + (height - spriteHeight)) % (height - spriteHeight);
}
/**
* Wipe the sprite. This method should usually be overridden.
*/
public void die(){
this.wipeSprite();
System.out.println("TODO - implement the DIE method for your subclass.");
}
}
/**
* Class for an animal
* @author Townsend
* @see Drawable
*/
private class Animal extends Drawable {
/** Descriptive Species Name of the animal. */
public String species = "Default";
/** Bundle used to load the animal's attributes. */
public String bundle = "";
/** Direction in radians. Default is a random direction. */
public double direction = 2.0 * Math.PI * randomGen.nextDouble();
/** Changed direction, in radians */
public Double directionAltered;
/** Units of Life for the animal. Default is the game baseLife value plus a random value between 0 and 50. */
public int life = randomGen.nextInt(50) + baseLife;
/** Rate at which evolvable attributes change per generation. Default is 5%. */
public double rateOfEvolution = 0.05;
/** Whether the rate of evolution itself can evolve. Default is FALSE. */
public boolean rateOfEvolutionCanEvolve = false;
/** Colour of the animal. Default is light red. */
public int colour = 0xffff4040; // light red by default
/** Whether the colour can evolve. Default is TRUE. */
public boolean colourCanEvolve = true;
/** Speed (in pixels/iteration) of the animal when moving. This affects Life used per iteration. Default is 4.*/
public double speed = 4;
/** Whether the speed can evolve. Default is TRUE. */
public boolean speedCanEvolve = true;
/** Whether the animal is stopped. Default is FALSE. */
public boolean stopped = false;
/** Multiplier for the speed; this is changed by Hazards. Default value is 1 (no change to speed.) */
public double speedFactor = 1;
/** Life threshold at which the animal spawns. Default is 350. */
public int spawnLife = 350;
/** Number of children spawned each time. Default is 1. */
public int spawnChildren = 1;
/** Whether the spawning attributes can evolve. Default is TRUE. */
public boolean spawnCanEvolve = true;
/** Distance (in pixels) that the animal can see. Default is 10. */
public int sight = 10;
/** Whether the sight can evolve. Default is TRUE. */
public boolean sightCanEvolve = true;
/** Multiplier for the sight; this is changed by Hazards. Default value is 1 (no change to sight.) */
public double sightFactor = 1;
/** Alignment of the animal when deciding if other animals are friends or foes. Variance in the Red, Blue and Green ranges
* are calculated separately, and then added together to get the total variance. If that variance exceeds the colourAlignment,
* the animal is considered an enemy. Default is 64.
*/
public int colourAlignment = 64;
/** Whether the colour aligment can change. Default is TRUE. */
public boolean colourAlignmentCanEvolve = true;
/** Whether the sprite can evolve. Default is TRUE. */
public boolean spriteCanEvolve = true;
/** The current action for the animal. Default is Forward. */
public Action currentAction = Action.Forward;
/** Grid of the animal's actions. This translates to a 4 x 4 grid, of 0 to "3 or more" friends by 0 to "3 or more" foes.
* Default is all Forward.
*/
public Action[] actions;
/** Whether the actions grid can evolve. Default is TRUE. */
public boolean actionsCanEvolve = true;
/** Generation of the animal (how many generations parented it). */
public int generation = 0;
/** Random number used as the randomizer seed if randomizeChildren is false. This allows for repeatable runs. */
public double rand = randomGen.nextDouble();
/** Whether the randomization of children should be truly random, or should use a fixed seed value.
* A fixed seed allows for repeatable runs. Default is TRUE.
*/
public boolean randomizeChildren = true;
/** Number of kills made by the animal. */
public long kills = 0;
/** Number of times the animal has spawned. */
public long spawns = 0;
/** Number of children spawned by the animal. */
public long childrenSpawned = 0;
/** During which iteration the animal was born. */
public long born = 0;
/**
* Constructor, sets default locations, sprite, actions, statistics
*
*/
public Animal(){
super();
this.born = currentFrame;
// set defaults
this.positionX = randomGen.nextDouble() * (width - spriteWidth);
this.positionY = randomGen.nextDouble() * (height - spriteHeight);
this.setSprite(defaultSprite());
this.actions = new Action[ReactionEvent.values().length];
for(int i=0; i<ReactionEvent.values().length; i++){
actions[i] = Action.Forward;
}
}
/**
* Returns an array containing the default sprite.
* @return int[] An array of the default sprite.
*/
private int[] defaultSprite(){
return new int[] {
1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
0, 0, 1, 0, 0, 0, 0, 1, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 1, 0, 0, 1, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 0
};
}
/**
* Moves a sprite to it's next position, based on direction and speed.
* ... Note that I trim the sprite pixels from the max position, so
* it doesn't try to draw the sprite partially off the screen. I had
* to add the mod dividend to the divisor to allow for wrapping negative
* values, because Java assigns the sign of the divisor to the remainder.
*
* ie: -2 % 7 = -2, which isn't what we want, we want -2 % 7 = 5. To do
* that, I do (-2 + 7) % 7 = 5, which will also work for positives:
* (6 + 7) % 7 = 6, (11 + 7) % 7 = 4, etc...
*/
public void move(){
double dX = 0;
double dY = 0;
double directionToUse = (directionAltered != null ? directionAltered : direction);
this.directionAltered = null;
if (!stopped){
double[] deltaPos = deltaPosition((double)this.getSpeed(), directionToUse);
dX = deltaPos[0];
dY = deltaPos[1];
this.speedFactor = 1.0;
this.sightFactor = 1.0;
this.wipeSprite();
this.setPositionX(this.positionX + dX);
this.setPositionY(this.positionY + dY);
}
if (!this.scanForHazards(dX, dY)) return;
this.scanForPlants();
this.drawSprite();
}
/**
* Processes one iteration for the animal.<br/>
* Reduces an animal's Life based on the cost of living, plus the cost of whatever movement they made this iteration.
* <ul>
* <li>If the animal's Life drops below zero, they die</li>
* <li>If the animal has sufficient Life to survive the round, processes any movement for the animal.</li>
* </ul>
*/
public void live(){
this.life -= (this.stopped? 0 : (int)(this.getSpeed() * movingCost)) + livingCost;
if (life <= 0)
die();
else
this.move();
}
/**
* Kills the animal. Removes the sprite from the screen, removes the animal from the list, and updates the related statistic.
*/
public void die(){
// Decrement the count in the statistics
adjustStat(this.species, StatisticType.SpeciesCount, -1);
this.wipeSprite();
animals.remove(this);
}
/**
* Try to find a plant within the sight-range of the animal.
* @return Plant A plant, or NULL if no plants found.
*/
public Plant scanForAVisiblePlant(){
for (Plant p: plants){
// if the animal can see this plant, return it
if (spritesOverlap(this.getPositionX(), this.getPositionY(), p.getPositionX(), p.getPositionY(), this.getSight(), this.getSight()))
return p;
}
// nothing found
return null;
}
/**
* Find all plants touching the animal, and eat them.
*/
public void scanForPlants(){
Plant p;
for (int i=0; i<plants.size(); i++){
p = plants.get(i);
if (spritesOverlap(this.getPositionX(), this.getPositionY(), p.getPositionX(), p.getPositionY(), 0, 0))
this.eat(p);
}
}
/**
* Try to find a hazard within sight-range of the animal.
* @return Hazard A hazard, or NULL if no hazards found.
*/
public Hazard scanForAVisibleHazard(){
for (Hazard h: hazards){
// if the animal can see this hazard, return it
if (spritesOverlap(this.getPositionX(), this.getPositionY(), h.getPositionX(), h.getPositionY(), this.getSight(), this.getSight()))
return h;
}
// nothing found
return null;
}
/**
* Find all hazards either touching the animal (dX = 0, dY = 0), or else within a given range of the animal. For each hazard found,
* trigger the hazard's effects on the animal. If the hazard returns FALSE when affecting the animal, return false to the caller to
* indicate the action should be cancelled.
*
* @param dX X distance away that the hazard can be and still be counted
* @param dY Y distnace way that the hazard can be and still be counted
* @return boolean Whether the action that triggered this scan for Hazards should be cancelled.
*/
public boolean scanForHazards(double dX, double dY) {
for (Hazard h: hazards){
if (spritesOverlap(this.getPositionX(), this.getPositionY(), h.getPositionX(), h.getPositionY(), h.range, h.range))
if (!h.affectAnimal(this, dX, dY)) {
return false;
}
}
return true;
}
/**
* Determine who can see the animal. The two lists, enemies and friends, are populated by this method,
* and the aggregate difference in Life between the two sets of animals is returned. If <i>allowStandoffs</i> is set, then
* the search stops when more than 3 friends and more than 3 enemies are found; in that case, the method returns 0 as the
* Life difference simply because the difference is meaningless in standoffs.
*
* @param enemies List to be populated with all enemies that can see the animal.
* @param friends List to be populated iwth all the friends that can see the animal.
* @param range Maximum range at which other animals can detect the animal. The method uses the other animal's sight unless it exceeds this maximum.
* @return int The difference in life between the enemy and friend lists.
*/
public int whoCanSeeMe(ArrayList<Animal> enemies, ArrayList<Animal> friends, int range){
Animal a;
int lifeDiff = 0;
for (int i=0; i<animals.size(); i++){
a = animals.get(i);
int minRange = a.getSight() < range? a.getSight(): range;
if (a != this){
if (spritesOverlap(this.getPositionX(), this.getPositionY(), a.getPositionX(), a.getPositionY(), minRange, minRange)){
if (a.isFriendlyTo(this)) {
friends.add(a);
lifeDiff -= a.life;
} else {
enemies.add(a);
lifeDiff += a.life;
}
// If standoffs are allowed, and there are more than 3 enemies and 3 friends, then just return that a standoff occurred (lifeDiff won't matter.)
if (allowStandoffs && enemies.size() > 3 && friends.size() > 3) return 0;
}
}
}
return lifeDiff;
}
/**
* Determine who the anmial can see. The two lists, enemies and friends, are populated by this method. The animal's sight is used
* when determining what animals are within range. The method returns if it finds at least 3 friends and at least 3 enemies.
*
* @param enemies List to be populated with all enemies that the animal can see.
* @param friends List to be populated iwth all the friends that the animal can see.
*/
public void whoCanISee(ArrayList<Animal> enemies, ArrayList<Animal> friends){
Animal a;
for (int i=0; i<animals.size(); i++){
a = animals.get(i);
if (a != this){
if (spritesOverlap(this.getPositionX(), this.getPositionY(), a.getPositionX(), a.getPositionY(), this.getSight(), this.getSight())){
if (this.isFriendlyTo(a)) {
friends.add(a);
} else {
enemies.add(a);
}
if (enemies.size() > 3 && friends.size() > 3) return;
}
}
}
}
/**
* Determine if this animal is killed. Calculates which other animals can see the animal,
* then evaluates whether a standoff occurred, the animal survived, or the animal was killed.
* Allocates the animal's Life evenly between the killers if the animal dies (any excess Life
* that can't be evenly divided amongst the killers is lost.) Uses the
* <i>killRulesLifeThreshold</i> setting if <i>killRulesLifeBased</i> is set.
*/
public void calculateKill(){
ArrayList<Animal> enemies = new ArrayList<Animal>();;
ArrayList<Animal> friends = new ArrayList<Animal>();;
int lifeDiff = whoCanSeeMe(enemies, friends, killRange);
// If
// no standoff occurred (or standoffs aren't allowed), AND
// Life-based kill rules aren't being used and there are two more enemies than friends, OR
// Life-based kill rules are in use and the difference is greater or equal to the threshhold
// THEN
// the enemies kill this animal.
if (!(allowStandoffs && enemies.size() > 3 && friends.size() > 3)
&& ((!killRulesLifeBased && enemies.size() - friends.size() > 2)
|| (killRulesLifeBased && (lifeDiff - this.life) >= killRulesLifeThreshold))
)
{
for(Animal b: enemies){
// this is intentionally designed to potentially leave some Life points unallocated.
b.life += (this.life / enemies.size());
b.kills++;
}
this.die();
return;
}
}
/**
* Determines the animal's reaction to its environment (enemies and friends), and adjusts the animal's movement based
* on the determined action.
*/
public void calculateReaction(){
ArrayList<Animal> enemies = new ArrayList<Animal>();
ArrayList<Animal> friends = new ArrayList<Animal>();
Plant p;
Hazard h;
whoCanISee(enemies, friends);
int enemiesIndex = limitRange(enemies.size(), 0, 3);
int friendsIndex = limitRange(friends.size(), 0, 3);
this.currentAction = this.actions[enemiesIndex * 4 + friendsIndex];
this.stopped = false;
switch (this.currentAction){
case Stop:
this.stopped = true;
break;
case Forward:
break;
case Reverse:
this.setDirection(this.getDirection() + 180);
break;
case Left:
this.setDirection(this.getDirection() + 90);
break;
case Right:
this.setDirection(this.getDirection() - 90);
break;
case North:
this.setDirection(90);
break;
case NorthEast:
this.setDirection(45);
break;
case East:
this.setDirection(0);
break;
case SouthEast:
this.setDirection(315);
break;
case South:
this.setDirection(270);
break;
case SouthWest:
this.setDirection(225);
break;
case West:
this.setDirection(180);
break;
case NorthWest:
this.setDirection(135);
break;
case FollowFriend:
if (friends.size() > 0)
// NOTE: Sets the direction to be the SAME as the friend's. Rather than actually moving towards the friend, they'll move in parallel.
this.setDirection(friends.get(0).getDirection());
break;
case FollowEnemy:
if (enemies.size() > 0)
this.direction = Math.atan2(enemies.get(0).positionY - this.positionY, this.positionX - enemies.get(0).positionX) + Math.PI;
break;
case FollowFood:
p = scanForAVisiblePlant();
if (p != null) this.direction = Math.atan2(p.positionY - this.positionY, this.positionX - p.positionX) + Math.PI;
break;
case FollowMouse:
if (spritesOverlap(this.getPositionX(), this.getPositionY(), xMouse, (yMouse-titlebar), this.sight, this.sight))
this.direction = Math.atan2((yMouse-titlebar) - this.positionY, this.positionX - xMouse) + Math.PI;
break;
case FollowHazard:
h = scanForAVisibleHazard();
if (h != null) this.direction = Math.atan2(h.positionY - this.positionY, this.positionX - h.positionX) + Math.PI;
break;
case FleeFriend:
if (friends.size() > 0)
this.direction = Math.atan2(this.positionY - friends.get(0).positionY, friends.get(0).positionX - this.positionX) + Math.PI;
break;
case FleeEnemy:
if (enemies.size() > 0)
this.direction = Math.atan2(this.positionY - enemies.get(0).positionY, enemies.get(0).positionX - this.positionX) + Math.PI;
break;
case FleeFood:
p = scanForAVisiblePlant();
if (p != null) this.direction = Math.atan2(this.positionY - p.positionY, p.positionX - this.positionX) + Math.PI;
break;
case FleeMouse:
if (spritesOverlap(this.getPositionX(), this.getPositionY(), xMouse, (yMouse-titlebar), this.sight, this.sight))
this.direction = Math.atan2(this.positionY - (yMouse-titlebar), xMouse - this.positionX) + Math.PI;
break;
case FleeHazard:
h = scanForAVisibleHazard();
if (h != null) this.direction = Math.atan2(this.positionY - h.positionY, h.positionX - this.positionX) + Math.PI;
break;
case Random:
this.setDirection((int)(this.rand() * 360));
}
}
/**
* Determine if this animal is friendly to another animal. This is determined by finding the difference between the ARGB
* channels of the colour of this animal and the other animal, then summing the absolute value of those differences. If the total
* difference exceeds this animal's colour alignment, then the other animal is an enemy.
*
* @param a Animal to be considered friend or foe
* @return boolean TRUE if the animal is considered a friend, FALSE if the animal is considered an enemy.
*/
public boolean isFriendlyTo(Animal a){
int[] thisARGB = splitARGB(this.colour);
int[] aARGB = splitARGB(a.colour);
int colourDiff =
Math.abs(thisARGB[0] - aARGB[0]) +
Math.abs(thisARGB[1] - aARGB[1]) +
Math.abs(thisARGB[2] - aARGB[2]) +
Math.abs(thisARGB[3] - aARGB[3]);
// If the colour difference is greater than this animal's alignment, this animals considers the other animal an enemy
return (colourDiff <= this.colourAlignment);
}
/**
* Eats a plant. Transfers the plant's life to this animal, and kills the plant.
* @param p Plant to be eaten
*/
public void eat(Plant p){
this.life += p.life;
p.die();
}
/**
* Spawn children. Divides the parent's life evenly among the parent and all the children. (Any excess Life that
* can't be evenly divided is lost.) Applies randomized evolution to each child's attributes (where evolution is enabled.)
*/
public void spawn(){
Animal a;
int originalLife = this.life;
if (this.spawnChildren > 0) {
this.spawns++;
for (int i=0; i<this.spawnChildren && animals.size() < animalLimit; i++){
this.childrenSpawned++;
// Increment the count in the statistics
adjustStat(this.species, StatisticType.SpeciesCount, 1);
a = new Animal();
a.life = originalLife / (this.spawnChildren + 1);
animals.add(a);
//direct inheritance
a.species = this.species;
a.rateOfEvolution = this.rateOfEvolution;
a.colourCanEvolve = this.colourCanEvolve;
a.colourAlignmentCanEvolve = this.colourAlignmentCanEvolve;
a.sightCanEvolve = this.sightCanEvolve;
a.spawnCanEvolve = this.spawnCanEvolve;
a.speedCanEvolve = this.speedCanEvolve;
a.spriteCanEvolve = this.spriteCanEvolve;
a.actionsCanEvolve = this.actionsCanEvolve;
a.rateOfEvolutionCanEvolve = this.rateOfEvolutionCanEvolve;
a.stopped = this.stopped;
a.sprite = this.sprite.clone();
a.actions = this.actions.clone();
a.generation = this.generation + 1;
a.randomizeChildren = this.randomizeChildren;
this.life -= a.life;
a.setPositionX(this.positionX + (randomGen.nextDouble() * spriteWidth));
// Use of spriteHeight here yields skewed spawn points, so I've used spriteWidth for positionY.
a.setPositionY(this.positionY + (randomGen.nextDouble() * spriteWidth));
//inheritance dependent on evolution
if (this.spriteCanEvolve){
int pixelsToChange = (int)Math.round(a.sprite.length * this.rand() * this.rateOfEvolution);
while (pixelsToChange > 0){
int pixel = randomGen.nextInt(a.sprite.length);
// Only change this pixel if it will change to match one of it's neighbours
if (
((pixel % spriteWidth > 0) && (a.sprite[pixel - 1] != a.sprite[pixel])) // left
|| ((pixel % spriteWidth < (spriteWidth - 1)) && (a.sprite[pixel + 1] != a.sprite[pixel])) // right
|| ((pixel - spriteWidth > 0) && (a.sprite[pixel - spriteWidth] != a.sprite[pixel])) //top
|| ((pixel + spriteWidth < a.sprite.length) && (a.sprite[pixel + spriteWidth] != a.sprite[pixel])))
{
a.sprite[pixel] = ++a.sprite[pixel] % 2;
pixelsToChange--;
}
}
}
if (this.rateOfEvolutionCanEvolve){
a.rateOfEvolution = limitRange(this.rateOfEvolution + (randNegOneToOne() * 0.1), 0.0001, 1.0);
} else {
a.rateOfEvolution = this.rateOfEvolution;
}
if (this.colourCanEvolve){
int[] argb = splitARGB(this.colour);
short channel = (short)(this.rand() * 3 + 1);
argb[channel] = limitRange(argb[channel] + (int)Math.round(0xff * randNegOneToOne() * this.rateOfEvolution), 0, 0xff);
a.colour = getARGB(argb[0], argb[1], argb[2], argb[3]);
} else {
a.colour = this.colour;
}
if (this.speedCanEvolve) {
a.speed = limitRange(this.speed + (speedLimit * randNegOneToOne() * this.rateOfEvolution), 0.00001, speedLimit);
} else {
a.speed = this.speed;
}
if (this.colourAlignmentCanEvolve) {
a.colourAlignment = limitRange(this.colourAlignment + (int)Math.round((3 * 0xff) * randNegOneToOne() * this.rateOfEvolution), 0, 3 * 0xff);
} else {
a.colourAlignment = this.colourAlignment;
}
if (this.sightCanEvolve){
a.sight = limitRange(this.sight + (int)Math.round(sightLimit * randNegOneToOne() * this.rateOfEvolution), 0, sightLimit);
} else {
a.sight = this.sight;
}
if (this.spawnCanEvolve){
a.spawnLife = limitRange(this.spawnLife + (int)Math.round(baseLife * randNegOneToOne() * this.rateOfEvolution), 2, Integer.MAX_VALUE);
a.spawnChildren = limitRange(this.spawnChildren + (int)Math.round(spawnChildrenLimit * randNegOneToOne() * this.rateOfEvolution), 1, spawnChildrenLimit);
} else {
a.spawnLife = this.spawnLife;
a.spawnChildren = this.spawnChildren;
}
if (this.actionsCanEvolve){
// I added an extra sum to the end of this equation so that if the rateOfEvolution isn't zero, you always have a chance to change at least one action.
// Otherwise, when the rateOfEvolution was below 1/numReactions, the number returned was always too low and got rounded to zero.
int actionsToChange = (int)Math.round((a.actions.length - 1) * this.rand() * this.rateOfEvolution + (this.rand() * Math.ceil(this.rateOfEvolution)));
for (int j=0; j<actionsToChange; j++){
a.actions[(int)Math.floor(a.actions.length * this.rand())] = Action.values()[(int)Math.floor(Action.values().length * this.rand())];
}
}
a.drawSprite();
}
}
}
/**
* Draw's the sprite onto a pixel array. Only draws the non-background pixels (i.e.: sprites have transparent backgrounds.)
*/
public void drawSprite(int x, int y, int[] image){
for (int i=0; i<spriteHeight; i++) {
for (int j=0; j<spriteWidth; j++){
// if this isn't a background pixel, draw it
if (this.sprite[i * spriteWidth + j] == 1){
image[(width * (y+i)) + (x+j)] = this.colour;
}
}
}
}
/**
* Removes the sprite from the pixel array. Resets the pixels to those from the background layer (including hazards); that
* means that any overlapping sprites (animals or plants) will need to redraw themselves onto those pixels.
*/
public void wipeSprite(){
for (int i=0; i<spriteHeight && doWipe; i++) {
for (int j=0; j<spriteWidth; j++){
if (this.sprite[i * spriteWidth + j] == 1){
int index = (width * (this.getPositionY()+i)) + (this.getPositionX()+j);
pixels[index] = backgroundPixels[index];
}
}
}
}
/**
* Returns an array of the animal's sprite's pixels. Replaces the 1's with the sprite's current colour, and the 0's with
* the default background colour.
*
* @return int[] Array of the sprite's pixels.
*/
public int[] getSprite(){
int[] result = this.sprite.clone();
for (int i = 0; i < result.length; i++){
if (result[i] == 1)
result[i] = this.colour;
else
result[i] = background;
}
return result;
}
/**
* Returns an array of the animal's sprite's pixels, zoomed by the magnification factor.
*
* @return int[] Array of the sprite's pixels, magnified
*/
public int[] getSpriteZoomed(int magnificationFactor){
return super.getSpriteZoomed(this.getSprite(), magnificationFactor);
}
/**
* Gets the animal's direction in degrees.
* @return int Direction in degrees.
*/
public int getDirection() {
return (int) Math.toDegrees(this.direction);
}
/**
* Sets the animal's direction.
* @param direction Direction in degrees.
*/
public void setDirection(int direction) {
this.direction = Math.toRadians((direction + 360) % 360);
}
/**
* Gets the animal's sight distance. Applies <i>sightFactor</i> multiplier
* to the sight distance returned.
* @return int Distance the animal can see, in pixels.
*/
public int getSight(){
return (int) (this.sight * this.sightFactor);
}
/**
* Sets the X position, to a higher precision than single pixels.
* @param positionX
*/
public void setPositionX(double positionX) {
//this.positionX = (positionX + (width - spriteWidth)) % (width - spriteWidth);
this.positionX = positionX;
this.wrapPosition();
}
/**
* Sets the Y position, to a higher precision than single pixels.
* @param positionY
*/
public void setPositionY(double positionY) {
//this.positionY = (positionY + (height - spriteHeight)) % (height - spriteHeight);
this.positionY = positionY;
this.wrapPosition();
}
/**
* Builds a string with all of the animal's attributes.
* @return String A String representation of the animal's attributes.
*/
public String toString(){
String pad = " ";
StringBuilder str = new StringBuilder();
str.append("ID: ");
str.append(this.id);
str.append("\nSpecies: ");
str.append(this.species);
str.append("\nGeneration: ");
str.append(this.generation);
str.append("\nAge: ");
str.append(currentFrame - this.born);
str.append("\nLife: ");
str.append(this.life);
str.append("\nKills: ");
str.append(this.kills);
str.append("\nSpawns: ");
str.append(this.spawns);
str.append("\nChildren Spawned: ");
str.append(this.childrenSpawned);
str.append("\nPosition: (");
str.append((int)this.positionX);
str.append(",");
str.append((int)this.positionY);
str.append(")");
str.append("\nDirection: ");
str.append(this.getDirection());
str.append(String.format("\nSpeed: %.2f (* %.2f%%)", this.speed, this.speedFactor * 100));
str.append("\nColour: ");
str.append(Integer.toHexString(this.colour));
str.append("\nSpawn Life: ");
str.append(this.spawnLife);
str.append("\nSpawn Children: ");
str.append(this.spawnChildren);
str.append("\nSight: ");
str.append(this.sight);
str.append("\nColour Alignement: ");
str.append(this.colourAlignment);
str.append(String.format("\nRate of Evolution: %.2f%%", this.rateOfEvolution * 100));
str.append("\nCurrent Action: ");
str.append(this.currentAction);
str.append("\nReaction Grid: (friends [across], enemies [down])\n");
str.append(" 0 1 2 3\n");
for (int i=0; i<4; i++){
str.append(i + " ");
for (int j=0; j<4; j++){
str.append(this.actions[i*4 + j] + pad.substring(this.actions[i*4 + j].toString().length()));
}
str.append("\n");
}
return str.toString();
}
/**
*
* @return
*/
private double rand(){
if (this.randomizeChildren)
return randomGen.nextDouble();
else
return this.rand;
}
private double randNegOneToOne(){
if (this.randomizeChildren)
return getRand();
else
return this.rand;
}
public double getSpeed() {
return this.speed * this.speedFactor;
}
public Double getDirectionAltered() {
return new Double(Math.toDegrees(this.directionAltered));
}
public void setDirectionAltered(Double directionNew) {
this.directionAltered = Math.toRadians((directionNew + 360) % 360);
}
}
private class Plant extends Drawable {
int life = basePlantLife;
int colour = 0xff008020; // green
int growthRate = (int)(getRand() * growthLimit);
public Plant(){
this.positionX = randomGen.nextInt(width - spriteWidth);
this.positionY = randomGen.nextInt(height - spriteHeight);
this.setSprite();
}
public void setSprite(){
super.setSprite(new int[] {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 0, 1, 1, 0, 0,
0, 1, 1, 0, 1, 1, 0, 1, 1, 0,
0, 1, 1, 1, 0, 1, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 0, 1, 0, 0,
0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
});
for (int i = 0; i < this.sprite.length; i++){
if (sprite[i] == 1)
sprite[i] = this.colour;
else
sprite[i] = background;
}
}
public void live(){
this.life += growthRate;
if (this.life <=0)
this.die();
else {
int[] rgb = splitARGB(this.colour);
if (growthRate != 0) {
this.colour = getARGB(
rgb[0],
rgb[1],
limitRange((Math.abs(this.life - basePlantLife) / growthRate) + Math.abs(basePlantLife / growthRate), 0, 255),
rgb[3]);
this.setSprite();
this.drawSprite();
}
}
}
public void die(){
this.wipeSprite();
plants.remove(this);
}
public String toString(){
StringBuilder str = new StringBuilder();
str.append("Life: ");
str.append(this.life);
str.append("\nGrowthRate: ");
str.append(this.growthRate);
str.append("\nColour: ");
str.append(Integer.toHexString(this.colour));
str.append("\nPosition: (");
str.append((int)this.positionX);
str.append(",");
str.append((int)this.positionY);
str.append(")");
return str.toString();
}
}
private class Hazard extends Drawable {
public HazardType type;
public double strength = randomGen.nextDouble();
public double direction = randomGen.nextDouble() * 2.0 * Math.PI;
public int range = 0;
/** Use all the defaults */
public Hazard(HazardType type){
// Set the default values
this.type = type;
this.positionX = randomGen.nextInt(width - spriteWidth);
this.positionY = randomGen.nextInt(height - spriteHeight);
// If this is Paint, then pump the strength up to a realistic value
if (type == HazardType.Paint){
this.strength = randomGen.nextInt(0xFFFFFF + 1);
}
// Initialize this object
this.initialize(null, null, null, null, null, null);
}
/** Initialize this object's member variables */
public void initialize(Double positionX, Double positionY, HazardType type, Double strength, Double direction, Integer range){
if (positionX != null) this.positionX = positionX;
if (positionY != null) this.positionY = positionY;
if (type != null) this.type = type;
if (strength != null) this.strength = strength;
if (range != null) this.range = range;
if (direction != null) this.direction = Math.toRadians(direction);
this.sprite = hazardSprites.get(this.type).clone();
if (this.type == HazardType.Paint) {
if (splitARGB((int)this.strength)[0] == 0) this.strength = 0xff000000 | (int)this.strength;
for (int i=0; i<spriteHeight; i++) {
for (int j=0; j<spriteWidth; j++){
// if this isn't a background pixel, change it's colour
if (this.sprite[i * spriteWidth + j] == (0xff000000 | 1)){
this.sprite[i * spriteWidth + j] = (int)this.strength;
}
}
}
}
}
/** Affect an animal */
public boolean affectAnimal(Animal a, double dX, double dY){
switch (this.type){
case Fire:
a.life -= 100 * this.strength;
if (a.life <= 0) {
a.die();
return false;
}
break;
case Fog:
a.sightFactor = 1.0 - this.strength;
break;
case Glue:
a.speedFactor = 0;
break;
case Hole:
a.die();
return false;
case Ice:
a.speedFactor = 1.0 + this.strength;
break;
case Mud:
a.speedFactor = 1.0 - this.strength;
break;
case Paint:
a.colour = (int)(this.strength);
break;
case Teleporter:
double[] deltas = deltaPosition(this.strength * width, this.direction);
a.setPositionX(a.positionX + deltas[0]);
a.setPositionY(a.positionY + deltas[1]);
break;
case Wall:
//TODO: Define effects of a wall
// Local vars for doing walk backs (set interval here)
double dX2 = dX/2.0; //walk back by halves
double dY2 = dY/2.0; //walk back by halves
boolean xFirst = (Math.abs(dX) < Math.abs(dY));
// System.out.println(a.positionX + "," + a.positionY
// + " / " + this.positionX + "," + this.positionY
// + " / " + dX + "," + dY);
// Do alternating walkbacks until clear of the obstacle
while (spritesOverlap(
this.getPositionX(),
this.getPositionY(),
a.getPositionX(),
a.getPositionY(), 0, 0)) {
// Walk back on smallest diff first
if (xFirst) {
a.setPositionX(a.positionX - dX2);
} else {
a.setPositionY(a.positionY - dY2);
}
// System.out.print(" __1 " + a.positionX + ", " + a.positionY);
// If still overlapping, walk back on biggest diff instead
if (spritesOverlap(
this.getPositionX(),
this.getPositionY(),
a.getPositionX(),
a.getPositionY(), 0, 0)) {
if (xFirst) {
a.setPositionX(a.positionX + dX2);
a.setPositionY(a.positionY - dY2);
} else {
a.setPositionY(a.positionY + dY2);
a.setPositionX(a.positionX - dX2);
}
// System.out.print("__2 " + a.positionX + ", " + a.positionY);
}
// If still overlapping, walk back on both
if (spritesOverlap(
this.getPositionX(),
this.getPositionY(),
a.getPositionX(),
a.getPositionY(), 0, 0)) {
if (xFirst) {
a.setPositionX(a.positionX - dX2);
} else {
a.setPositionY(a.positionY - dY2);
}
// System.out.print("__3 " + a.positionX + ", " + a.positionY);
}
// System.out.println();
}
/*
for (int i=0; i<3 && spritesOverlap(
this.getPositionX(),
this.getPositionY(),
a.getPositionX(),
a.getPositionY(), 0, 0);
i++){
if (xFirst) {
a.setPositionY(a.positionY - (dY/2));
} else {
a.setPositionX(a.positionX - (dX/2));
}
xFirst = !xFirst;
}
a.setPositionX(a.positionX);
a.setPositionY(a.positionY);
*/
/* Break into x/y movement components
* if abs(x) < abs(y)
* try to move -x/2
* if collision try -y/2
* if collision do -x, -y
* else
* try to move -y/2
* if collision try -x/2
* if collision do -x, -y
*
*/
/* if (Math.abs(this.getPositionX() - a.getPositionX()) > Math.abs(this.getPositionY() - a.getPositionY())) {
if (this.getPositionX() > a.getPositionX())
a.setPositionX(this.getPositionX() - spriteWidth);
else
a.setPositionX(this.getPositionX() + spriteWidth);
} else {
if (this.getPositionY() > a.getPositionY())
a.setPositionY(this.getPositionY() - spriteHeight);
else
a.setPositionY(this.getPositionY() + spriteHeight);
}
*/ break;
}
return true;
}
/** Override drawSprite() so we can set the hazard into the background pixels */
public void drawSprite(){
super.drawSprite(pixels);
super.drawSprite(backgroundPixels);
}
public void wipeSprite(){
super.wipeSprite(backgroundPixels, backgroundImagePixels);
super.wipeSprite(pixels, backgroundPixels);
}
public void die(){
this.wipeSprite();
hazards.remove(this);
}
public int getDirection() {
return (int) Math.toDegrees(this.direction);
}
public void setDirection(int direction) {
this.direction = Math.toRadians((direction + 360) % 360);
}
public String toString(){
StringBuilder str = new StringBuilder();
str.append("Hazard type: ");
str.append(this.type);
str.append("\nPosition: (");
str.append((int)this.positionX);
str.append(",");
str.append((int)this.positionY);
str.append(")");
str.append("\nDirection: ");
str.append(this.getDirection());
str.append("\nStrength: ");
str.append(this.strength);
return str.toString();
}
}
private static void showStats(BufferedImage buffStats, Graphics gr) throws InterruptedException {
if (clicked.size() > 0) {
// local variables to iterate over the collection of clicked animals
Drawable a = null;
int count = clicked.size() - 1;
int index = 0;
while (showClicked && !escape && clicked.size() > 0){
int line = 0;
if (clicked.size() - 1 != count) {
count = clicked.size() - 1;
index = 0;
a = null;
}
if (a == null || (moveUp && index > 0) || (moveDown && index < count) || killCurrent){
if (a == null){
// If this is the first time the screen is shown
index = 0;
a = clicked.get(index);
frame.setTitle(title + " (stats: right-click to return to main view)");
} else if (moveUp && index > 0) {
// If we're moving up one animal in the list
index--;
a = clicked.get(index);
} else if (moveDown && index < count) {
// If we're moving down one animal in the list
index++;
a = clicked.get(index);
} else if (killCurrent) {
a.die();
clicked.remove(a);
}
if (!killCurrent) {
// prep the screen
buffStats.setRGB(11, lineHeight * 4 + 4, spriteWidth * spriteStatsZoom, spriteHeight * spriteStatsZoom, a.getSpriteZoomed(spriteStatsZoom), 0, spriteWidth * spriteStatsZoom);
gr.drawImage(buffStats, 0, titlebar, new Color(background), null);
gr.setFont(new Font("Sans-Serif", Font.BOLD, 18));
gr.setColor(Color.WHITE);
gr.drawString("STATS (right-click to exit)", 100, line + titlebar + 18);
gr.setFont(new Font("Monospaced", Font.PLAIN, 12));
gr.setColor(Color.GREEN);
line += lineHeight * 2;
if (index > 0) gr.drawString("[" + index + "] Previous (press up arrow)", textOffset, line + titlebar + 12);
line += lineHeight * 2;
for (String str : a.toString().split("\n")) {
gr.drawString(str, textOffset, line + titlebar + 12);
line += lineHeight;
}
line += lineHeight;
if (index < count) gr.drawString("[" + (count - index) + "] Next (press down arrow)", textOffset, line + titlebar + 12);
}
}
moveUp = false;
moveDown = false;
killCurrent = false;
Thread.sleep(250);
}
} else {
clicked = null;
showClicked = false;
}
}
/**
* Generates a random number between -1 and 1
* @return double between -1.0 and 1.0
*/
private static double getRand(){
return randomGen.nextDouble() * 2.0 - 1.0;
}
/**
* Finds the position, in x and y coordinates, if travelling the given distance in the given direction.
* @param distance
* @param direction
* @return double[2] array containing the position delta in x and y coords
*/
private static double[] deltaPosition(double distance, double direction){
double[] result = new double[2];
result[0] = distance * Math.cos(direction);
result[1] = - distance * Math.sin(direction);
return result;
}
/**
* Extremity check integer values to bounds
* @param value
* @param lower
* @param upper
* @return int Value within given bounds
*/
private static int limitRange(long value, int lower, int upper){
if (value < lower)
return lower;
else if (value > upper)
return upper;
else
return (int) value;
}
/**
* Extremity check integer values to bounds
* @param value
* @param lower
* @param upper
* @return double Value within given bounds
*/
private static double limitRange(double value, double lower, double upper){
if (value < lower)
return lower;
else if (value > upper)
return upper;
else
return value;
}
/**
* Convert colour elements to an ARGB value using bit-shifting.
* @param a
* @param r
* @param g
* @param b
* @return ARGB as an INT
*/
private static int getARGB(int a, int r, int g, int b){
return a << 24 | r << 16 | g << 8 | b;
}
/**
* Convert an ARGB int into it's component parts using bit-masking
* @param argb
* @return 4 element
*/
private static int[] splitARGB(int argb){
int[] result = new int[] {
argb >> 24 & 0x000000ff,
argb >> 16 & 0x000000ff,
argb >> 8 & 0x000000ff,
argb & 0x000000ff
};
return result;
}
/**
* Calculate if two sprites overlap, based on their upper left corner.
* Relies partially on the fact that all sprites are the same size.
* @param x1
* @param y1
* @param x2
* @param y2
* @param xRange Extra distance to scan (beyond sprite perimeter)
* @param yRange
* @return boolean Whether or not they overlap
*/
private static boolean spritesOverlap(int x1, int y1, int x2, int y2, int xRange, int yRange){
// if(Math.abs(x1-x2) < (spriteWidth+xRange) && Math.abs(y1-y2) < (spriteHeight+yRange))
// System.out.println("overlap:(" + x1 + "," + y1 + "),(" + x2 + "," + y2 + ")");;
return (Math.abs(x1-x2) < (spriteWidth+xRange) && Math.abs(y1-y2) < (spriteHeight+yRange));
/*
boolean partialOverlap = false;
boolean overlap = false;
partialOverlap = (x1 > x2 && x1 < x2 + spriteWidth);
partialOverlap = partialOverlap || (x1 + spriteWidth > x2 && x1 + spriteWidth < x2 + spriteWidth);
overlap = partialOverlap && (y1 > y2 && y1 < y2 + spriteHeight);
overlap = overlap || (partialOverlap && (y1 > y2 && y1 < y2 + spriteHeight));
return overlap;
*/
}
/**
* Returns a list of all sprites (animals, plants, hazards) the user clicked on.
* @param xMouse
* @param yMouse
* @return ArrayList List of sprites
*/
private ArrayList<Drawable> spritesClicked(int xMouse, int yMouse){
ArrayList<Drawable> clicked = new ArrayList<Drawable>();
for (Animal animal : animals) {
if (Math.abs(animal.positionX + (spriteWidth / 2) - xMouse) < spriteWidth / 2
&& Math.abs(animal.positionY + (spriteHeight / 2) - (yMouse - titlebar)) < spriteHeight / 2)
clicked.add(animal);
}
for (Plant plant : plants) {
if (Math.abs(plant.positionX + (spriteWidth / 2) - xMouse) < spriteWidth / 2
&& Math.abs(plant.positionY + (spriteHeight / 2) - (yMouse - titlebar)) < spriteHeight / 2)
clicked.add(plant);
}
for (Hazard hazard : hazards) {
if (Math.abs(hazard.positionX + (spriteWidth / 2) - xMouse) < spriteWidth / 2
&& Math.abs(hazard.positionY + (spriteHeight / 2) - (yMouse - titlebar)) < spriteHeight / 2)
clicked.add(hazard);
}
return clicked;
}
/**
* Process input events to the JFrame.
*/
public void processEvent(AWTEvent e)
{
if (e.getID() == KeyEvent.KEY_PRESSED && ((KeyEvent)e).getWhen() - System.currentTimeMillis() > 100)
return;
switch (e.getID())
{
// If window closes, exit the app
case WindowEvent.WINDOW_CLOSING:
System.exit(0);
break;
// If window is minimized/restored, set a flag to indicate if
// the app should bother painting the screen.
case WindowEvent.WINDOW_ICONIFIED:
case WindowEvent.WINDOW_DEICONIFIED:
doPaint = (e.getID() == WindowEvent.WINDOW_DEICONIFIED);
break;
// If the window is resized, check if the playing field should be resized along with the window
case ComponentEvent.COMPONENT_RESIZED:
if ((frame.getHeight() != (height + titlebar)) || (frame.getWidth() != width)){
doResize = true;
}
break;
// If the mouse wheel is rolled, adjust the gamespeed up or down to match
case MouseEvent.MOUSE_WHEEL:
sleepTime = limitRange(sleepTime + (((MouseWheelEvent)e).getWheelRotation() * 10), 0, 3000);
System.out.println("Loop Pause: " + sleepTime);
break;
// Reset the mouse button flag
case MouseEvent.MOUSE_RELEASED:
mouseButton = 0;
break;
// Set a flag indicating whic mouse button was clicked
case MouseEvent.MOUSE_PRESSED:
mouseButton = ((MouseEvent)e).getButton();
// Set flags to indicate where the mouse was moved to, and fire a method to handle it
case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
xMouse = ((MouseEvent)e).getX();
yMouse = ((MouseEvent)e).getY();
processMouseClick();
break;
// Set flags to indicate which keys are currently down, and fire a method to handle it
case KeyEvent.KEY_PRESSED:
case KeyEvent.KEY_RELEASED:
// skip keypresses more than 0.1 seconds old, because they indicate bad lag time
if (System.currentTimeMillis() - ((KeyEvent)e).getWhen() > 100)
break;
keys[((KeyEvent)e).getKeyCode()] = (e.getID() == KeyEvent.KEY_PRESSED);
processKeyPress();
break;
}
}
/**
* Handles mouse clicks, either adding sprites to a list of items to be displayed,
* or resetting the flag to indicate that the click results window should be hidden.
*/
private void processMouseClick(){
switch (mouseButton){
case MouseEvent.BUTTON1:
if (paused && !showClicked) {
clicked = spritesClicked(xMouse, yMouse);
showClicked = true;
}
break;
case MouseEvent.BUTTON3:
showClicked = false;
clicked = null;
break;
}
}
/**
* Handles key presses, by initiating actions for any keys that are currently "down" (pressed)
*
*/
@SuppressWarnings("unchecked")
private void processKeyPress(){
if (keys[KeyEvent.VK_SPACE]) paused = !paused; // space = pause/unpause
if (keys[KeyEvent.VK_ESCAPE]) escape = true; // escape key (used to exit)
if (keys[KeyEvent.VK_UP]) moveUp = true; // up = scroll up the list of sprites
if (keys[KeyEvent.VK_DOWN]) moveDown = true; // down = scroll down the list of sprites
for (int i=0; i<10; i++){ // #0-9 = put a hazard at the current mouse location
if (keys[KeyEvent.VK_0 + ((i + 1) % 10)]) dropHazard(i);
}
if (keys[KeyEvent.VK_C]) { // "c" = take a screenshot
writeBuffer(buff);
}
if (keys[KeyEvent.VK_K] // "k" - kill the currently selected object
&& paused
&& showClicked) {
killCurrent = true;
}
if (keys[KeyEvent.VK_L] && paused) { // "l" = list all animals
clicked = (ArrayList<Drawable>)animals.clone();
showClicked = true;
}
if (keys[KeyEvent.VK_P]) { // "p" = drop a plant
dropPlant();
}
if (keys[KeyEvent.VK_R]) remapPixels(); // "r" = remap pixels
if (keys[KeyEvent.VK_W]) doWipe = !doWipe; // "w" = wipe/don't wipe previous sprite positions
}
/**
* Drops a hazard at the current mouse location
* @param index of the hazard in the HazardType enumeration
*/
private static void dropHazard(int index){
if (index < HazardType.values().length) {
Hazard h = frame.new Hazard(HazardType.values()[index]);
h.positionX = dropPointX();
h.positionY = dropPointY();
h.drawSprite(pixels);
h.drawSprite(backgroundPixels); // hazards are drawn directly to the background
droppedHazards.add(h); // this list of hazards will be appended to all
// hazards after the next cycle
// If the game is paused, it doesn't repaint at all, so trigger a repaint if more than 100
// milliseconds have passed since the last repaint
if (paused && (System.currentTimeMillis() > (lastPaintTime + 100))) paint(buff, gr);
}
}
/**
* Drops a plant at the current mouse location
*/
private static void dropPlant(){
Plant p = frame.new Plant();
p.positionX = dropPointX();
p.positionY = dropPointY();
p.drawSprite();
droppedPlants.add(p); // this list of plants will be appended to all
// plants after the next cycle
// If the game is paused, it doesn't repaint at all, so trigger a repaint if more than 100
// milliseconds have passed since the last repaint
if (paused && (System.currentTimeMillis() > (lastPaintTime + 100))) paint(buff, gr);
}
/**
* Calculates the x-coord of the drop point on the playing field, based on the mouse position
* @return int, x-coord on the playing field
*/
private static int dropPointX(){
return (xMouse + width - (spriteWidth / 2)) % width;
}
/**
* Calculates the y-coord of the drop point on the playing field, based on the mouse position
* @return int, y-coord on the playing field
*/
private static int dropPointY(){
return (yMouse - titlebar + height - (spriteHeight / 2)) % height;
}
/**
* I split this method out to keep it generic. Just paints the buffer to the window.
* @param buff Buffer to draw
* @param gr Graphics object to draw on
*/
private static void paint(BufferedImage buff, Graphics gr){
buff.setRGB(0, 0, width, height, pixels, 0, width);
gr.drawImage(buff, 0, titlebar, new Color(background), null);
lastPaintTime = System.currentTimeMillis();
}
/**
* Remaps pixels back to the background pixels buffer, then redraws all
* non-background sprites to the buffer. Basically, redraws the full image,
* by drawing the background layer and then all upper layers over-top.
*/
private static synchronized void remapPixels(){
pixels = backgroundPixels.clone();
for (Hazard h: hazards){
h.wrapPosition();
h.drawSprite();
}
for (Plant p: plants){
p.wrapPosition();
p.drawSprite();
}
for (Animal a: animals){
a.wrapPosition();
a.drawSprite();
}
}
/**
* Resizes the window and playing field, moves all off-screen sprites so they fit
* into the new dimensions, and remaps the sprites into the resized buffers.
*/
private static synchronized void resizePixels(){
height = frame.getHeight() - titlebar;
height = (height < spriteHeight + 1 ? spriteHeight + 1 : height);
width = frame.getWidth();
width = (width < spriteWidth + 1 ? spriteWidth + 1 : width);
setUpBuffers();
remapPixels();
doResize = false;
System.out.println("Resized: " + width + "/" + height);
}
/**
* Set up the buffers, using the window size information and the background image
*
*/
private static synchronized void setUpBuffers(){
try {
buffStats = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
buff = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
backgroundImagePixels = new int[height * width];
if (backgroundFile != null && backgroundFile.length() > 0) {
// Get the background image
URL url = Evolution.class.getResource(backgroundFile);
ImageIcon imgImport = new ImageIcon(url);
Image img = imgImport.getImage();
PixelGrabber pGrab = new PixelGrabber(img, 0, 0, width, height, backgroundImagePixels, 0, width);
pGrab.grabPixels();
while ((pGrab.getStatus() & ImageObserver.ALLBITS) == 0) {
System.out.println("waiting for image to load");
Thread.sleep(500);
}
} else {
java.util.Arrays.fill(backgroundImagePixels, background);
}
backgroundPixels = backgroundImagePixels.clone();
} catch (InterruptedException e){
System.out.println("Interrupted");
}
}
/**
* Convenience method to load specs with no prefix
* @param a Object to be configured using the specs
* @param type Type of specs to be loaded
* @return boolean Success or Failure
*/
private static boolean loadSpecs(Object a, String type){
return loadSpecs(a, type, "");
}
/**
* Load specs for a given type, using the specified prefix
* @param a Object to be configured using the specs
* @param type Type of specs to be loaded
* @param prefix Prefix to apply in front of the type when searching for the specs
* @return boolean Success or Failure
*/
private static boolean loadSpecs(Object a, String type, String prefix){
PropertyResourceBundle b = bundles.get(type);
if (b == null)
try {
String bundle = prefix + type;
b = (PropertyResourceBundle) PropertyResourceBundle.getBundle(bundle);
bundles.put(type, b);
} catch (Exception e) {
e.printStackTrace();
return false;
}
Enumeration<String> e = b.getKeys();
while(e.hasMoreElements()){
String key = e.nextElement();
//System.out.println(key + "=" + b.getString(key));
try {
Field f = a.getClass().getField(key);
String fType = f.getType().toString();
if (key.equals("colour"))
f.setInt(a, Integer.parseInt(b.getString(key), 16) | 0xff000000);
else if (fType.equals("int"))
f.setInt(a, Integer.parseInt(b.getString(key)));
else if (fType.equals("long"))
f.setLong(a, Long.parseLong(b.getString(key)));
else if (fType.equals("double"))
f.setDouble(a, Double.parseDouble(b.getString(key)));
else if (fType.equals("float"))
f.setFloat(a, Float.parseFloat(b.getString(key)));
else if (fType.equals("class [I"))
f.set(a, parseIntArray(b.getString(key)));
else if (fType.equals("class [LEvolution$Action;"))
f.set(a, parseActionArray(b.getString(key)));
else if (fType.equals("boolean"))
f.setBoolean(a, Boolean.parseBoolean(b.getString(key)));
else {
f.set(a, b.getString(key));
}
} catch (Exception ia){
ia.printStackTrace();
}
}
return true;
}
/**
* Parse a comma delimited string into an integer array
* @param array Comma delimited string
* @return int[] Array of ints
*/
private static int[] parseIntArray(String array){
int[] returnArray = new int[spriteWidth * spriteHeight];
String[] strArray = array.split("\\s*,\\s*");
if (strArray.length == 1) return returnArray;
for (int i=0; i<returnArray.length && i<strArray.length; i++){
returnArray[i] = Integer.parseInt(strArray[i]);
}
return returnArray;
}
/**
* Parse a comma delimited string into an Action array
* @param array Comma delimited string
* @return Action[] Array of Actions
*/
private static Action[] parseActionArray(String array){
Action[] returnArray = new Action[ReactionEvent.values().length];
String[] strArray = array.split("\\s*,\\s*");
if (strArray.length == 1) return returnArray;
for (int i=0; i<returnArray.length && i<strArray.length; i++){
returnArray[i] = Action.valueOf(strArray[i].trim());
}
return returnArray;
}
/**
* Load the hash of named colours from the Colours resource bundle
*/
private static void loadColours(){
if (colours == null) {
try{
colours = new Hashtable<String,String>();
PropertyResourceBundle b = (PropertyResourceBundle) PropertyResourceBundle.getBundle("colours");
Enumeration<String> e = b.getKeys();
while(e.hasMoreElements()){
String key = e.nextElement();
colours.put(key, b.getString(key));
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
/**
* Load the hazards from the Hazards initialisation file
*/
private static void loadHazards(){
//Only run this if a file was specified
if (hazardsFile.length() > 0){
try{
// Get the file from the package resources
File file = new File(Evolution.class.getResource(hazardsFile).toURI());
// Buffered reader so we can parse the file line by line
BufferedReader br = new BufferedReader(new FileReader(file));
// Some local variables to help with parsing
String line = null;
String[] oneHazard = null;
Hazard h = null;
HazardType type = null;
Double strength = null;
Integer range = null;
Double direction = null;
int hazardsToSpawn = 0;
Double xPosition = null;
Double yPosition = null;
// For every line in the file
while((line=br.readLine())!=null){
// If the line has characters, and isn't a comment line, and isn't all blank
if (line.length() > 0
&& line.charAt(0) != '#'
&& !line.trim().equals("")) {
// Split the line into an array of strings
oneHazard = line.split("\\s*:\\s*");
// Default the number of hazards to spawn to 1
hazardsToSpawn = 1;
// Get the type of hazard
type = HazardType.valueOf(oneHazard[0]);
// All other descriptors are optional; if supplied, parse them from the string array
if (oneHazard.length > 1 && !oneHazard[1].trim().equals("")) strength = new Double(oneHazard[1]);
if (oneHazard.length > 2 && !oneHazard[2].trim().equals("")) range = new Integer(oneHazard[2]);
if (oneHazard.length > 3 && !oneHazard[3].trim().equals("")) direction = new Double(oneHazard[3]);
if (oneHazard.length > 4 && !oneHazard[4].trim().equals("")) hazardsToSpawn = new Integer(oneHazard[4]);;
if (oneHazard.length > 5 && !oneHazard[5].trim().equals("")) xPosition = new Double(oneHazard[5]);
if (oneHazard.length > 6 && !oneHazard[6].trim().equals("")) yPosition = new Double(oneHazard[6]);
// Create as many hazards as requested, using the supplied descriptors
for (int i=0; i<hazardsToSpawn; i++){
// Create the new hazard
h = frame.new Hazard(type);
h.initialize(xPosition, yPosition, type, strength, direction, range);
// Draw it onto the buffer
h.drawSprite();
// Add it to the list of hazards
hazards.add(h);
}
System.out.println("Hazard:" + type.name() + ", Number:" + hazardsToSpawn);
}
}
// Close the buffered reader
br.close();
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
/**
* Spawn up the initial set of animals
*/
private static void doInitialSpawn(){
loadColours();
String[] allSpecies = initialSpeciesLoad.split("\\s*,\\s*");
String[] oneSpecies;
int animalsToSpawn;
Animal a;
ArrayList<String> species = new ArrayList<String>();
for (String s: allSpecies){
oneSpecies = s.split("\\s*:\\s*");
if (oneSpecies.length == 2){
String name = oneSpecies[0];
int offset = 0;
String offsetChar = "";
String defaultColourName = "";
int defaultColour;
// Add a digit to the species name for each additional time the same species is included
while (species.contains(name + offsetChar)){
offset++;
offsetChar = Integer.toString(offset);
}
species.add(name + offsetChar);
// if a colour list is available, grab a default colour from that list; if not, just pick a random rgb value.
if (colours != null){
defaultColourName = (String)(colours.keySet().toArray()[randomGen.nextInt(colours.size())]);
defaultColour = Integer.parseInt(colours.get(defaultColourName),16);
} else {
defaultColour = (int)(randomGen.nextDouble() * 0xFFFFFF);
}
defaultColour = defaultColour | 0xff000000;
animalsToSpawn = Integer.parseInt(oneSpecies[1]);
for (int i=0; i<animalsToSpawn; i++){
a = frame.new Animal();
a.colour = defaultColour;
loadSpecs(a, oneSpecies[0], speciesPath);
a.species += offsetChar;
a.bundle = name;
animals.add(a);
// Stats are calculated against the species name from the bundle, not from the bundle name itself,
// so have to load the specs first and only then can the stats be initialised.
if (!speciesStats.containsKey(a.species)) speciesStats.put(a.species, new Hashtable<StatisticType,Integer>());
adjustStat(a.species, StatisticType.SpeciesCount, 1);
// If colour was overridden, change the reported values
if (a.colour != defaultColour) {
defaultColourName = "";
defaultColour = a.colour;
}
}
System.out.println("Species:" + name + offsetChar + ", Number:" + animalsToSpawn + ", Colour:" + defaultColourName + "(" + Integer.toHexString(defaultColour) + ")");
}
}
System.out.println(species);
}
/**
* Load up the inital set of hazards.
* These can be specified in the main properties file, or the hazards init file.
*/
private static void doInitialHazards(){
// Load the sprites for the hazards
loadHazardSprites();
// If no simple list of hazards was specified, try to load them from the hazards init file
if (initialHazardsLoad.length() == 0 || initialHazardsLoad.trim().equals("")) {
loadHazards();
} else {
// Otherwise, hazards were specified in the main properties file
// Split the hazards list
String[] allHazards = initialHazardsLoad.split("\\s*,\\s*");
// Some local variables to help with loading hazards.
String[] oneHazard;
int hazardsToSpawn;
Hazard h;
// For as many hazard specifications as were in the list...
for (String s: allHazards){
// Split the spec into name and number
oneHazard = s.split("\\s*:\\s*");
// Obviously, only continue if the spec has the correct number of arguments
if (oneHazard.length == 2){
// Get the hazard name (HazardType)
String name = oneHazard[0];
// Parse the number of instances to spawn
hazardsToSpawn = Integer.parseInt(oneHazard[1]);
// Spawn up that many hazards of that type
for (int i=0; i<hazardsToSpawn; i++){
h = frame.new Hazard(HazardType.valueOf(name));
h.drawSprite();
hazards.add(h);
}
System.out.println("Hazard:" + name + ", Number:" + hazardsToSpawn);
}
}
}
}
/**
* Adjust a statistic against a given species
* @param species Species affected
* @param stat Statistic to update
* @param value Amount to modify the stat by
*/
private static void adjustStat(String species, StatisticType stat, int value) {
if (reportCycles > 0) {
// Increment the count in the statistics
Integer count = speciesStats.get(species).get(StatisticType.SpeciesCount);
if (count == null)
speciesStats.get(species).put(StatisticType.SpeciesCount, new Integer(value));
else
speciesStats.get(species).put(StatisticType.SpeciesCount, count + value);
}
}
/**
* A simple method to count the number of species that have at least one living member.
* @return int, number of distinct species alive
*/
private static int activeSpecies(){
Hashtable<StatisticType, Integer> stats;
int returnval = 0;
Enumeration<Hashtable<StatisticType,Integer>> e = speciesStats.elements();
while(e.hasMoreElements()){
stats = e.nextElement();
if (stats.get(StatisticType.SpeciesCount) > 0) returnval++;
}
return returnval;
}
/**
* Print the statistics to the standard output.
* @param fps Frames per second, which is calculated as part of the calling method
*/
private static void printStats(double fps){
StringBuffer sb = new StringBuffer();
sb.append("Iteration: " + currentFrame + ", FPS: " + String.format("%.2f", fps) + ", Plants: " + plants.size() + ", Animals: " + animals.size());
Enumeration<String> e = speciesStats.keys();
while(e.hasMoreElements()){
String key = e.nextElement();
int count = speciesStats.get(key).get(StatisticType.SpeciesCount);
if(count > 0) sb.append(", " + key + ": " + count);
}
System.out.println(sb);
}
/**
* Load the sprites for the various Hazard Types.
*/
private static void loadHazardSprites(){
// Get a list of the hazard types from the enumeration
HazardType[] types = HazardType.values();
// For each hazard type, locate the sprite with that name, and load it.
for(HazardType type : types){
try {
System.out.println("Now loading sprite for " + type);
// Get the sprite
int[] sprite = new int[spriteHeight * spriteWidth];
URL url = Evolution.class.getResource("/hazard_sprites/" + type + ".bmp");
BufferedImage bi = ImageIO.read(url);
bi.getRGB(0, 0, spriteWidth, spriteHeight, sprite, 0, spriteWidth);
hazardSprites.put(type, sprite);
} catch (Exception e){
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
/**
* Writes out the screen to file system as a PNG
* @param buff The screen buffer to write
*/
private static void writeBuffer(BufferedImage buff) {
// Only write a screenshot if we're on a new frame
if (currentFrame > lastScreenshotFrame) {
// Handle for the buffered image to use
BufferedImage image;
// If we're not currently painting to the screen, at least update the pixel buffer
if (!doPaint) buff.setRGB(0, 0, width, height, pixels, 0, width);
// Create the directory if it doesn't exist
if (!new File(screenshotDirectory).exists())
new File(screenshotDirectory).mkdirs();
// Track the screenshot number
lastScreenshotNum++;
lastScreenshotFrame = currentFrame;
// Generate the file object
File outputfile = new File(screenshotDirectory + File.separator
+ gameSeed + "_" + String.format("%08d", lastScreenshotNum) + "." + screenshotType);
// While the file exists, increment the file number and generate a new file string
while (outputfile.exists()){
System.out.println(outputfile.getPath() + " exists...");
lastScreenshotNum++;
outputfile = new File(screenshotDirectory + File.separator
+ gameSeed + "_" + String.format("%08d", lastScreenshotNum) + "." + screenshotType);
}
// This is the new file's name
System.out.println(outputfile.getPath());
// Write out the file
try {
// Get a writer for the specified type of image
ImageWriter writer = null;
Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(screenshotType);
if (iter.hasNext()) {
writer = (ImageWriter)iter.next();
} else {
throw new IOException("Unsupported image format: " + screenshotType);
}
// If this is a JPG, convert colour model from ARGB to just RGB
if (screenshotType.equalsIgnoreCase("jpg")) {
BufferedImage raw_image = buff;
image = new BufferedImage(raw_image.getWidth(), raw_image.getHeight(), BufferedImage.TYPE_INT_RGB);
ColorConvertOp xformOp = new ColorConvertOp(null);
xformOp.filter(raw_image, image);
} else {
image = buff;
}
// Set up the output stream
ImageOutputStream ios = ImageIO.createImageOutputStream(outputfile);
writer.setOutput(ios);
// Create a parameter object for use with compression
ImageWriteParam iwparam = writer.getDefaultWriteParam();
// Try to set compression (will throw exception if compression isn't supported)
try {
iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT) ;
iwparam.setCompressionQuality(screenshotCompressionRatio);
} catch (UnsupportedOperationException e) { }
// Write the image to a file
writer.write(null, new IIOImage(image , null, null), iwparam);
// Clean up the stream and writer
ios.flush();
ios.close();
writer.dispose();
} catch (IOException e) {
System.out.println("Error writing screen capture to file: " + outputfile.getPath()
+ "\n" + e.getMessage());
}
}
}
/**
* Draws the splash screen, with the menu of commands.
*/
private static void drawSplash(){
int line = 150;
gr.setFont(new Font("Sans-Serif", Font.BOLD, 30));
gr.setColor(Color.WHITE);
gr.drawImage(buff, 0, titlebar, new Color(background), null);
gr.drawString(title + " (space to start)", 80, line=line + lineHeight);
gr.setFont(new Font("Sans-Serif", Font.PLAIN, 12));
if (headless) gr.drawString("<running in headless mode, screen will not repaint>", 80, line=line + lineHeight);
gr.drawString("space: start/pause/unpause", 80, line=line+ lineHeight);
gr.drawString("escape: exit", 80, line=line+ lineHeight);
gr.drawString("left-click: select animal(s) to display stats (only while paused)", 80, line=line+ lineHeight);
gr.drawString("up/down arrows: scroll amongst animals (on stats screen)", 80, line=line+ lineHeight);
gr.drawString("right-click: exit stats screen", 80, line=line+ lineHeight);
gr.drawString("mousewheel: roll up to increase game speed, down to decrease", 80, line=line+ lineHeight);
gr.drawString("Minimize the window to increase game speed (up to 35x increase)", 80, line=line+ lineHeight);
gr.drawString("Press \"C\" to take a screenshot", 80, line=line+ lineHeight);
gr.drawString("Press \"L\" to display stats for all animals (only while paused)", 80, line=line+ lineHeight);
gr.drawString("Press \"P\" (and optionally hold it) to drop a plant", 80, line=line+ lineHeight);
gr.drawString("Press \"R\" to redraw the entire screen (useful if the image is skewed)", 80, line=line+ lineHeight);
gr.drawString("Press \"W\" to stop/start wiping sprite's previous locations", 80, line=line+ lineHeight);
gr.drawString("Press \"0\" - \"9\" (and optionally hold it) to drop a hazard", 80, line=line+ lineHeight);
}
/**
* Constructor
* @param str
*/
public Evolution(String str) {
super(str);
}
/**
* Main code block.
* Commandline args are all optional:
* - 0: primary game specs (default: evolution.properties)
* - 1: headless mode indicator (must include word "headless")
* - 2: game seed, seeds random number generator (allows for exact replays)
* - 3: path to hazards file
* @param args
*/
public static void main(String[] args) throws Throwable {
frame = new Evolution(title);
// Load the initial specs, default to looking for "evolution"
if (!(args.length > 0 && loadSpecs(frame, args[0])))
if (!loadSpecs(frame, "evolution"))
throw new Exception("Evolution specs not found!");
if (args.length > 1 && args[1].contains("headless")) headless = true;
if (args.length > 2) {
try{
int temp = Integer.parseInt(args[2]);
if (temp != 0) gameSeed = temp;
} catch (Exception e) {
System.out.println("Couldn't parse game seed from command line!");
System.out.println(e.getStackTrace());
}
}
if (args.length > 3){
hazardsFile = args[3];
}
if (headless){
doPaint = false;
sleepTime = 0;
frame.setState(JFrame.ICONIFIED);
}
// Random number generator for randomized game decisions
randomGen = new Random(gameSeed);
// JFrame setup
frame.setSize(width, height + titlebar);
frame.setVisible(true);
frame.enableEvents(
KeyEvent.KEY_EVENT_MASK |
MouseEvent.MOUSE_EVENT_MASK |
MouseEvent.MOUSE_MOTION_EVENT_MASK |
MouseEvent.MOUSE_WHEEL_EVENT_MASK |
ComponentEvent.COMPONENT_EVENT_MASK);
gr = frame.getGraphics();
// Set up the pixel buffers (for layering and animation)
setUpBuffers();
pixels = backgroundPixels.clone();
paint(buff, gr);
// Draw the splash screen and instructions
drawSplash();
// If we're not in Headless mode, wait for the game to be started or exited
if (!headless) paused = true;
while (paused && !escape){
Thread.sleep(250);
// If the window was resized, resize the game grid
if (doResize) {
resizePixels();
drawSplash();
}
}
// If a timeout period is specified, offset it by the current time
long lastLoopTime = System.currentTimeMillis();
if (tournamentTimeout > 0) tournamentTimeout += lastLoopTime;
try{
// Populate the initial hazards
doInitialHazards();
// Populate the initial plants
for (int i = 0; i < initialPlants; i++){
Plant p = frame.new Plant();
p.drawSprite();
plants.add(p);
}
// Populate the initial animals
doInitialSpawn();
//calculateStats();
printStats(0);
// Paint the screen
if (doPaint) paint(buff, gr);
// local variables for loop processing
Animal animal1;
Plant plant1;
// Main loop
while (!escape && animals.size() > 0){
// Tack on the plants just dropped
plants.addAll(droppedPlants);
droppedPlants = new Vector<Plant>();
// Tack on the hazards just dropped
hazards.addAll(droppedHazards);
droppedHazards = new Vector<Hazard>();
// Process all animals (don't use a foreach loop, in case the animal dies from starvation)
for (int i=0; i<animals.size(); i++) {
animal1 = animals.get(i);
animal1.live();
}
// Process all animals (don't use a foreach loop, in case the plant dies or is eaten)
for (int i=0; i<plants.size(); i++) {
plant1 = plants.get(i);
plant1.live();
}
// Kill animals (don't use a foreach loop, in case the animal is killed)
for (int i=0; i<animals.size(); i++){
animals.get(i).calculateKill();
}
// Calculate Reactions
for (Animal a: animals){
a.calculateReaction();
}
// Spawn animals
for (int i=0; i<animals.size(); i++){
animal1 = animals.get(i);
if (animal1.life > animal1.spawnLife)
animal1.spawn();
}
// Spawn plants
for (int i=0; i<plantSpawnRate; i++){
Plant p = frame.new Plant();
p.drawSprite();
plants.add(p);
}
// If the window was resized, resize the game grid
if (doResize) resizePixels();
// If the results are being displayed, paint them
if (doPaint) paint(buff, gr);
// Calculate the animal statistics
//calculateStats();
// If this loop is one of the report cycles, print out the stats (with a FPS calculation)
if (reportCycles > 0){
if (++currentFrame % reportCycles == 0) {
printStats((reportCycles * 1000.0) / (System.currentTimeMillis() - lastLoopTime + 1));
// Update the last loop time variable
lastLoopTime = System.currentTimeMillis();
}
}
// Scale down the game speed
Thread.sleep(sleepTime);
// Loop while the game is paused and not exited
while (paused && !escape){
// If the Stats screen should be shown, and we're painting the screen, then do both
if (showClicked && doPaint){
showStats(buffStats, gr);
paint(buff, gr);
} else {
// Otherwise, just set the game title to indicate the game is paused
frame.setTitle(title + " (paused: space to unpause)");
}
// The game is paused, so only loop 4 times a second
Thread.sleep(250);
// If the window was resized, resize the game grid
if (doResize) {
resizePixels();
paint(buff, gr);
}
// Reset the game title
frame.setTitle(title);
}
// Tournament termination checks
if (tournamentMode){
if ((tournamentCycles > 0 && currentFrame >= tournamentCycles)
|| (tournamentTimeout > 0 && lastLoopTime > tournamentTimeout)
|| (tournamentOneWinner && activeSpecies() < 2)) {
escape = true;
printStats(0);
if (tournamentFinalScreenshot) writeBuffer(buff);
}
}
if (!escape && (screenshotInterval > 0) && (currentFrame % screenshotInterval == 0)) {
writeBuffer(buff);
}
}
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
frame.dispose();
System.exit(0);
}
}
}