//------------------------------------------------------------------------------
// cosporen.jc
//------------------------------------------------------------------------------
//
// Taking the "Sporen" applet to the next level...
// This implementation uses a cofunction to run the simulation code in it's own
// cooperative thread. It also has some improvements to the Sporen simulation,
// such as mutation, development of new families, etc...
// To run this script, simply drag it onto the ScriptApp's icon.
//
//------------------------------------------------------------------------------

import stdlib;
import math;
import time;
import Screen;
import RectLayer;

using stdlib, math, time;

//------------------------------------------------------------------------------
// global variables and constants
//------------------------------------------------------------------------------

const int kMaxLevel            = 255;   // maximum energy a spore can have
const int kEnergyDec           = 3;     // defines how fast spore grows old (and dies)
const int kEatBonus            = 155;   // how much energy eating brings
const int kReproCost           = 15;    // how much energy reproduction costs
const int kEatLevel            = 92;    // level at which spore starts to eat
const int kReproLevel          = 181;   // level at which spore starts to reproduce
const int kCellSize            = 8;     // width and height of a cell in pixel
const int kViewWidth           = 640;   // width in pixel of the spore view
const int kViewHeight          = 480;   // height in pixel of the spore view
const int kRedrawSpeed         = 30;    // redraw interval in msec
const int kSimulationSpeed     = 30;    // simulation speed, 1 (slow) ~ 60 (fast)
const int kMutateSpeed         = 25;    // mutate every n generations
const int kUseSync             = true;  // if 'true' fullscreen will attempt to sync to display

//------------------------------------------------------------------------------
// forward declarations
//------------------------------------------------------------------------------

class World;
class Spore;
class Cell;
cofunction SporeLife(World);

//------------------------------------------------------------------------------
// class SporeApplet
//------------------------------------------------------------------------------

class SporeApplet : Applet
{
    method              SporeApplet (Screen scr);
    method              OnOpen      ();
    method              OnClose     ();
    method              OnKey       (int key);
    method              Idle        ();

    Screen              m_Screen;
    World               m_World;
    SporeLife           m_SporeLife;
    time                m_Timer;
}

//------------------------------------------------------------------------------
// class World
//------------------------------------------------------------------------------
// The World is a two dimensional array where the Spores live.
// This class uses the built-in array to create a two-dimensional array.

class World
{
    method              World       (int width, int height);
    method              PlaceSpore  (Point pos, Spore s);
    method              RemoveSpore (Point pos);
    method Spore        GetSpore    (Point pos);
    method              SetSpore    (Point pos, Spore s);
    method              Wrap        (Point pos);

    int                 dimX;
    int                 dimY;
    Cell[]              matrix;
}

//------------------------------------------------------------------------------
// class Spore
//------------------------------------------------------------------------------
// A spore has a fixed life-cycle: It gets born into a cell, eats one or more of it's neighbors,
// reproduces itself to into a free cell next to it, and eventually dies.

class Spore
{
    method              Spore       (const Color c, int s);
    method              Spore       (const Spore dad);
    method              Life        (World world);
    method              SpinToXY    (Point pt);

    int                 energy;     // decreases continually, if 0, it dies
    Point               pos;        // position in matrix
    Color               color;      // color of the spore
    int                 shape;      // "family" of the spore
    int                 gen;        // spore generation
    int                 spin;       // direction of the spore
    int                 ispin;      // spin increment
    int                 ieat;       // eat bonus
    int                 irepro;     // repro cost
}

//------------------------------------------------------------------------------
// class Cell
//------------------------------------------------------------------------------
// A simple data class that stores a reference to a Spore and a RectLayer, which
// is used to display the cell on the screen.

class Cell
{
    method              Cell        (Point pos);
    method              Update      ();

    Spore               spore;
    RectLayer           layer;
}

//------------------------------------------------------------------------------
// function CreateApplet
//------------------------------------------------------------------------------
// The Application calls this function to create our applet.
// We need to implement this function and return an instance of our applet.

function Applet CreateApplet(Screen screen)
{
    return new SporeApplet(screen);
}

//------------------------------------------------------------------------------
// cofunction SporeLife
//------------------------------------------------------------------------------
// We use a cofunction to simulate our spores in it's own cooperative thread.
// This thread is created by our SporeApplet class and resumed each time
// SporeApplet::Idle() is called.

cofunction SporeLife(World world)
{
    // infinitely run main loop
    for(;;)
    {
        // wait a little... for the dramatic effect
        for( int i = 0; i < 50; i++ )
            yield;
        // place four spores of different colors and shape as start population
        Point pos = new Point(world.dimX / 2, world.dimY / 2);
        world.PlaceSpore(pos, new Spore(new Color( 64, 64,255), 0));
        world.PlaceSpore(pos, new Spore(new Color(255, 64,64 ), 8000));
        world.PlaceSpore(pos, new Spore(new Color( 64,255,64 ), 16000));
        world.PlaceSpore(pos, new Spore(new Color(255,255,64 ), 24000));
        // wait again...
        for( int i = 0; i < 100; i++ )
            yield;
        int alive = true;
        int ycnt = 0;
        // go through all spores and simulate their life
        while( alive )
        {
            alive = false;
            for( int y = 0; y < world.dimY; y++ )
            {
                for( int x = 0; x < world.dimX; x++ )
                {
                    Cell cell = world.matrix[x, y];
                    Spore spore = cell.spore;
                    if( spore != null )
                    {
                        spore.Life( world );
                        cell.Update();
                        alive = true;
                    }
                }
                // yield every few rows
                ycnt++;
                if( ycnt >= kSimulationSpeed )
                {
                    yield;
                    ycnt = 0;
                }
            }
        }
    }
}

//------------------------------------------------------------------------------
// function Mutate
//------------------------------------------------------------------------------
// Darwin's little helper function.

function int Mutate(int val, int min, int max)
{
    int strength = (abs(min) + abs(max)) / 10 + 1;
    val += rand(-strength, strength);
    if( val < min )
        val = min;
    else if( val > max )
        val = max;
    return val;
}

//------------------------------------------------------------------------------
// class SporeApplet
//------------------------------------------------------------------------------
// constructor

method SporeApplet::SporeApplet(Screen screen)
{
    // store reference to screen
    m_Screen = screen;

    // set fixed size
    m_Screen.FixedSize( kViewWidth, kViewHeight );

    // set background color
    m_Screen.BackColor( new Color(0, 0, 0) );

    // try to make 60 fps
    m_Screen.SetDesiredFPS( 60 );

    // set window title
    m_Screen.Caption( "Sporen II - JewelScript demo applet" );

    // create our world
    m_World = new World( kViewWidth / kCellSize, kViewHeight / kCellSize );

    // init members
    m_Timer = new time();
    m_SporeLife = new SporeLife( m_World );

    // init random generator
    randInit();
}

//------------------------------------------------------------------------------
// OnOpen
//------------------------------------------------------------------------------
//

method SporeApplet::OnOpen()
{
    // add all Cell layers to our Screen object
    for( int y = 0; y < m_World.dimY; y++ )
    {
        for( int x = 0; x < m_World.dimX; x++ )
        {
            m_Screen.AddLayer( m_World.matrix[x, y].layer );
        }
    }
}

//------------------------------------------------------------------------------
// OnClose
//------------------------------------------------------------------------------
//

method SporeApplet::OnClose()
{
    m_Screen.RemoveAllLayers();
}

//------------------------------------------------------------------------------
// OnKey
//------------------------------------------------------------------------------
//

method SporeApplet::OnKey(int key)
{
    Point pos = new Point(rand(0, m_World.dimX - 1), rand(0, m_World.dimY - 1));
    switch( key )
    {
        case '1':
            m_World.PlaceSpore(pos, new Spore(new Color( 64, 64,255), 0));
            break;
        case '2':
            m_World.PlaceSpore(pos, new Spore(new Color(255, 64,64 ), 8000));
            break;
        case '3':
            m_World.PlaceSpore(pos, new Spore(new Color( 64,255,64 ), 16000));
            break;
        case '4':
            m_World.PlaceSpore(pos, new Spore(new Color(255,255,64 ), 24000));
            break;
        case kKeyF11:
            m_Screen.FullScreen(kViewWidth, kViewHeight, kUseSync);
            break;
    }
}

//------------------------------------------------------------------------------
// Idle
//------------------------------------------------------------------------------
//

method SporeApplet::Idle()
{
    // resume our thread
    m_SporeLife();

    // check if time to update screen
    if( m_Timer.isTick(kRedrawSpeed) )
    {
        m_Screen.Redraw();
    }
}

//------------------------------------------------------------------------------
// class World
//------------------------------------------------------------------------------
// Implementation

method World::World(int width, int height)
{
    dimX = width;
    dimY = height;
    matrix = new array(width, height);
    for( int y = 0; y < height; y++ )
    {
        for( int x = 0; x < width; x++ )
        {
            matrix[x, y] = new Cell(new Point(x, y));
        }
    }
}

//------------------------------------------------------------------------------
// PlaceSpore
//------------------------------------------------------------------------------
// Place a spore into a free cell around the given cell.

method World::PlaceSpore(Point pos, Spore s)
{
    int x = pos.x;
    int y = pos.y;
    if( matrix[x, y].spore == null )
    {
        SetSpore(pos, s);
    }
    else
    {
        int a = rand(0, 7);
        int b = a + 8;
        for( int c = a; c < b; c++ )
        {
            int d = c % 8;
            if( d >= 4 )
                d++;
            int nx = x + d % 3 - 1;
            int ny = y + d / 3 - 1;
            if( matrix[nx, ny].spore == null )
            {
                SetSpore(new Point(nx, ny), s);
                break;
            }
        }
    }
}

//------------------------------------------------------------------------------
// RemoveSpore
//------------------------------------------------------------------------------
// Remove a Spore from the world

method World::RemoveSpore(Point pos)
{
    Cell cell = matrix[pos.x, pos.y];
    cell.layer.Hide();
    cell.spore = null;
}

//------------------------------------------------------------------------------
// GetSpore
//------------------------------------------------------------------------------
// Check if a cell contains a Spore and return it

method Spore World::GetSpore(Point pos)
{
    return matrix[pos.x, pos.y].spore;
}

//------------------------------------------------------------------------------
// SetSpore
//------------------------------------------------------------------------------
// Set a Spore into specific cell in the world

method World::SetSpore(Point pos, Spore s)
{
    int x = pos.x;
    int y = pos.y;
    Cell cell = matrix[x, y];
    Color c = s.color;
    int e = s.energy;
    int r = (c.red * e) >> 8;
    int g = (c.green * e) >> 8;
    int b = (c.blue * e) >> 8;
    cell.layer.SetColor( new Color(r, g, b) );
    cell.layer.Show();
    cell.spore = s;
    s.pos = pos;
}

//------------------------------------------------------------------------------
// Wrap
//------------------------------------------------------------------------------
// Wrap around coords

method World::Wrap(Point pos)
{
    if( pos.x < 0 )
        pos.x += dimX;
    else if( pos.x >= dimX )
        pos.x -= dimX;
    if( pos.y < 0 )
        pos.y += dimY;
    else if( pos.y >= dimY )
        pos.y -= dimY;
}

//------------------------------------------------------------------------------
// class Cell
//------------------------------------------------------------------------------
// Constructor

method Cell::Cell(Point pos)
{
    spore = null;
    layer = new RectLayer();
    layer.ResizeTo( kCellSize - 1, kCellSize - 1 );
    layer.MoveTo( pos.x * kCellSize, pos.y * kCellSize );
}

//------------------------------------------------------------------------------
// Update
//------------------------------------------------------------------------------
//

method Cell::Update()
{
    if( spore != null )
    {
        int e = spore.energy;
        Color c = spore.color;
        int r = (c.red * e) >> 8;
        int g = (c.green * e) >> 8;
        int b = (c.blue * e) >> 8;
        layer.SetColor(new Color(r, g, b));
    }
}

//------------------------------------------------------------------------------
// class Spore
//------------------------------------------------------------------------------
// Constructor

method Spore::Spore(const Color c, int s)
{
    energy = kMaxLevel;
    pos = new Point();
    color = c;
    shape = s;
    gen = 0;
    spin = rand(0, 7);
    ispin = 1;
    ieat = kEatBonus;
    irepro = kReproCost;
}

//------------------------------------------------------------------------------
// class Spore
//------------------------------------------------------------------------------
// copy constructor

method Spore::Spore(const Spore dad)
{
    energy = dad.energy * 85 / 100; // 85% of dad's energy
    pos = new Point();
    color = new Color(dad.color);
    shape = dad.shape;
    gen = dad.gen + 1;
    spin = dad.spin;
    ispin = dad.ispin;
    ieat = dad.ieat;
    irepro = dad.irepro;
    // every few generations, mutate...
    if( gen >= kMutateSpeed )
    {
        gen = 0;
        ispin = Mutate(ispin, -3, 3);
        ieat = Mutate(ieat, 120, 150);
        irepro = Mutate(irepro, 10, 30);
        shape += rand(-1, 1);
        color.red = Mutate(color.red, 64, 255);
        color.green = Mutate(color.green, 64, 255);
        color.blue = Mutate(color.blue, 64, 255);
    }
}

//------------------------------------------------------------------------------
// Life
//------------------------------------------------------------------------------
// Simulate the Spores life

method Spore::Life(World world)
{
    int turn = true;
    Point spoPt;

    energy -= kEnergyDec;
    if( energy <= 0 )
    {
        // spore is dead, remove from world
        world.RemoveSpore( pos );
        return;
    }
    if( energy < kEatLevel )
    {
        // peek around to find something to eat...
        SpinToXY(spoPt);
        world.Wrap(spoPt);
        // get the spore from the cell
        Spore enemy = world.GetSpore(spoPt);
        // only eat if it's from other family
        if( enemy != null )
        {
            // found a meal?
            if( abs(enemy.shape - shape) >= 16 )
            {
                // yum!
                energy += enemy.ieat;
                if( energy > kMaxLevel )
                    energy = kMaxLevel;
                world.RemoveSpore(spoPt);
                turn = false;
            }
        }
    }
    else if( energy >= kReproLevel )
    {
        // peek around to find a free place
        SpinToXY(spoPt);
        world.Wrap(spoPt);
        // found a spot?
        if( world.GetSpore(spoPt) == null )
        {
            // place kid in cell
            world.PlaceSpore(spoPt, new Spore(this) );  // kid inherits from us
            energy -= irepro;
            if( energy < 0 )
                energy = 0;
            turn = false;
        }
    }
    if( turn )
    {
        spin += ispin;
        if( spin > 7 )
            spin -= 8;
        else if( spin < 0 )
            spin += 8;
    }
}

//------------------------------------------------------------------------------
// SpinToXY
//------------------------------------------------------------------------------
// Computes cell coords from the spin index

method Spore::SpinToXY(Point pt)
{
    int s = spin;
    if( s >= 4 )
        s++;
    pt.x = pos.x + s % 3 - 1;
    pt.y = pos.y + s / 3 - 1;
}