Newer
Older
ecologia / src / model / Animal.java
package model;

import java.util.HashMap;
import java.util.Random;
import java.util.ArrayList;

import main.EcologiaIO;
import controller.OccupantType;
import controller.World;

/**
 * This is the superclass of all animal classes. It holds common methods
 * needed by all animals.
 * 
 * @author Daniel Vedder
 * @version 30.8.2014
 */
public abstract class Animal 
{
	/*
	 * XXX Set ID numbers as long?
	 * Quick calculation: in a 100x100 world, an integer ID
	 * (max value: 2**31) should last for >40,000,000 updates
	 * (based on a 1000 update test run).
	 * => long IDs are not very urgent...
	 * --> CHECK THIS AGAIN
	 */
	protected int IDnumber; //A unique identifier for this animal
	protected int parent; //The ID number of the parent
	protected Genome genome;
	protected int generation;
	protected int offspring;
	protected OccupantType type;
	protected int x, y; //The animal's position
	protected int age;
	protected int energy;
	
	protected int movesThisTurn;
	protected int attemptedMovesThisTurn;
	protected int exhaustion;
	protected int gestationPeriod;
	protected boolean isAlive;
	protected Random random;
	
	/**
	 * The constructor.
	 * @param setID
	 * @param myType
	 * @param newGenome
	 * @param myGeneration
	 * @param setX
	 * @param setY
	 * @param setEnergy
	 * @param parentID
	 */
	public Animal(int setID, OccupantType myType, Genome newGenome,
				  int myGeneration, int setX, int setY, int setEnergy,
				  int parentID)
	{
		IDnumber = setID;
		genome = newGenome;
		generation = myGeneration;
		offspring = 0;
		type = myType;
		x = setX;
		y = setY;
		energy = setEnergy;
		parent = parentID;
		movesThisTurn = 0;
		attemptedMovesThisTurn = 0;
		exhaustion = 0;
		gestationPeriod = genome.getGestation();
		isAlive = true;
		random = new Random();
		Simulator.getField(x, y).setOccupant(type);
		EcologiaIO.analysis("Created "+type.toString()+" with ID="+IDnumber+
							" parent="+parent+" generation="+generation+
							" update="+World.getInstance().getTurn());
		String genStr = genome.asHashMap().toString();
		EcologiaIO.analysis("Genome of animal "+IDnumber+": "+
							genStr.substring(1, genStr.length()-1));
	}
	
	/*
	 * --- Generic methods needed by all animals ---
	 */
	
	/**
	 * This method has to be called by every species.
	 */
	public void update()
	{
		age++;
		movesThisTurn = 0;
		attemptedMovesThisTurn = 0;
		if (exhaustion > 0) exhaustion--;
		if (gestationPeriod > 0) gestationPeriod--;
		if (age >= genome.getAgeLimit()) {
			isAlive = false;
			World.getInstance().giveNews("A "+type.toString()+" has died!");
			Simulator.removeAnimal(x, y, type);
			return;
		}
		changeEnergy(-1);
		if (!isAlive) return;
		else if (age >= genome.getMaturityAge() && gestationPeriod == 0
				 && energy >= genome.getReproductiveEnergy()
				 && random.nextInt(3) == 0) {
			reproduce();
		}
	}
	
	/**
	 * The animal reproduces, setting down a child on a neighbouring square
	 */
	public void reproduce() 
	{
		int r = genome.getReproductionRate();
		for (int i = 0; i < r; i++) {
			int[] childField = getNeighbouringField(Direction.randomDirection());
			int ttl = 10; //Make sure we don't end up in an endless loop
			while (childField == null || World.getInstance().getFieldInfo(childField[0], childField[1]).get("Occupant") != OccupantType.NONE.toInt()) {
				if (ttl == 0) return; //If we still haven't found a space, break off
				childField = getNeighbouringField(Direction.randomDirection());
				ttl--;
			}
			int childEnergy = energy/(r+1);
			if (type == OccupantType.HERBIVORE) {
				Herbivore child = new Herbivore(World.getInstance().getNextID(), 
												new Genome(genome), generation+1, childField[0], 
												childField[1], childEnergy, IDnumber);
				Simulator.addAnimal(child);
			}
			else if (type == OccupantType.CARNIVORE) {
				Carnivore child = new Carnivore(World.getInstance().getNextID(), 
												new Genome(genome), generation+1, childField[0], 
												childField[1], childEnergy, IDnumber);
				Simulator.addAnimal(child);
			}
			offspring++;
		}
		changeEnergy(-energy/(r+1));
		gestationPeriod = genome.getGestation();
		World.getInstance().incGeneration(generation+1);
		World.getInstance().giveNews("A new "+type.toString()+" has been born!"); //XXX Comment this out?
	}
	
	/**
	 * The animal moves in the specified direction.
	 * @return success
	 */
	public boolean move(Direction dir)
	{
		/*
		 * Fix the Spring frost bug (very random freezing):
		 * If there have been more than 12 attempted (and failed) moves this turn, 
		 * e.g. due to the animal being surrounded, we are probably in an endless 
		 * loop and need to break out.
		 * 
		 * Also fix the Ghost bug: we cannot guarantee that the animal is
		 * alive at this point, so let's make sure to check.
		 */
		if (attemptedMovesThisTurn > 12 || !isAlive) {
			movesThisTurn++;
			return true; 
		}
		
		boolean success = true;
		int[] nextPos = getNeighbouringField(dir);
		//Check if the square to move to is valid
		if (nextPos == null || movesThisTurn >= genome.getSpeed() || exhaustion > genome.getStamina() ||
				OccupantType.fromInt(World.getInstance().getFieldInfo(nextPos[0], nextPos[1]).get("Occupant")) != OccupantType.NONE) {
			success = false;
			attemptedMovesThisTurn++;
		}
		
		//Execute the move
		if (success) {
			Simulator.getField(x, y).setOccupant(OccupantType.NONE);
			Simulator.getField(nextPos[0], nextPos[1]).setOccupant(type);
			x = nextPos[0];
			y = nextPos[1];
			movesThisTurn++;
			exhaustion++;
			changeEnergy(-1);
		}
		return success;
	}

	/**
	 * Search for the inputed object within the line of sight.
	 */
	public int[] search(OccupantType type)
	{
		//return randomizedSearch(type);
		//return closestSearch(type);
		return mixedSearch(type);
	}
	
	/**
	 * Search for the inputed object within the line of sight.
	 * The returned coordinates are chosen at random from a list of eligible ones.
	 */
	public int[] randomizedSearch(OccupantType type)
	{
		ArrayList<int[]> targets = new ArrayList<int[]>();
		for (int xdist = x-genome.getSight(); xdist < x+genome.getSight(); xdist++) {
			for (int ydist = y-genome.getSight(); ydist < y+genome.getSight(); ydist++) {
				if (xdist >= 0 && ydist >= 0 && xdist < World.getInstance().getParam("xsize")
					&& ydist < World.getInstance().getParam("ysize")) {
					if (Simulator.getField(xdist, ydist).getOccupant() == type) {
						int[] newTarget = {xdist, ydist};
						targets.add(newTarget);
					}
				}
			}
		}
		if (targets.size() > 0) return targets.get(random.nextInt(targets.size()));
		else return null;
	}
	
	/**
	 * Search for the inputed object within the line of sight.
	 * Finds the object closest to the individual.
	 */
	public int[] closestSearch(OccupantType type)
	{
		int[] target = {-1, -1};
		int minDist = genome.getSight()+1;
		for (int xdist = x-genome.getSight(); xdist < x+genome.getSight(); xdist++) {
			for (int ydist = y-genome.getSight(); ydist < y+genome.getSight(); ydist++) {
				if (xdist >= 0 && ydist >= 0 && xdist < World.getInstance().getParam("xsize")
					&& ydist < World.getInstance().getParam("ysize")) {
					if (Simulator.getField(xdist, ydist).getOccupant() == type) {
						int distance = getDistance(xdist, ydist);
						if (distance != 0 && distance < minDist) {
							target[0] = xdist;
							target[1] = ydist;
							if (distance == 1) break; //Ain't gonna get any better...
						}
					}
				}
			}
		}
		if (target[0] == -1) return null;
		else return target;
	}

	/**
	 * Search for the inputed object within the line of sight.
	 * A random target is chosen out of a list of targets closest to the individual.
	 */
	public int[] mixedSearch(OccupantType type)
	{
		ArrayList<int[]> targets = new ArrayList<int[]>();
		int minDist = genome.getSight()+1;
		for (int xdist = x-genome.getSight(); xdist < x+genome.getSight(); xdist++) {
			for (int ydist = y-genome.getSight(); ydist < y+genome.getSight(); ydist++) {
				if (xdist >= 0 && ydist >= 0 && xdist < World.getInstance().getParam("xsize")
					&& ydist < World.getInstance().getParam("ysize")) {
					if (Simulator.getField(xdist, ydist).getOccupant() == type) {
						int distance = getDistance(xdist, ydist);
						int[] newTarget = {xdist, ydist};
						if (distance < minDist) {
							targets.clear();
							minDist = distance;
						}
						if (distance <= minDist) targets.add(newTarget);
					}
				}
			}
		}
		if (targets.isEmpty()) return null;
		else return targets.get(random.nextInt(targets.size()));
	}
	
	/**
	 * Calculate the neighbouring square in the specified direction
	 * (return null if out of bounds)
	 */
	public int[] getNeighbouringField(Direction dir)
	{
		int nextX = x;
		int nextY = y;
		switch (dir) {
			case UP: nextY--; break;
			case RIGHT: nextX++; break;
			case DOWN: nextY++; break;
			case LEFT: nextX--; break;
			case TOP_RIGHT: nextY--; nextX++; break;
			case BOTTOM_RIGHT: nextY++; nextX++; break;
			case BOTTOM_LEFT: nextY++; nextX--; break;
			case TOP_LEFT: nextY--; nextX--; break;
			default: EcologiaIO.error("Invalid direction passed to Animal.getNeighbouringField()! ("+dir+") by "+type.toString()+" @"+x+"/"+y); 
		}
		if (nextX < 0 || nextX >= World.getInstance().getParam("xsize") || 
			nextY < 0 || nextY >= World.getInstance().getParam("ysize")) {
			return null;
		}
		else {
			int[] square = {nextX, nextY};
			return square;
		}
	}
	
	/**
	 * In which direction are the given coordinates relative to this animal?
	 * @param xpos
	 * @param ypos
	 * @return Direction
	 */
	public Direction getDirection(int xpos, int ypos)
	{
		if (xpos == x && ypos > y) return Direction.DOWN;
		else if (xpos == x && ypos < y) return Direction.UP;
		else if (xpos > x && ypos == y) return Direction.RIGHT;
		else if (xpos < x && ypos == y) return Direction.LEFT;
		else if (xpos > x && ypos > y) return Direction.BOTTOM_RIGHT;
		else if (xpos < x && ypos > y) return Direction.BOTTOM_LEFT;
		else if (xpos > x && ypos < y) return Direction.TOP_RIGHT;
		else if (xpos < x && ypos < y) return Direction.TOP_LEFT;
		else return Direction.CENTER;
	}

	/**
	 * How many steps are needed to get to the specified position?
	 */
	public int getDistance (int xpos, int ypos)
	{
		int xdist = Math.abs(xpos - x);
		int ydist = Math.abs(ypos - y);
		return Math.max(xdist, ydist);
	}

	/*
	 * --- Getters ---
	 */
	
	/**
	 * Return a hash map containing all the information about this animal.
	 */
	public HashMap<String, Integer> getInfo()
	{
		HashMap<String, Integer> info = new HashMap<String, Integer>();
		
		//Lifetime variables
		info.put("ID", IDnumber);
		info.put("Type", type.toInt());
		info.put("X", x);
		info.put("Y", y);
		info.put("Age", age);
		info.put("Energy", energy);
		info.put("Generation", generation);
		info.put("Parent", parent);
		info.put("Offspring", offspring);
		
		//Genome variables
		//XXX This is redundant with Genome.asHashMap()
		info.put("Mutation rate", genome.getMutationRate());
		info.put("Speed", genome.getSpeed());
		info.put("Stamina", genome.getStamina());
		info.put("Sight", genome.getSight());
		info.put("Metabolism", genome.getMetabolism());
		info.put("Age limit", genome.getAgeLimit());
		info.put("Strength", genome.getStrength());
		info.put("Reproductive energy", genome.getReproductiveEnergy());
		info.put("Maturity age", genome.getMaturityAge());
		info.put("Gestation", genome.getGestation());
		info.put("Reproduction rate", genome.getReproductionRate());
		
		return info;
	}
	
	//XXX Deprecate other getters? [getInfo() available]
	
	public boolean isAlive()
	{
		return isAlive;
	}
	
	public long getID()
	{
		return IDnumber;
	}
	
	public Genome getGenome()
	{
		return genome;
	}

	public int getGeneration()
	{
		return generation;
	}
	
	public int getParent()
	{
		return parent;
	}

	public int getOffspring()
	{
		return offspring;
	}

	public OccupantType getType()
	{
		return type;
	}

	public int getX()
	{
		return x;
	}

	public int getY()
	{
		return y;
	}

	public int getAge()
	{
		return age;
	}
	
	public int getEnergy()
	{
		return energy;
	}
	
	/*
	 * A few setters that may be needed 
	 */
	
	/**
	 * Change the energy level of this animal. If it dips to <= 0, the animal
	 * dies and is removed. This is a convenience wrapper method around
	 * setEnergy().
	 * @param amount
	 */
	public void changeEnergy(int amount)
	{
		setEnergy(energy+amount);
	}
	
	public void setEnergy(int newEnergy)
	{
		energy = newEnergy;
		if (energy <= 0) {
			isAlive = false;
			World.getInstance().giveNews("A "+type.toString()+" has starved!");
			Simulator.removeAnimal(x, y, type);
		}
	}
	
	public void setAge(int newAge)
	{
		age = newAge;
	}
	
	public void setPosition(int newX, int newY)
	{
		x = newX;
		y = newY;
	}

	public void exhaust(int e)
	{
		exhaustion += e;
		if (exhaustion < 0) exhaustion = 0;
	}	
}