Quantcast
Viewing all articles
Browse latest Browse all 5

Answer by user16991 for Forest - A Simulated Ecosystem

Javascript+HTML - try it

Updated as per popular request

Image may be NSFW.
Clik here to view.
forest and graph

General behaviour

The program is now somewhat interactive.
The source code is completely parametrized, so you can tweak a few more internal parameters with your favourite text editor.

You can change the forest size.
A minimum of 2 is required to have enough room to place a tree, a lumberjack and a bear on 3 different spots, and the max is arbitrarily fixed to 100 (which will make your average computer crawl).

You can also change the simulation speed.
Display is updated every 20 ms, so a greater time step will produce finer animations.

The buttons allow to stop/start the simulation, or run it for a month or a year.

Movement of the forest dwellers is now somewhat animated. Mauling and tree cutting events are also figured.

A log of some events is also displayed. Some more messages are available if you change the verbosity level, but that would flood you with "Bob cuts yet another tree" notifications.
I would rather not do it if I were you, but I ain't, so...

Beside the playground, a set of auto-scaled graphics are drawn:

  • bears and lumberjacks populations
  • total number of trees, divided in saplings, mature and elder trees

The legend also displays current quantities of each item.

System stability

The graphs show that the initial conditions do not scale that gracefully. If the forest is too big, too many bears decimate the lumberjack population until enough of the pancake lovers have been put behind bars. This causes an initial explosion of elder trees, that in turn helps the lumberjack population to recover.

It seems 15 is the minimal size for the forest to survive. A forest of size 10 will usually get razed after a few hundred years. Any size above 30 will produce a map nearly full of trees. Between 15 and 30, you can see the tree population oscillating significantly.

Some debatable rule points

In the comments of the original post, it seems various bipeds are not supposed to occupy the same spot. This contradicts somehow the rule about a redneck wandering into a pancake amateur.
At any rate, I did not follow that guideline. Any forest cell can hold any number of inhyabitants (and exactly zero or one tree). This might have some consequences on the lumberjack efficiency: I suspect it allows them to dig into a clump of elder trees more easily. As for bears, I don't expect this to make a lot of difference.

I also opted for having always at least one lumberjack in the forest, despite the point stating that the redneck population could reach zero (firing the last lumberjack on the map if the harvest was really poor, which will never happen anyway unless the forest has been chopped down to extinction).

Tweaking

In order to achieve stability, I added two tweaking parameters :

1) lumberjacks growth rate

a coefficient applied to the formula that gives the number of extra lumberjacks hired when there is enough timber. Set to 1 to get back to original definition, but I found a value of about .5 allowed the forest (esp. elder trees) to develop better.

2) bear removal criterion

a coefficient that defines the minimal percentage of mauled lumberjacks to send a bear to the zoo. Set to 0 to go back to original definition, but this drastic bear elimination will basically limit the population to a 0-1 oscillation cycle.I set it to .15 (i.e. a bear is removed only if 15% or more of the lumberjacks have been mauled this year). This allows for a moderate bear population, sufficient to prevent the rednecks from wiping the area clean but still allowing a sizeable part of the forest to be chopped.

As a side note, the simulation never stops (even past the required 400 years). It could easily do so, but it doesn't.

The code

The code is entirely contained in a single HTML page.
It must be UTF-8 encoded to display the proper unicode symbols for bears and lumberjacks.

For the Unicode impaired systems (e.g. Ubuntu):find the following lines:

    jack   :{ pic: '🙎', color:'#bc0e11' },    bear   :{ pic: '🐻', color:'#422f1e' }},

and change the pictograms for characters easier to display (#, *, whatever)

<!doctype html><meta charset=utf-8><title>Of jacks and bears</title><body onload='init();'><style>    #log p { margin-top: 0; margin-bottom: 0; }</style><div id='main'></div><table><tr><td><canvas id='forest'></canvas></td><td><table><tr><td colspan=2><div>Forest size     <input type='text' size=10 onchange='create_forest(this.value);'>     </div><div>Simulation tick <input type='text' size= 5 onchange='set_tick(this.value);'> (ms)</div><div><input type='button' value='◾'       onclick='stop();'><input type='button' value='▸'       onclick='start();'><input type='button' value='1 month' onclick='start(1);'><input type='button' value='1 year'  onclick='start(12);'></div></td></tr><tr><td id='log' colspan=2></td></tr><tr><td><canvas id='graphs'></canvas></td><td id='legend'></td></tr><tr><td align='center'>evolution over 60 years</td><td id='counters'></td></tr></table></td></tr></table><script>// ==================================================================================================// Global parameters// ==================================================================================================var Prm = {    // ------------------------------------    // as defined in the original challenge    // ------------------------------------    // forest size    forest_size: 45, // 2025 cells    // simulation duration    duration: 400*12, // 400 years    // initial populations    populate: { trees: .5, jacks:.1, bears:.02 },    // tree ages    age: { mature:12, elder:120 },    // tree spawning probabilities    spawn: { sapling:0, mature:.1, elder:.2 },    // tree lumber yields    lumber: { mature:1, elder:2 },    // walking distances    distance: { jack:3, bear:5 },    // ------------------------------------    // extra tweaks    // ------------------------------------    // lumberjacks growth rate    // (set to 1 in original contest parameters)    jacks_growth: 1, // .5,    // minimal fraction of lumberjacks mauled to send a bear to the zoo    // (set to 0 in original contest parameters)    mauling_threshold: .15, // 0,    // ------------------------------------    // internal helpers    // ------------------------------------    // offsets to neighbouring cells    neighbours: [     {x:-1, y:-1}, {x: 0, y:-1}, {x: 1, y:-1},    {x:-1, y: 0},               {x: 1, y: 0},    {x:-1, y: 1}, {x: 0, y: 1}, {x: 1, y: 1}],    // ------------------------------------    // goodies    // ------------------------------------    // bear and people names    names:     { bear: ["Art", "Ursula", "Arthur", "Barney", "Bernard", "Bernie", "Bjorn", "Orson", "Osborn", "Torben", "Bernadette", "Nita", "Uschi"],     jack: ["Bob", "Tom", "Jack", "Fred", "Paul", "Abe", "Roy", "Chuck", "Rob", "Alf", "Tim", "Tex", "Mel", "Chris", "Dave", "Elmer", "Ian", "Kyle", "Leroy", "Matt", "Nick", "Olson", "Sam"] },    // months    month: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ],    // ------------------------------------    // graphics    // ------------------------------------    // messages verbosity (set to 2 to be flooded, -1 to have no trace at all)    verbosity: 1,     // pixel sizes     icon_size: 100,     canvas_f_size: 600,   // forest canvas size     canvas_g_width : 400, // graphs canvas size     canvas_g_height: 200,     // graphical representation     graph: {         soil: { color: '#82641e' },        sapling:{ radius:.1, color:'#52e311', next:'mature'},        mature :{ radius:.3, color:'#48b717', next:'elder' },        elder  :{ radius:.5, color:'#8cb717', next:'elder' },        jack   :{ pic: '🙎', color:'#2244ff' },        bear   :{ pic: '🐻', color:'#422f1e' },        mauling:{ pic: '★', color:'#ff1111' },        cutting:{ pic: '●', color:'#441111' }},    // animation tick    tick:100 // ms};// ==================================================================================================// Utilities// ==================================================================================================function int_rand (num){    return Math.floor (Math.random() * num);}function shuffle (arr){    for (        var j, x, i = arr.length;        i;         j = int_rand (i), x = arr[--i], arr[i] = arr[j], arr[j] = x);}function pick (arr){    return arr[int_rand(arr.length)];}function message (str, level){    level = level || 0;    if (level <= Prm.verbosity)    {        while (Gg.log.childNodes.length > 10) Gg.log.removeChild(Gg.log.childNodes[0]);        var line = document.createElement ('p');        line.innerHTML = Prm.month[Forest.date%12]+""+Math.floor(Forest.date/12)+": "+str;        Gg.log.appendChild (line);    }}// ==================================================================================================// Forest// ==================================================================================================// --------------------------------------------------------------------------------------------------// a forest cell// --------------------------------------------------------------------------------------------------function cell(){    this.contents = [];}cell.prototype = {    add: function (elt)    {        this.contents.push (elt);    },    remove: function (elt)    {        var i = this.contents.indexOf (elt);        this.contents.splice (i, 1);    },    contains: function (type)    {        for (var i = 0 ; i != this.contents.length ; i++)        {            if (this.contents[i].type == type)            {                return this.contents[i];            }        }        return null;    }}// --------------------------------------------------------------------------------------------------// an entity (tree, jack, bear)// --------------------------------------------------------------------------------------------------function entity (x, y, type){    this.age = 0;    switch (type)    {        case "jack": this.name = pick (Prm.names.jack); break;        case "bear": this.name = pick (Prm.names.bear); break;        case "tree": this.name = "sapling"; Forest.t.low++; break;    }    this.x = this.old_x = x;    this.y = this.old_y = y;    this.type = type;}entity.prototype = {    move: function ()    {        Forest.remove (this);        var n = neighbours (this);        this.x = n[0].x;        this.y = n[0].y;        return Forest.add (this);    }};// --------------------------------------------------------------------------------------------------// a list of entities (trees, jacks, bears)// --------------------------------------------------------------------------------------------------function elt_list (type){    this.type = type;    this.list = [];}elt_list.prototype = {    add: function (x, y)    {        if (x === undefined) x = int_rand (Forest.size);        if (y === undefined) y = int_rand (Forest.size);        var e = new entity (x, y, this.type);        Forest.add (e);        this.list.push (e);        return e;    },    remove: function (elt)    {        var i;        if (elt) // remove a specific element (e.g. a mauled lumberjack)        {            i = this.list.indexOf (elt);        }        else // pick a random element (e.g. a bear punished for the collective pancake rampage)        {                       i = int_rand(this.list.length);            elt = this.list[i];        }        this.list.splice (i, 1);        Forest.remove (elt);        if (elt.name == "mature") Forest.t.mid--;        if (elt.name == "elder" ) Forest.t.old--;        return elt;    }};// --------------------------------------------------------------------------------------------------// global forest handling// --------------------------------------------------------------------------------------------------function forest (size){    // initial parameters    this.size = size;    this.surface = size * size;    this.date = 0;    this.mauling = this.lumber = 0;    this.t = { low:0, mid:0, old:0 };    // initialize cells    this.cells = new Array (size);    for (var i = 0 ; i != size ; i++)    {        this.cells[i] = new Array(size);        for (var j = 0 ; j != size ; j++)        {            this.cells[i][j] = new cell;        }    }    // initialize entities lists    this.trees = new elt_list ("tree");    this.jacks = new elt_list ("jack");    this.bears = new elt_list ("bear");    this.events = [];}forest.prototype = {    populate: function ()    {        function fill (num, list)        {            for (var i = 0 ; i < num ; i++)            {                var coords = pick[i_pick++];                list.add (coords.x, coords.y);            }        }        // shuffle forest cells        var pick = new Array (this.surface);        for (var i = 0 ; i != this.surface ; i++)        {            pick[i] = { x:i%this.size, y:Math.floor(i/this.size)};        }        shuffle (pick);        var i_pick = 0;        // populate the lists        fill (Prm.populate.jacks * this.surface, this.jacks);        fill (Prm.populate.bears * this.surface, this.bears);        fill (Prm.populate.trees * this.surface, this.trees);        this.trees.list.forEach (function (elt) { elt.age = Prm.age.mature; });    },    add: function (elt)    {        var cell = this.cells[elt.x][elt.y];        cell.add (elt);        return cell;    },    remove: function (elt)    {        var cell = this.cells[elt.x][elt.y];        cell.remove (elt);    },    evt_mauling: function (jack, bear)    {        message (bear.name+" sniffs a delicious scent of pancake, unfortunately for "+jack.name, 1);        this.jacks.remove (jack);        this.mauling++;        Gg.counter.mauling.innerHTML = this.mauling;        this.register_event ("mauling", jack);    },    evt_cutting: function (jack, tree)    {        if (tree.name == 'sapling') return; // too young to be chopped down        message (jack.name+" cuts a "+tree.name+" tree: lumber "+this.lumber+" (+"+Prm.lumber[tree.name]+")", 2);        this.trees.remove (tree);        this.lumber += Prm.lumber[tree.name];        Gg.counter.cutting.innerHTML = this.lumber;        this.register_event ("cutting", jack);    },    register_event: function (type, position)    {        this.events.push ({ type:type, x:position.x, y:position.y});    },    tick: function()    {        this.date++;        this.events = [];        // monthly updates        this.trees.list.forEach (b_tree);        this.jacks.list.forEach (b_jack);        this.bears.list.forEach (b_bear);        // feed graphics        Gg.graphs.trees.add (this.trees.list.length);        Gg.graphs.jacks.add (this.jacks.list.length);        Gg.graphs.bears.add (this.bears.list.length);        Gg.graphs.sapling.add (this.t.low);        Gg.graphs.mature .add (this.t.mid);        Gg.graphs.elder  .add (this.t.old);        // yearly updates        if (!(this.date % 12))        {            // update jacks            if (this.jacks.list.length == 0)            {                message ("An extra lumberjack is hired after a bear rampage");                this.jacks.add ();            }            if (this.lumber >= this.jacks.list.length)            {                var extra_jacks = Math.floor (this.lumber / this.jacks.list.length * Prm.jacks_growth);                message ("A good lumbering year. Lumberjacks +"+extra_jacks, 1);                for (var i = 0 ; i != extra_jacks ; i++) this.jacks.add ();            }            else if (this.jacks.list.length > 1)            {                var fired = this.jacks.remove();                message (fired.name+" has been chopped", 1);            }            // update bears            if (this.mauling > this.jacks.list.length * Prm.mauling_threshold)            {                var bear = this.bears.remove();                message (bear.name+" will now eat pancakes in a zoo", 1);            }            else            {                var bear = this.bears.add();                message (bear.name+" starts a quest for pancakes", 1);            }            // reset counters            this.mauling = this.lumber = 0;        }    }}function neighbours (elt){    var ofs,x,y;    var list = [];    for (ofs in Prm.neighbours)    {        var o = Prm.neighbours[ofs];        x = elt.x + o.x;        y = elt.y + o.y;        if (  x < 0 || x >= Forest.size           || y < 0 || y >= Forest.size) continue;        list.push ({x:x, y:y});    }    shuffle (list);    return list;}// --------------------------------------------------------------------------------------------------// entities behaviour// --------------------------------------------------------------------------------------------------function b_tree (tree){    // update tree age and category    if      (tree.age == Prm.age.mature) { tree.name = "mature"; Forest.t.low--; Forest.t.mid++; }    else if (tree.age == Prm.age.elder ) { tree.name = "elder" ; Forest.t.mid--; Forest.t.old++; }    tree.age++;    // see if we can spawn something    if (Math.random() < Prm.spawn[tree.name])    {        var n = neighbours (tree);        for (var i = 0 ; i != n.length ; i++)        {            var coords = n[i];            var cell = Forest.cells[coords.x][coords.y];            if (cell.contains("tree")) continue;            Forest.trees.add (coords.x, coords.y);            break;        }    }}function b_jack (jack){    jack.old_x = jack.x;    jack.old_y = jack.y;    for (var i = 0 ; i != Prm.distance.jack ; i++)    {        // move        var cell = jack.move ();        // see if we stumbled upon a bear        var bear = cell.contains ("bear");        if (bear)        {            Forest.evt_mauling (jack, bear);            break;        }        // see if we reached an harvestable tree        var tree = cell.contains ("tree");        if (tree)        {            Forest.evt_cutting (jack, tree);            break;        }    }}function b_bear (bear){    bear.old_x = bear.x;    bear.old_y = bear.y;    for (var i = 0 ; i != Prm.distance.bear ; i++)    {        var cell = bear.move ();        var jack = cell.contains ("jack");        if (jack)        {            Forest.evt_mauling (jack, bear);            break; // one pancake hunt per month is enough        }    }}// --------------------------------------------------------------------------------------------------// Graphics// --------------------------------------------------------------------------------------------------function init(){    function create_counter (desc)    {        var counter = document.createElement ('span');        var item = document.createElement ('p');        item.innerHTML = desc.name+"&nbsp;";        item.style.color = desc.color;        item.appendChild (counter);        return { item:item, counter:counter };    }    // initialize forest canvas    Gf = { period:20, tick:0 };    Gf.canvas = document.getElementById ('forest');    Gf.canvas.width  =    Gf.canvas.height = Prm.canvas_f_size;    Gf.ctx = Gf.canvas.getContext ('2d');    Gf.ctx.textBaseline = 'Top';    // initialize graphs canvas    Gg = { counter:[] };    Gg.canvas = document.getElementById ('graphs');    Gg.canvas.width  = Prm.canvas_g_width;    Gg.canvas.height = Prm.canvas_g_height;    Gg.ctx = Gg.canvas.getContext ('2d');    // initialize graphs    Gg.graphs = {        jacks:   new graphic({ name:"lumberjacks" , color:Prm.graph.jack.color }),        bears:   new graphic({ name:"bears"       , color:Prm.graph.bear.color, ref:'jacks' }),        trees:   new graphic({ name:"trees"       , color:'#0F0' }),        sapling: new graphic({ name:"saplings"    , color:Prm.graph.sapling.color, ref:'trees' }),        mature:  new graphic({ name:"mature trees", color:Prm.graph.mature .color, ref:'trees' }),        elder:   new graphic({ name:"elder trees" , color:Prm.graph.elder  .color, ref:'trees' })    };    Gg.legend = document.getElementById ('legend');    for (g in Gg.graphs)    {        var gr = Gg.graphs[g];        var c = create_counter (gr);        gr.counter = c.counter;        Gg.legend.appendChild (c.item);    }    // initialize counters    var counters = document.getElementById ('counters');    var def = [ "mauling", "cutting" ];    var d; for (d in def)    {        var c = create_counter ({ name:def[d], color:Prm.graph[def[d]].color });        counters.appendChild (c.item);        Gg.counter[def[d]] = c.counter;    }    // initialize log    Gg.log = document.getElementById ('log');    // create our forest    create_forest(Prm.forest_size);    start();}function create_forest (size){    if (size < 2) size = 2;    if (size > 100) size = 100;    Forest = new forest (size);    Prm.icon_size = Prm.canvas_f_size / size;    Gf.ctx.font = 'Bold '+Prm.icon_size+'px Arial';    Forest.populate ();    draw_forest();    var g; for (g in Gg.graphs) Gg.graphs[g].reset();    draw_graphs();}function animate(){    if (Gf.tick % Prm.tick == 0)    {        Forest.tick();        draw_graphs();    }    draw_forest();    Gf.tick+= Gf.period;    if (Gf.tick == Gf.stop_date) stop();}function draw_forest (){    function draw_dweller (dweller)    {        var type = Prm.graph[dweller.type];        Gf.ctx.fillStyle = type.color;        var x = dweller.x * time_fraction + dweller.old_x * (1 - time_fraction);        var y = dweller.y * time_fraction + dweller.old_y * (1 - time_fraction);        Gf.ctx.fillText (type.pic, x * Prm.icon_size, (y+1) * Prm.icon_size);    }    function draw_event (evt)    {        var gr = Prm.graph[evt.type];        Gf.ctx.fillStyle = gr.color;        Gf.ctx.fillText (gr.pic, evt.x * Prm.icon_size, (evt.y+1) * Prm.icon_size);    }    function draw_tree (tree)    {        // trees grow from one category to the next        var type = Prm.graph[tree.name];        var next = Prm.graph[type.next];        var radius = (type.radius + (next.radius - type.radius) / Prm.age[type.next] * tree.age) * Prm.icon_size;        Gf.ctx.fillStyle = Prm.graph[tree.name].color;        Gf.ctx.beginPath();        Gf.ctx.arc((tree.x+.5) * Prm.icon_size, (tree.y+.5) * Prm.icon_size, radius, 0, 2*Math.PI);        Gf.ctx.fill();    }    // background    Gf.ctx.fillStyle = Prm.graph.soil.color;    Gf.ctx.fillRect (0, 0, Gf.canvas.width, Gf.canvas.height);    // time fraction to animate displacements    var time_fraction = (Gf.tick % Prm.tick) / (Prm.tick-Gf.period);    // entities    Forest.trees.list.forEach (draw_tree);    Forest.jacks.list.forEach (draw_dweller);    Forest.bears.list.forEach (draw_dweller);    Forest.events.forEach (draw_event);}// --------------------------------------------------------------------------------------------------// Graphs// --------------------------------------------------------------------------------------------------function graphic (prm){    this.name  = prm.name  || '?';    this.color = prm.color || '#FFF';    this.size  = prm.size  || 720;    this.ref   = prm.ref;    this.values = [];    this.counter = document.getElement}graphic.prototype = {    draw: function ()    {        Gg.ctx.strokeStyle = this.color;        Gg.ctx.beginPath();        for (var i = 0 ; i != this.values.length ; i++)        {            var x = (i + this.size - this.values.length) / this.size * Gg.canvas.width;            var y = (1-(this.values[i] - this.min) / this.rng)       * Gg.canvas.height;            if (i == 0) Gg.ctx.moveTo (x, y);            else        Gg.ctx.lineTo (x, y);        }        Gg.ctx.stroke();    },    add: function (value)    {        // store value        this.values.push (value);        this.counter.innerHTML = value;        // cleanup history        while (this.values.length > this.size) this.values.splice (0,1);        // compute min and max        this.min = Math.min.apply(Math, this.values);        if (this.min > 0) this.min = 0;        this.max = this.ref                  ? Gg.graphs[this.ref].max                 : Math.max.apply(Math, this.values);        this.rng = this.max - this.min;        if (this.rng == 0) this.rng = 1;    },    reset: function()    {        this.values = [];    }}function draw_graphs (){    function draw_graph (graph)    {        graph.draw();    }    // background    Gg.ctx.fillStyle = '#000';    Gg.ctx.fillRect (0, 0, Gg.canvas.width, Gg.canvas.height);    // graphs    var g; for (g in Gg.graphs)    {        var gr = Gg.graphs[g];        gr.draw();    }}// --------------------------------------------------------------------------------------------------// User interface// --------------------------------------------------------------------------------------------------function set_tick(value){    value = Math.round (value / Gf.period);    if (value < 2) value = 2;    value *= Gf.period;    Prm.tick = value;    return value;}function start (duration){    if (Prm.timer) stop();    Gf.stop_date = duration ? Gf.tick + duration*Prm.tick : -1;    Prm.timer = setInterval (animate, Gf.period);}function stop (){    if (Prm.timer)    {        clearInterval (Prm.timer);        Prm.timer = null;    }    Gf.stop_date = -1;}</script></body>

What next?

More remarks are still welcome.

N.B: I'm aware sapling/mature/elder trees count is still a bit messy, but to hell with it.

Also, I find document.getElementById more readable than $, so no need to complain about the lack of jQueryisms. It's jQuery free on purpose. To Each his own, right?


Viewing all articles
Browse latest Browse all 5

Trending Articles