// JavaScript Cellular Automata Simulation
// Copyright (c) 1999 Kelly Yancey <kbyanc@posi.net>
// All rights reserved.
//
// Redistribution and use in source form, with or without modification, is
// permitted provided that the following conditions are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer,
//    without modification, immediately at the beginning of the file.
// 2. All advertising materials mentioning features or use of this software
//    must display the following acknowledgement:
//      This product includes software developed by Kelly Yancey     
//    and a reference to the URL http://www.posi.net/software/automata/
// 3. The name of the author may not be used to endorse or promote products
//    derived from this software without specific prior written permission.
//    
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  
// IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT  
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


// size of the pictures used to represent cells (all pictures must be the same size)
var	cellwidth = 20;
var	cellheight = 20;

// default size of the 'board'
var	width = 18;
var	height = 13;

// the delay between simulation cycles (in milliseconds)
var	cycledelay = 500;

// default values game variables
var	breedlevel = 2;			// number of neighbors required to multiply
var	starvelevel = 5;		// minimum number of neighbors before cell starves
var	breedage = 5;			// minimum age before a cell can mate
var	maxage = 20;			// how long a cell can live
var	maxchildren = 10;		// maximum number of children any one cell can spawn
var	immigration = 5;		// percentage chance of cells immigrating into board

// user-supplied expression to evaluate at the end of each simulation cycle
var	cyclehook = "";

// some internal global variables
var	numgenerations = 0;
var	picture = new Array();
var	life = new Array(width);
var	numedgecells = (width * 2) + (height * 2) - 4;

// some stats gathered during the cycle
var	cyclecount = 0;
var	numlivingcells = 0;
var	populationage = 0;
var	numbirths = 0;
var	numdeaths = 0;
var	numimmigrants = 0;

// flag set to terminate the cycle
var	stopcycle = false;


var     lastlookup = 0;
function lookup(name) {
// routine to get image number by name
// optimized for the case where we lookup successive images
    for(i = lastlookup; i < document.images.length; i++)
	if(document.images[i].name == name) { lastlookup = i; return(i); }
    for(i = 0; i < lastlookup; i++)
	if(document.images[i].name == name) { lastlookup = i; return(i); }
    return(-1);
}


function cell(imagename) {
// object definition for 'cell' type
  this.imagenum = lookup(imagename);
  this.age = 0;
  this.neighbors = 0;
  this.children = maxchildren;
  this.mated = false;
}

function SetCellImage(x, y, src) {
// routine to set the image associated with the given cell
    if(document.images[life[x][y].imagenum].src != src)
	// we explicitely check to see if we are changing the image source to prevent
	// unnecissary redrawing of images which do not change
	document.images[life[x][y].imagenum].src = src;
}

function CountNeighbors(x, y) {
// routine to update the count per-cell of the cells adjacent to it
    var result = 0;
    var checkx;
    var checky;

    for(checkx = x - 1; checkx <= x + 1; checkx++)
	for(checky = y - 1; checky <= y + 1; checky++) {
	    if((checkx == x) && (checky == y)) continue;	// skip our own cell

	    // we can either ignore the edges of impose a penalty; we impose a penalty since
	    // logically there would be no food these to sustain cells there
	    if((checkx < 0) || (checkx >= width)) result++; 
	    else if((checky < 0) || (checky >= height)) result++;
	    else if(life[checkx][checky].age > 0) result++;
	}
    return(result);
}

function CanMate(x, y) {
// routine to determine if the given cell can mate
    return((x >= 0) && (y >= 0) &&
	   (x < width) && (y < height) &&
	   (life[x][y].children > 0) &&
	   (life[x][y].mated == false) &&
	   (life[x][y].age >= breedage) &&
	   (life[x][y].neighbors < starvelevel));
	// note: we could add 1 to the neighbors to represent the parent
	// avoiding having a child which will starve it
}

function CreateChild(x, y) {
// routine to create a child of the given cell
    var checkx;
    var checky;
    var mates_x = new Array(0);
    var mates_y = new Array(0);
    var lucky;

    // make sure we are in any condition to mate
    if(! CanMate(x, y)) return;

    // reset the mate coordinates arrays
    mates_x.length = 0;
    mates_y.length = 0;

    // first, we need to elect which neighbor to mate with
    for(checkx = x - 1; checkx <= x + 1; checkx++)
	for(checky = y - 1; checky <= y + 1; checky++) {
	    if((checkx == x) && (checky == y)) continue;	// can't mate with ourself

	    if(CanMate(checkx, checky)) {
		// add the mate's coordinates to the end of the list
		mates_x[mates_x.length] = checkx;
		mates_y[mates_y.length] = checky;
	    }
	}

    // double-check if there are any eligable mates
    if(mates_x.length == 0) return;

    // now, pick an eligable mate
    lucky = Math.floor(Math.random() * mates_x.length);
    if(lucky == mates_x.length) return;				// very rarely, we'll decide not to mate (only when random() returns 1)
    // fetch the coordinates for the lucky cell
    checkx = mates_x[lucky];	// X
    checky = mates_y[lucky];	// Y

    // flag the mate so it doesn't mate again
    life[checkx][checky].mated = true;
    life[x][y].mated = true;

    // update the birth count
    numbirths++;

    // now find an empty cell for the child
    if((checky <= y) && (y > 0)) {
	if(life[x    ][y - 1].age == 0) { life[x    ][y - 1].age = 1; return; }
	if(checkx <= x) if((x >         0) && (life[x - 1][y - 1].age == 0)) { life[x - 1][y - 1].age = 1; return; }
	if(checkx >= x) if((x < width - 1) && (life[x + 1][y - 1].age == 0)) { life[x + 1][y - 1].age = 1; return; }
    }
    if((checky >= y) && (y < height - 1)) {
	if(life[x    ][y + 1].age == 0) { life[x    ][y + 1].age = 1; return; }
	if(checkx <= x) if((x >         0) && (life[x - 1][y + 1].age == 0)) { life[x - 1][y + 1].age = 1; return; }
	if(checkx >= x) if((x < width - 1) && (life[x + 1][y + 1].age == 0)) { life[x + 1][y + 1].age = 1; return; }
    }
    if(checkx <= x) if((x >         0) && (life[x - 1][y    ].age == 0)) { life[x - 1][y    ].age = 1; return; }
    if(checkx >= x) if((x < width - 1) && (life[x + 1][y    ].age == 0)) { life[x + 1][y    ].age = 1; return; }

    // if we make it here, there is nowhere to put the child
    numbirths--;						// correct the birth count
}

function AgeCell(x, y) {
// routine to age the given cell
    life[x][y].age++;						// update the age of the cell
    if(life[x][y].age > maxage) { 				// died of old age
	numdeaths++;		// update the death count
	life[x][y].age = 0;	// reset the cell age (0 is empty)
    }
    else if(CanMate(x, y)) CreateChild(x, y);			// maybe a baby?
}

function PutCell(x, y) {
// routine trigger by mouse event to place a cell on the board
    var	t;

    AgeCell(x, y);
    if(life[x][y].age == 0) t = 0;
    else t = Math.ceil(life[x][y].age / maxage * numgenerations);
    SetCellImage(x, y, picture[t].src);    
}

function UpdateCell(x, y) {
// routine to update the status of the given cell

    if(life[x][y].age == 0) {
	// cell is currently empty
	if((x == 0) || (y == 0) || (x == width - 1) || (y == height - 1)) {
	    // edge of board, do we have immigration?
	    if((Math.random() * 100 * numedgecells) < immigration) {
		numimmigrants++;
		life[x][y].age = 1;				 	// immigrant!
	    }
	}
    }
    else {
	if(life[x][y].neighbors >= starvelevel) {			// died of starvation
	    numdeaths++;	// update the death count
	    life[x][y].age = 0;	// reset the cell age (0 is empty)
	}
	else AgeCell(x, y);
    }
}


function Cycle() {
// routine to perform the life cycle
    var x;
    var y;
    var t;

    // reset our stat counters
    numbirths = 0;
    numdeaths = 0;
    numimmigrants = 0;
    numlivingcells = 0;
    populationage = 0;

    // update the cycle count
    cyclecount++;

    // first, count all of the neighbors per cell and reset their mated status for this cycle
    for(x = 0; x < width; x++)
	for(y = 0; y < height; y++) {
	    life[x][y].neighbors = CountNeighbors(x, y);
	    life[x][y].mated = false;
	}

    // now update the status of each cell
    for(x = 0; x < width; x++)
	for(y = 0; y < height; y++) {
	    UpdateCell(x, y);
	    if(life[x][y].age == 0) t = 0;
	    else t = Math.ceil(life[x][y].age / maxage * numgenerations);
	    SetCellImage(x, y, picture[t].src);
	    if(t > 0) {
		numlivingcells++;
		populationage += life[x][y].age;
	    }
	}

    if(cyclehook != "") eval(cyclehook);

    if(! stopcycle) {
      // schedule to be invoked again shortly
      setTimeout("Cycle()", cycledelay);
    }
}

function InitializeBoard() {
// routine to initialize the state of the life simulation (and draw the 'board')
    // build the life cell array
    for(x = 0; x < width; x++)
	life[x] = new Array (height);

    for(y = 0; y < height; y++) {
	document.write('<NOBR>');
	for(x = 0; x < width; x++) {
	    document.write('<A HREF="javascript:void(0);" onclick="PutCell(' + x + ', ' + y + ')"><IMG NAME="c[' + x + ',' + y + ']" SRC="' + picture[0].src + '" BORDER="0" HEIGHT="' + cellheight + '" WIDTH="' + cellwidth + '"></A>');
	    life[x][y] = new cell('c[' + x + ',' + y + ']');
	}

	document.writeln('</NOBR><BR>');
    }
}

function ClearBoard() {
// routine to reset all of the cells on the board
    for(x = 0; x < width; x++)
	for(y = 0; y < height; y++) {
	    life[x][y].age = 0;
	    SetCellImage(x, y, picture[0].src);
	}
    cyclecount = 0;	// reset the number of simulation cycles
}

function RandomFillBoard() {
// routine to put some cell in random places on the board
    for(x = 0; x < width; x++)
	for(y = 0; y < height; y++)
	    if((life[x][y].age == 0) && (Math.floor(Math.random() * 10) == 0)) {
		life[x][y].age = Math.floor(Math.random() * (maxage - 1));
		SetCellImage(x ,y, picture[Math.ceil(life[x][y].age * numgenerations / maxage)].src);
	    }
}

function LoadCellImage(generation, pictureURL) {
// routine to associated the given picture with a cell of the given generation (age)
    picture[generation] = new Image();
    picture[generation].src = pictureURL;
    numgenerations = generation;
}


// you must load at least 2 cell images: one for generation 0 (ie dead) and one for generation 1; more just makes
// life more interesting (no pun intended).

// LoadCellImage(0, "blank.gif");
// LoadCellImage(1, "g1.gif");
// LoadCellImage(2, "g2.gif");
// LoadCellImage(3, "g3.gif");
// LoadCellImage(4, "g4.gif");
// LoadCellImage(5, "g5.gif");
// LoadCellImage(6, "g6.gif");
// LoadCellImage(7, "g7.gif");

// initialize the state of the board
// InitializeBoard();

// Cycle();



// ************************************************

function DisplayVariables(where) {
// routine to update the given form to display the simulation variables
    where.breedlevel.value = breedlevel;
    where.starvelevel.value = starvelevel;
    where.breedage.value = breedage;
    where.maxage.value = maxage;
    where.maxchildren.value = maxchildren;
    where.immigration.value = immigration;
}

function SetVariables(where) {
// routine to update the simulation variables from the values in the given form
    starvelevel = Math.max(Math.min(where.starvelevel.value, 8), 1);	// cannot have more than 8 neighbors
    breedlevel = Math.min(where.breedlevel.value, starvelevel - 1);	// cannot require more neighbors to breed than to starve!
    maxage = Math.max(where.maxage.value, 1);				// require all cells to live at least 1 cycle
    breedage = Math.min(where.breedage.value, maxage);			// at least give the cells a chance to breed
    maxchildren = Math.max(where.maxchildren.value, 1);			// all cells can have at least 1 child
    immigration = where.immigration.value;

    DisplayVariables(where);
}

// ************************************************
