# Forest - A Simulated Ecosystem

## NOTE

This problem was taken from this reddit thread (spoiler alert!), and I have adjusted it to make it fit with this site's format. All credit goes to reddit user "Coder_d00d".

In this problem, we will simulate a forest.

For this simulated forest we will be dealing with 3 aspects.

• Trees which can be a Sapling, Tree or Elder Tree.
• Lumberjacks (He chops down down trees, he eats his lunch and goes to the Lava-try)
• Bears (He mauls the lumberjacks who smells like pancakes)

A fore-warning: these rules are most probably not perfect. See them as a guideline, and if you need to tweak anything slightly that is fine (spawning rates have been pointed out as an issue, see kuroi neko's answer as an example of this.

## Cycle of time:

The simulation will simulate by months. You will progessive forward in time with a "tick". Each "tick" represents a month. Every 12 "ticks" represents a year. Our forest will change and be in constant change. We will record the progress of our forest and analyze what happens to it.

## Forest:

The forest will be a two dimensional forest. We will require an input of N to represent the size of the forest in a grid that is N x N in size. At each location you can hold Trees, Bears or Lumberjacks. They can occupy the same spot but often events occur when they occupy the same spot.

Our forest will be spawned randomly based on the size. For example if your value of N = 10. You will have a 10 by 10 forest and 100 spots.

• 10% of the Forest will hold a Lumberjack in 10 random spots. (using our 100 spot forest this should be 10 lumberjacks)
• 50% of the Forest will hold Trees (Trees can be one of 3 kinds and will start off as the middle one of "Tree") in random spots.
• 2% of the Forest will hold Bears.

How you receive the size of the forest is up to you (read from stdin, a file, or hardcode it in). I would recommend keeping N like 5 or higher. Small Forests are not much fun.

## Events:

During the simulation there will be events. The events occur based on some logic which I will explain below. I will describe the events below in each description of the 3 elements of our forest.

The events follow the order of trees first, lumberjacks second and bears last.

## Trees:

• Every month a Tree has a 10% chance to spawn a new "Sapling". In a random open space adjacent to a Tree you have a 10% chance to create a "Sapling".

• For example a Tree in the middle of the forest has 8 other spots around it. One of these (if they are empty) will become a "Sapling".

• After 12 months of being in existence a "Sapling" will be upgrade to a "Tree". A "Sapling" cannot spawn other trees until it has matured into a "Tree".

• Once a "Sapling" becomes a tree it can spawn other new "Saplings".

• When a "Tree" has been around for 120 months (10 years) it will become an "Elder Tree".

• Elder Trees have a 20% chance to spawn a new "Sapling" instead of 10%.

• If there are no open adjacent spots to a Tree or Elder Tree it will not spawn any new Trees.

## Lumberjacks:

Lumberjacks cut down trees, they skip and jump they like to press wild flowers.

• Each month lumberjacks will wander. They will move up to 3 times to a randomly picked spot that is adjacent in any direction. So for example a Lumberjack in the middle of your grid has 8 spots to move to. He will wander to a random spot. Then again. And finally for a third time. NB: This can be any spot (so they can walk into bears, resulting in a maul).

• When the lumberjack moves, if he encounters a Tree (not a sapling), he will stop and his wandering for that month comes to an end. He will then harvest the Tree for lumber. Remove the tree. Gain 1 piece of lumber.

• Lumberjacks will not harvest "Saplings".

• Lumberacks also harvest Elder Trees. Elder Trees are worth 2 pieces of lumber.

## Lumber Tracking:

Every 12 months the amount of lumber harvested is compared to the number of lumberjacks in the forest.

• If the lumber collected equals or exceeds the amount of lumberjacks in the forest a number of new lumberjacks are hired and randomly spawned in the forest.

• Work out the number of lumberjacks to hire with: floor(lumber_collected / number_of_lumberjacks)

• However if after a 12 month span the amount of lumber collected is below the number of lumberjacks then a lumberjack is let go to save money and 1 random lumberjack is removed from the forest. Note that you will never reduce your Lumberjack labor force below 0.

## Bears:

Bears wander the forest much like a lumberjack. However instead of 3 spaces a Bear will roam up to 5 spaces.

• If a bear comes across a Lumberjack he will stop his wandering for the month. (For example after 2 moves the bear lands on a space with a lumberjack he will not make any more moves for this month)

• Lumberjacks smell like pancakes. Bears love pancakes. Therefore the Bear will unfortunately maul and hurt the lumberjack. The lumberjack will be removed from the forest (He will go home and shop on wednesdays and have buttered scones for tea).

• We will track this as a "Maul" accident.

• Note that the lumberjack population can never drop below 1 - so if the last lumberjack is mauled just spawn another one in.

## Maul Tracking:

• During the course of 12 months if there 0 "Maul" accidents then the Bear population will increase by 1. If however there are any "Maul" accidents the Lumberjacks will hire a Zoo to trap and take a Bear away. Remove 1 random Bear. Note that if your Bear population reaches 0 bears then there will be no "Maul" accidents in the next year and so you will spawn 1 new Bear next year.

• If there is only 1 lumberjack in the forest and he gets Mauled, he will be sent home, but a new one will be hired immediately and respawned somewhere else in the forest. The lumberjack population can never drop below 1.

## Time:

The simulation occurs for 4800 months (400 years), or until no Saplings, Trees or Elder Trees exist.

## Output:

Every month you will print out a map of the forest - perhaps using an ASCII map, or using graphics and colours.

## Optional Extras

• You could output the populations of trees, lumberjacks and bears each tick.
• You could output whenever an event occurs (e.g: "A bear mauled a lumberjack.")

## Scoring

This is a popularity contest, so most upvotes wins!

EDIT - People have pointed out a number of flaws in my rules, and while you can feel free to ask me questions, it is also okay to tweak the rules a bit to suit your own program, or interpretation of the program.

• About: Note that you will never reduce your Lumberjack labor force below 0 in lumberjack section list item 3. perhaps change this to 1 to be in line with what you mention in the bear section? Jul 29, 2014 at 12:24
• Good point, will edit that in now. Jul 29, 2014 at 12:32
• Can you discuss the timing of the movement? Is it "trees move/advance", then "lumberjacks move", then "bears move"? Also, can bears and trees occupy the same spaces? Jul 29, 2014 at 17:06
• What happens if two bears both walk into the same spot? And what about two lumberjacks? Can they be in the same spot? Jul 30, 2014 at 11:44
• It is identical to reddit.com/r/dailyprogrammer/comments/27h53e/… You ought to give it credit - at least such that people can go there for other interesting solutions.
– user30052
Aug 1, 2014 at 12:04

# Javascript+HTML - try it

## 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>
<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' },
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 = {

{
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;
}
};

// --------------------------------------------------------------------------------------------------
// a list of entities (trees, jacks, bears)
// --------------------------------------------------------------------------------------------------
function elt_list (type)
{
this.type = type;
this.list = [];
}

elt_list.prototype = {
{
if (x === undefined) x = int_rand (Forest.size);
if (y === undefined) y = int_rand (Forest.size);
var e = new entity (x, y, this.type);
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++];
}
}

// 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; });
},

{
var cell = this.cells[elt.x][elt.y];
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 = [];

this.trees.list.forEach (b_tree);
this.jacks.list.forEach (b_jack);
this.bears.list.forEach (b_bear);

// feed graphics

if (!(this.date % 12))
{
// update jacks
if (this.jacks.list.length == 0)
{
message ("An extra lumberjack is hired after a bear rampage");
}

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
{
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;
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];
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();
},

{
// 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? • +1, This looks badass, I'd love to see the interactivity! Jul 31, 2014 at 1:08 • I can't see the bears and lumberjacks. What should I do to see them? Btw, only maximum of one bear should be removed per year, right? Jul 31, 2014 at 1:37 • @WilliamBarbosa If the vox populi screams loud enough, it might coax me into adding some :) – user16991 Jul 31, 2014 at 2:09 • @justhalf see my edit about unicode characters. As for bears, if I understand the specs correctly, yes: only one bear picked at random sent to the zoo each year, if some mauling occured. – user16991 Jul 31, 2014 at 2:10 • The growing trees are marvelous. Good job! Jul 31, 2014 at 15:50 ## AngularJS Here is my version, which is still a Work In Progress: the code is a bit… well… ugly. And quite slow. I also plan to add more options to parametrize the evolution and to analyse the state of the forest. Comments and amelioration proposals are welcome! Demonstration <div ng-app="ForestApp" ng-controller="ForestController"> <form name="parametersForm" ng-hide="evolutionInProgress" autocomplete="off" novalidate> <div class="line"> <label for="forestSize">Size of the forest:</label> <input type="number" ng-model="Parameters.forestSize" id="forestSize" min="5" ng-pattern="/^[0-9]+$/" required />
</div>
<div class="line">
<label for="simulationInterval">Number of milliseconds between each tick</label>
<input type="number" ng-model="Parameters.simulationInterval" id="simulationInterval" min="10" ng-pattern="/^[0-9]+$/" required /> </div> <div class="line"> <label for="animationsEnabled">Animations enabled? <br /><small>(>= 300 ms between each tick is advisable)</small> </label> <input type="checkbox" ng-model="Parameters.animationsEnabled" id="animationsEnabled" /> </div> <div class="line"> <button ng-disabled="parametersForm.$invalid || evolutionInProgress" ng-click="launchEvolution()">Launch the evolution!</button>
</div>
</form>
<div id="forest" ng-style="{width: side = (20*Parameters.forestSize) + 'px', height: side, 'transition-duration': transitionDuration = (Parameters.animationsEnabled ? 0.8*Parameters.simulationInterval : 0) + 'ms', '-webkit-transition-duration': transitionDuration}">
<div ng-repeat="bear in Forest.bearsList" class="entity entity--bear" ng-style="{left: (20*bear.x) + 'px', top: (20*bear.y) + 'px'}"></div>
<div ng-repeat="lumberjack in Forest.lumberjacksList" class="entity entity--lumberjack" ng-style="{left: (20*lumberjack.x) + 'px', top: (20*lumberjack.y) + 'px'}"></div>
<div ng-repeat="tree in Forest.treesList" class="entity entity--tree" ng-class="'entity--tree--' + tree.stage" ng-style="{left: (20*tree.x) + 'px', top: (20*tree.y) + 'px'}"></div>
</div>
<div class="line"><em>Age of the forest:</em><samp>{{floor(Forest.age/12)}} year{{floor(Forest.age/12) > 1 ? 's' : ''}} and {{Forest.age%12}} month{{Forest.age%12 > 1 ? 's' : ''}}</samp></div>
<div class="line"><em>Number of bears:</em><samp>{{Forest.bearsList.length}}</samp></div>
<div class="line"><em>Number of lumberjacks:</em><samp>{{Forest.lumberjacksList.length}}</samp></div>
<br />
<div class="line"><em>Number of lumbers collected:</em><samp>{{Forest.numberOfLumbers}}</samp></div>
<div class="line"><em>Number of mauls:</em><samp>{{Forest.numberOfMauls}}</samp></div>
</div>

/** @link http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array */
function shuffle(array) {
var currentIndex = array.length,
temporaryValue, randomIndex;

// While there remain elements to shuffle...
while (0 !== currentIndex) {

// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;

// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}

return array;
}

var forestApp = angular.module('ForestApp', ['ngAnimate']);

forestApp.value('Parameters', {
/** @var int[] Maximal number of moves by species */
speed: {
bear: 5,
lumberjack: 3,
},

/** @var int[] Initial percentage of each species in the forest */
initialPercentage: {
bear: 2,
lumberjack: 10,
tree: 50,
},

/** @var int[] Spawing rate, in percentage, of new saplings around an existing tree */
spawningPercentage: {
0: 0,
1: 10,
2: 20,
},

/** @var int[] Age of growth for an existing tree */
ageOfGrowth: {
sapling: 12,
tree: 120,
},

/** @var int[] Lumber collected on an existing tree */
numberOfLumbers: {
tree: 1,
elderTree: 2,
},

/** @var int Size of each side of the forest */
forestSize: 20,

/** @var int Number of milliseconds between each tick (month in the forest) */
simulationInterval: 50,
});

forestApp.constant('TREE_STAGE', {
SAPLING: 0,
TREE: 1,
ELDER_TREE: 2,
});

forestApp.factory('Tree', ['Forest', 'Parameters', 'TREE_STAGE', function (Forest, Parameters, TREE_STAGE) {
// Classes which represents a tree
var Tree = function (stage, x, y) {
/** @var TREE_STAGE Current stage of the tree */
this.stage = stage;

/** @var int Current age of the tree, in month */
this.age = 0;

/** @var int X coordinates of the tree */
this.x = x;

/** @var int Y coordinates of the tree */
this.y = y;

this.tick = function () {
if (Math.random() < Parameters.spawningPercentage[this.stage] / 100) {
var freePositionsList = shuffle(Forest.getFreePositionsAround(this.x, this.y));
if (freePositionsList.length > 0) {
var saplingPosition = freePositionsList[0];

Tree.create(TREE_STAGE.SAPLING, saplingPosition[0], saplingPosition[1]);
}
}

++this.age;

if (this.stage === TREE_STAGE.SAPLING && this.age == Parameters.ageOfGrowth.sapling) {
this.stage = TREE_STAGE.TREE;
} else if (this.stage === TREE_STAGE.TREE && this.age == Parameters.ageOfGrowth.tree) {
this.stage = TREE_STAGE.ELDER_TREE;
}
};

/**
* Remove the entity
*/
this.remove = function () {
var index = Forest.treesList.indexOf(this);
Forest.treesList.splice(index, 1);
};
};

Tree.create = function (stage, x, y) {
};

return Tree;
}]);

forestApp.factory('Lumberjack', ['Forest', 'Parameters', 'TREE_STAGE', function (Forest, Parameters, TREE_STAGE) {
// Classes which represents a lumberjack
var Lumberjack = function (x, y) {
/** @var int X coordinates of the lumberjack */
this.x = x;

/** @var int Y coordinates of the lumberjack */
this.y = y;

this.tick = function () {
for (movement = Parameters.speed.lumberjack; movement > 0; --movement) {
var positionsList = shuffle(Forest.getPositionsAround(this.x, this.y));
var newPosition = positionsList[0];
this.x = newPosition[0];
this.y = newPosition[1];

var tree = Forest.getTreeAt(this.x, this.y);
if (tree !== null) {
if (tree.stage === TREE_STAGE.SAPLING) {
return;
} else if (tree.stage === TREE_STAGE.TREE) {
Forest.numberOfLumbers += Parameters.numberOfLumbers.tree;
} else {
Forest.numberOfLumbers += Parameters.numberOfLumbers.elderTree;
}

tree.remove();
movement = 0;
};
}
};

/**
* Remove the entity
*/
this.remove = function () {
if (Forest.lumberjacksList.length === 1) {
this.x = Math.floor(Math.random() * Parameters.forestSize);
this.y = Math.floor(Math.random() * Parameters.forestSize);
} else {
var index = Forest.lumberjacksList.indexOf(this);
Forest.lumberjacksList.splice(index, 1);
}
};
};

Lumberjack.create = function (x, y) {
};

return Lumberjack;
}]);

forestApp.factory('Bear', ['Forest', 'Parameters', function (Forest, Parameters) {
// Classes which represents a bear
var Bear = function (x, y) {
/** @var int X coordinates of the bear */
this.x = x;

/** @var int Y coordinates of the bear */
this.y = y;

this.tick = function () {
for (movement = Parameters.speed.bear; movement > 0; --movement) {
var positionsList = shuffle(Forest.getPositionsAround(this.x, this.y));
var newPosition = positionsList[0];
this.x = newPosition[0];
this.y = newPosition[1];

angular.forEach(Forest.getLumberjacksListAt(this.x, this.y), function (lumberjack) {
lumberjack.remove();
++Forest.numberOfMauls;
movement = 0;
});
}
};

/**
* Remove the entity
*/
this.remove = function () {
var index = Forest.bearsList.indexOf(this);
Forest.bearsList.splice(index, 1);
};
};

Bear.create = function (x, y) {
};

return Bear;
}]);

forestApp.service('Forest', ['Parameters', function (Parameters) {
var forest = this;

this.age = 0;
this.numberOfLumbers = 0;
this.numberOfMauls = 0;

this.bearsList = [];
this.lumberjacksList = [];
this.treesList = [];

this.getEntitiesList = function () {
return forest.bearsList.concat(forest.lumberjacksList, forest.treesList);
};

/**
* Age the forest by one month
*/
this.tick = function () {
angular.forEach(forest.getEntitiesList(), function (entity) {
entity.tick();
});

++forest.age;
};

bear: function (bear) {
forest.bearsList.push(bear);
},
lumberjack: function (lumberjack) {
forest.lumberjacksList.push(lumberjack);
},
tree: function (tree) {
forest.treesList.push(tree);
},
};

/**
* @return Tree|null Tree at this position, or NULL if there is no tree.
*/
this.getTreeAt = function (x, y) {
var numberOfTrees = forest.treesList.length;
for (treeId = 0; treeId < numberOfTrees; ++treeId) {
var tree = forest.treesList[treeId];
if (tree.x === x && tree.y === y) {
return tree;
}
}

return null;
};

/**
* @return Lumberjack[] List of the lumberjacks at this position
*/
this.getLumberjacksListAt = function (x, y) {
var lumberjacksList = [];
angular.forEach(forest.lumberjacksList, function (lumberjack) {
if (lumberjack.x === x && lumberjack.y === y) {
lumberjacksList.push(lumberjack);
}
});

return lumberjacksList;
};

/**
* @return int[] Positions around this position
*/
this.getPositionsAround = function (x, y) {
var positionsList = [
[x - 1, y - 1],
[x, y - 1],
[x + 1, y - 1],
[x - 1, y],
[x + 1, y],
[x - 1, y + 1],
[x, y + 1],
[x + 1, y + 1]
];

return positionsList.filter(function (position) {
return (position[0] >= 0 && position[1] >= 0 && position[0] < Parameters.forestSize && position[1] < Parameters.forestSize);
});
};

/**
* @return int[] Positions without tree around this position
*/
this.getFreePositionsAround = function (x, y) {
var positionsList = forest.getPositionsAround(x, y);

return positionsList.filter(function (position) {
return forest.getTreeAt(position[0], position[1]) === null;
});
};
}]);

forestApp.controller('ForestController', ['$interval', '$scope', 'Bear', 'Forest', 'Lumberjack', 'Parameters', 'Tree', 'TREE_STAGE', function ($interval,$scope, Bear, Forest, Lumberjack, Parameters, Tree, TREE_STAGE) {
$scope.Forest = Forest;$scope.Parameters = Parameters;
$scope.evolutionInProgress = false;$scope.floor = Math.floor;

var positionsList = [];

/**
* Start the evolution of the forest
*/
$scope.launchEvolution = function () {$scope.evolutionInProgress = true;

for (var x = 0; x < Parameters.forestSize; ++x) {
for (var y = 0; y < Parameters.forestSize; ++y) {
positionsList.push([x, y]);
}
}

shuffle(positionsList);
var numberOfBears = Parameters.initialPercentage.bear * Math.pow(Parameters.forestSize, 2) / 100;
for (var bearId = 0; bearId < numberOfBears; ++bearId) {
Bear.create(positionsList[bearId][0], positionsList[bearId][1]);
}

shuffle(positionsList);
var numberOfLumberjacks = Parameters.initialPercentage.lumberjack * Math.pow(Parameters.forestSize, 2) / 100;
for (var lumberjackId = 0; lumberjackId < numberOfLumberjacks; ++lumberjackId) {
Lumberjack.create(positionsList[lumberjackId][0], positionsList[lumberjackId][1]);
}

shuffle(positionsList);
var numberOfTrees = Parameters.initialPercentage.tree * Math.pow(Parameters.forestSize, 2) / 100;
for (var treeId = 0; treeId < numberOfTrees; ++treeId) {
Tree.create(TREE_STAGE.TREE, positionsList[treeId][0], positionsList[treeId][1]);
}

$interval(function () { Forest.tick(); if (Forest.age % 12 === 0) { // Hire or fire lumberjacks if (Forest.numberOfLumbers >= Forest.lumberjacksList.length) { shuffle(positionsList); var numberOfLumberjacks = Math.floor(Forest.numberOfLumbers / Forest.lumberjacksList.length); for (var lumberjackId = 0; lumberjackId < numberOfLumberjacks; ++lumberjackId) { Lumberjack.create(positionsList[lumberjackId][0], positionsList[lumberjackId][1]); } } else { shuffle(Forest.lumberjacksList); Forest.lumberjacksList[0].remove(); } // Hire or fire bears if (Forest.numberOfMauls === 0) { shuffle(positionsList); Bear.create(positionsList[0][0], positionsList[0][1]); } else { Forest.bearsList[0].remove(); } Forest.numberOfLumbers = 0; Forest.numberOfMauls = 0; } }, Parameters.simulationInterval); }; }]);  • also i like how we use the same code for randomize shuffle array Jul 29, 2014 at 17:22 • Interesting. The first time I ran your simulation, it ends with all bears and no lumberjacks, which led me to think that this simulation is imbalanced. But it never happens again afterwards, so I guess it was just "bad luck" that all the lumberjack got mauled by the bears (because if there are so many bears, lumberjacks will almost certainly die) Jul 31, 2014 at 1:20 • getEntitiesAt seems to be a CPU hog! running the system with a 50x50 grid takes more than one second per month on my PC. Also there is a case when all trees get chopped, then all lumberjacks are fired and the map slowly fills with bears :). Try a small size (10 or less) to see it happen. – user16991 Jul 31, 2014 at 2:55 • @kuroineko: It's because there's a bug (or rather an unimplemented rule) where after a lumberjack gets mauled and if it respawned on a bear, it won't be there. So it gets to 0. Fix it with a check after Forest.tick(), if Forest.lumberjackList.length == 0, then Lumberjack.create(<number>, <number>). Jul 31, 2014 at 6:02 • I have tried tweaking your code to make bears prefer to stay in the forest (and also incorporate @kuroineko-like bear removal criteria, like add bears if there are less than 10% mauling, and remove bear if there are more than 15% mauling). It is interesting to see how the behaviour changes =) Aug 2, 2014 at 5:50 # Javascript I think this mostly works. There's some wonky behavior where I spawn all the new bears/lumberjacks in sync and right next to each other because laziness in insertions. This implementation does not allow lumberjacks to stand on saplings, cause you know, trampling saplings is bad. Fiddle art uses colored rectangles by default, change the second line to false to use letters to draw. fiddle HTML: <canvas id="c" width="1" height="1"></canvas> <div id="p1"></div> <div id="p2"></div> <div id="p3"></div> <div id="p4"></div>  Js: var n = 10; // Size of the grid var drawUsingColor = true; // If true, draws colors for each entity instead :D var intervalTime = 1000; // how often each tick happens, in milliseconds var jackRatio = 0.1; var treeRatio = 0.5; var bearRatio = 0.02; var size = 48; // Pixels allocated (in size x size) for each entity var font = "30px Lucida Console"; // if drawUsingColor is false var bearColor = '#8B4513'; // Saddlebrown var elderColor = '#556B2F'; // DarkOliveGreen var lumberjackColor = '#B22222'; // Firebrick var treeColor = '#008000'; // Green var saplingColor = '#ADFF2F'; // GreenYellow // Game rules: var spawnSaplingChance = 0.1; var elderTreeAge = 120; var elderSaplingChance = 0.2; var treeAge = 12; var lumberjackRange = 3; var bearRange = 5; var zooPeriod = 12; // If a maul happens within this period var lumberPeriod = 12; // New lumberjacks hired in this period var time = 1; var world; var n2 = n * n; //because one saved keystroke var zooqueue = []; var lumberqueue = []; var canvas = document.getElementById('c'); // Needs more jquery var context = canvas.getContext('2d'); context.font = font; // various statistics var treesAlive = 0; var jacksAlive = 0; var bearsAlive = 0; var currentLumber = 0; var lumberjacksMauled = 0; var recentEvents = ''; // Entity is a bear, eldertree, lumberjack, tree, sapling, with age. aka belts. function Entity(belts, birthday) { this.type = belts; this.age = 0; this.birthday = birthday; } function initWorld() { canvas.height = size * n; canvas.width = size * n; world = new Array(n2); // One pass spawning algorithm: numEntity = number of entity left to spawn // If rand() in range [0,numtrees), spawn tree // if rand() in range [numtrees, numtrees+numjacks), spawn lumberjack // if rand() in range [numtrees+numjacks, numtrees+numjacks+numbears), spawn bear var numTrees = treeRatio * n2; var numJacks = jackRatio * n2; var numBears = bearRatio * n2; var godseed = new Array(n2); for (var i = 0; i < n2; i++) { godseed[i] = i; } shuffle(godseed); for (var i = 0; i < n2; i++) { var god = godseed.pop(); if (god < numTrees) { world[i] = new Entity('T', 0); treesAlive++; } else if (god < numTrees + numJacks) { world[i] = new Entity('L', 0); jacksAlive++; } else if (god < numTrees + numJacks + numBears) { world[i] = new Entity('B', 0); bearsAlive++; } // console.log(world, i); } // populate zoo array, lumber array for (var i = 0; i < zooPeriod; i++) { zooqueue.push(0); } for (var i = 0; i < lumberPeriod; i++) { lumberqueue.push(0); } } animateWorld = function () { recentEvents = ''; computeWorld(); drawWorld(); time++;$('#p1').text(treesAlive + ' trees alive');
$('#p2').text(bearsAlive + ' bears alive');$('#p3').text(jacksAlive + ' lumberjacks alive');
$('#p4').text(recentEvents); }; function computeWorld() { zooqueue.push(lumberjacksMauled); lumberqueue.push(currentLumber); // Calculate entity positions for (var i = 0; i < n2; i++) { if (world[i]) { switch (world[i].type) { case 'B': bearStuff(i); break; case 'E': elderStuff(i); break; case 'L': lumberjackStuff(i); break; case 'T': treeStuff(i); break; case 'S': saplingStuff(i); break; } } } // Pop the # mauls from zooPeriod's ago, if lumberjacksMauled > oldmauls, then someone was eaten. var oldmauls = zooqueue.shift(); if (time % zooPeriod === 0) { if (lumberjacksMauled > oldmauls) { if (remove('B') == 1) { bearsAlive--; recentEvents += 'Bear sent to zoo! '; } } else { bearsAlive++; spawn('B'); recentEvents += 'New bear appeared! '; } } var oldLumber = lumberqueue.shift(); if (time % lumberPeriod === 0) { // # lumberjack to hire var hire = Math.floor((currentLumber - oldLumber) / jacksAlive); if (hire > 0) { recentEvents += 'Lumber jack hired! (' + hire + ') '; while (hire > 0) { jacksAlive++; spawn('L'); hire--; } } else { if (remove('L') == 1) { recentEvents += 'Lumber jack fired! '; jacksAlive--; } else { } } } // Ensure > 1 lumberjack if (jacksAlive === 0) { jacksAlive++; spawn('L'); recentEvent += 'Lumberjack spontaneously appeared'; } } // Not the job of spawn/remove to keep track of whatever was spawned/removed function spawn(type) { var index = findEmpty(type); if (index != -1) { world[index] = new Entity(type, time); } // recentEvents += 'Spawned a ' + type + '\n'; } function remove(type) { var index = findByType(type); if (index != -1) { world[index] = null; return 1; } return -1; // recentEvents += 'Removed a ' + type + '\n'; } // Searches in world for an entity with type=type. Currently implemented as // linear scan, which isn't very random function findByType(type) { for (var i = 0; i < n2; i++) { if (world[i] && world[i].type == type) return i; } return -1; } // Also linear scan function findEmpty(type) { for (var i = 0; i < n2; i++) { if (!world[i]) { return i; } } return -1; } function bearStuff(index) { if (world[index].birthday == time) { return; } // Wander around var tindex = index; for (var i = 0; i < lumberjackRange; i++) { var neighbors = get8Neighbor(tindex); var mov = neighbors[Math.floor(Math.random() * neighbors.length)]; if (world[mov] && world[mov].type == 'L') { recentEvents += 'Bear (' + index % 10 + ',' + Math.floor(index / 10) + ') mauled a Lumberjack (' + mov % 10 + ',' + Math.floor(mov / 10) + ') !'; lumberjacksMauled++; jacksAlive--; world[mov] = new Entity('B', time); world[mov].age = ++world[index].age; world[index] = null; return; } tindex = mov; } if (!world[tindex]) { world[tindex] = new Entity('B', time); world[tindex].age = ++world[index].age; world[index] = null; } } function elderStuff(index) { if (world[index].birthday == time) { return; } neighbors = get8Neighbor(index); // spawn saplings for (var i = 0; i < neighbors.length; i++) { if (!world[neighbors[i]]) { if (Math.random() < elderSaplingChance) { world[neighbors[i]] = new Entity('S', time); treesAlive++; } } } // become older world[index].age++; } function lumberjackStuff(index) { if (world[index].birthday == time) { return; } // Wander around var tindex = index; for (var i = 0; i < lumberjackRange; i++) { var neighbors = get8Neighbor(tindex); var mov = neighbors[Math.floor(Math.random() * neighbors.length)]; if (world[mov] && (world[mov].type == 'T' || world[mov].type == 'E')) { world[mov].type == 'T' ? currentLumber++ : currentLumber += 2; treesAlive--; world[mov] = new Entity('L', time); world[mov].age = ++world[index].age; world[index] = null; return; } tindex = mov; } if (!world[tindex]) { world[tindex] = new Entity('L', time); world[tindex].age = ++world[index].age; world[index] = null; } } function treeStuff(index) { if (world[index].birthday == time) { return; } neighbors = get8Neighbor(index); // spawn saplings for (var i = 0; i < neighbors.length; i++) { if (!world[neighbors[i]]) { if (Math.random() < spawnSaplingChance) { world[neighbors[i]] = new Entity('S', time); treesAlive++; } } } // promote to elder tree? if (world[index].age >= elderTreeAge) { world[index] = new Entity('E', time); return; } // become older world[index].age++; } function saplingStuff(index) { if (world[index].birthday == time) { return; } // promote to tree? if (world[index].age > treeAge) { world[index] = new Entity('T', time); return; } world[index].age++; } // Returns array containing up to 8 valid neighbors. // Prolly gonna break for n < 3 but oh well function get8Neighbor(index) { neighbors = []; if (index % n != 0) { neighbors.push(index - n - 1); neighbors.push(index - 1); neighbors.push(index + n - 1); } if (index % n != n - 1) { neighbors.push(index - n + 1); neighbors.push(index + 1); neighbors.push(index + n + 1); } neighbors.push(index - n); neighbors.push(index + n); return neighbors.filter(function (val, ind, arr) { return (0 <= val && val < n2) }); } // Each entity allocated 5x5px for their art function drawWorld() { context.clearRect(0, 0, canvas.width, canvas.height); for (var i = 0; i < n2; i++) { if (world[i]) { var x = i % n; var y = Math.floor(i / n); switch (world[i].type) { case 'B': drawBear(x, y); break; case 'E': drawElder(x, y); break; case 'L': drawJack(x, y); break; case 'T': drawTree(x, y); break; case 'S': drawSapling(x, y); break; } } } } function drawBear(x, y) { if (drawUsingColor) { drawRect(x * size, y * size, size, size, bearColor); } else { drawLetter(x * size, y * size, 'B'); } } function drawElder(x, y) { if (drawUsingColor) { drawRect(x * size, y * size, size, size, elderColor); } else { drawLetter(x * size, y * size, 'E'); } } function drawJack(x, y) { if (drawUsingColor) { drawRect(x * size, y * size, size, size, lumberjackColor); } else { drawLetter(x * size, y * size, 'J'); } } function drawTree(x, y) { if (drawUsingColor) { drawRect(x * size, y * size, size, size, treeColor); } else { drawLetter(x * size, y * size, 'T'); } } function drawSapling(x, y) { if (drawUsingColor) { drawRect(x * size, y * size, size, size, saplingColor); } else { drawLetter(x * size, y * size, 'S'); } } function drawLine(x1, y1, x2, y2, c) { context.beginPath(); context.moveTo(x1, y1); context.lineTo(x2, y2); context.lineWidth = 3; context.strokeStyle = c; context.stroke(); } function drawRect(x, y, w, h, c) { context.fillStyle = c; context.fillRect(x, y, w, h); } function drawLetter(x, y, l) { context.fillText(l, x, y); }$(document).ready(function () {
initWorld();
intervalID = window.setInterval(animateWorld, intervalTime);
/*\$('#s').click(function() {
animateWorld();
})*/
});

// http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function shuffle(array) {
var currentIndex = array.length,
temporaryValue, randomIndex;

// While there remain elements to shuffle...
while (0 !== currentIndex) {

// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;

// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}

• There is something strange with high-size forest (try n = 50, for instance). Jul 30, 2014 at 17:38
• @Blackhole something strange = ____ ? Jul 30, 2014 at 17:45
• Jul 30, 2014 at 20:18
• I notice the bears getting stuck, and the forest taking over the lower part of the map since I spawn lumberjacks in the upper part of the map Jul 30, 2014 at 20:20
• There is something wrong with the way new bears and lumberjacks spawn. I think putting then all in the top left corner changes the global balance of the system quite a bit.
– user16991
Jul 31, 2014 at 2:25

# Python

Nothing fancy. I kept on adding stuff, so refactoring might be in order. (And I didn't do unitest so bugs might still be present).

I gave random names to lumberjacks and bears. Trees are i, then I, then #, Lumberjacks are x, Bears are o

import os

from random import randint, choice, shuffle
from time import sleep

NGRID = 15
SLEEPTIME = 0.0125
DURATION = 4800
VERBOSE = True

###init
grid = [[[] for _ in range(NGRID)] for _ in range(NGRID)]
#Money earned this year
n_lumbers = 0
#Lumberjacks killed this year
n_maul = 0

tick = 0
events = []
#total number of
d_total = {'trees':0,
'lumberjacks': 0,
'bears': 0,
'cut': 0,
'maul': 0,
'capture': 0,
'lumbers': 0,
'fired': 0}

d_oldest = {'tree': 0,
'lumberjack': (0, ""),
'bear': (0, "")}

d_most = {'n_maul': (0, ""),
'n_lumber': (0, ""),
'n_cut': (0, "")}

d_year = {'n_maul': 0,
'n_lumber': 0}

###Classes
class Tree(object):
"""Represent a Sapling, Tree, or Elder Tree"""
def __init__(self, coords, m=0, t='Sapling'):
self.months = m
self.typeof = t
self.coords = coords
def grow(self, m=1):
"""the tree grows 1 month and its type might change"""
self.months = self.months + m
if self.months == 12:
self.typeof = 'Tree'
elif self.months == 480:
self.typeof = 'Elder Tree'
def __str__(self):
if self.typeof == 'Sapling':
return 'i'
elif self.typeof == 'Tree':
return 'I'
else:
return '#'

class Animated(object):
"""Animated beings can move"""
def __init__(self, coords):
self.coords = coords
self.old_coords = None
self.months = 0
def where(self):
return c_neighbors(self.coords)
def choose_new_coords(self):
self.old_coords = self.coords
possible = self.where()
if possible:
direction = choice(self.where())
self.coords = [(self.coords[i]+direction[i]) % NGRID for i in range(2)]
#    def __del__(self):
#        print "died at "+ str(self.coords)

class Lumberjack(Animated):
"""Lumberjacks chop down trees"""
def __init__(self, coords):
super(Lumberjack, self).__init__(coords)
self.nb_cut = 0
self.nb_lumber = 0
self.name = gen_name("l")
def __str__(self):
return "x"

class Bear(Animated):
"""Bears maul"""
def __init__(self, coords):
super(Bear, self).__init__(coords)
self.nb_maul = 0
self.name = gen_name("b")
def where(self):
return c_land_neighbors(self.coords)
def __str__(self):
return "o"

###list of coords
def c_neighbors(coords):
"""returns the list of coordinates of adjacent cells"""
return [[(coords[0] + i) % NGRID, (coords[1] + j) % NGRID] \
for i in [-1, 0, 1] \
for j in [-1, 0, 1] \
if (i,j) != (0, 0)]

def c_empty_neighbors(coords):
"""returns the list of coordinates of adjacent cells that are empty """
return [[i, j] for [i,j] in c_neighbors(coords) if grid[i][j] == []]

def c_land_neighbors(coords):
"""returns the list of coordinates of adjacent cells that contain not Trees
for bears"""
return [[i, j] for [i,j] in c_neighbors(coords)\
if (grid[i][j] == []) or (not isinstance(grid[i][j][0], Tree))]

def c_empty_cells():
"""returns list of coords of empty cells in the grid"""
return [[i, j] for i in range(NGRID) for j in range(NGRID) if grid[i][j] == []]

def c_not_bear_cells():
"""returns list of coords of cells without bear"""
return [[i, j] for i in range(NGRID) for j in range(NGRID) \
if not isinstance(grid[i][j], Bear)]

###one less
def maul(lumberjack):
"""a lumberjack will die"""
global n_maul
n_maul = n_maul + 1
d_total['maul'] = d_total['maul'] + 1
remove_from_grid(lumberjack.coords, lumberjack)
return lumberjack.name + " is sent to hospital" + check_lumberjacks()

def capture_bear():
"""too many mauls, a Zoo traps a bear"""
d_total['capture'] = d_total['capture'] + 1
bear = choice(get_bears())
remove_from_grid(bear.coords, bear)
return bear.name + " has been captured"

def fire_lumberjack():
"""the job is not done correctly, one lumberjack is let go"""
d_total['fired'] = d_total['fired'] + 1
lumberjack = choice(get_lumberjacks())
remove_from_grid(lumberjack.coords, lumberjack)
return lumberjack.name + " has been fired" + check_lumberjacks()

def remove_from_grid(coords, item):
"""remove item from the grid at the coords"""
grid[coords[0]][coords[1]].remove(item)
del item

###one more
def new_animate(class_):
"""a new lumberjack or bear joins the forest"""
if class_==Bear:
d_total['bears'] = d_total['bears'] + 1
x, y = choice(c_empty_cells())
else:
d_total['lumberjacks'] = d_total['lumberjacks'] + 1
x, y = choice(c_not_bear_cells())
new_being = class_([x,y])
grid[x][y].append(new_being)
return "a new " + class_.__name__ + " enters the forest: " + new_being.name

def check_lumberjacks():
"""we will never reduce our Lumberjack labor force below 0"""
if len(get_lumberjacks())==0:
return " - no more lumberjack, " + new_animate(Lumberjack)
return ""

###movements
def move_on_grid(being):
[x, y] = being.old_coords
grid[x][y].remove(being)
[x, y] = being.coords
grid[x][y].append(being)

def move_lumberjack(lumberjack):
"""Lumberjacks move 3 times if they don't encounter a (Elder) Tree or a Bear"""
global n_lumbers
for _ in range(3):
lumberjack.choose_new_coords()
move_on_grid(lumberjack)
[x, y] = lumberjack.coords
#is there something at the new coordinate?
#move append so this lumberjack is at the end
if grid[x][y][:-1] != []:
if isinstance(grid[x][y][0], Tree):
the_tree = grid[x][y][0]
price = worth(the_tree)
if price > 0:
lumberjack.nb_cut = lumberjack.nb_cut + 1
d_most['n_cut'] = max((lumberjack.nb_cut, lumberjack.name), \
d_most['n_cut'])
d_total['cut'] = d_total['cut'] + 1
n_lumbers = n_lumbers + price
d_total['lumbers'] = d_total['lumbers'] + 1
lumberjack.nb_lumber = lumberjack.nb_lumber + price
d_most['n_lumber'] = max(d_most['n_lumber'], \
(lumberjack.nb_lumber, lumberjack.name))
remove_from_grid([x, y], the_tree)
return lumberjack.name + " cuts 1 " + the_tree.typeof
#if there is a bear, all lumberjacks have been sent to hospital
if isinstance(grid[x][y][0], Bear):
#the first bear is the killer
b = grid[x][y][0]
b.nb_maul = b.nb_maul + 1
d_most['n_maul'] = max((b.nb_maul, b.name), d_most['n_maul'])
return maul(lumberjack)
return None

def move_bear(bear):
"""Bears move 5 times if they don't encounter a Lumberjack"""
for _ in range(5):
bear.choose_new_coords()
move_on_grid(bear)
[x, y] = bear.coords
there_was_something = (grid[x][y][:-1] != [])
if there_was_something:
#bears wander where there is no tree
#so it's either a lumberjack or another bear
#can't be both.
if isinstance(grid[x][y][0], Lumberjack):
bear.nb_maul = bear.nb_maul + 1
d_most['n_maul'] = max((bear.nb_maul, bear.name), \
d_most['n_maul'])
return maul(grid[x][y][0])
return None

###get objects
def get_objects(class_):
"""get a list of instances in the grid"""
l = []
for i in range(NGRID):
for j in range(NGRID):
if grid[i][j]:
for k in grid[i][j]:
if isinstance(k, class_):
l.append(k)
return l

def get_trees():
"""list of trees"""
return get_objects(Tree)

def get_bears():
"""list of bears"""
return get_objects(Bear)

def get_lumberjacks():
"""list of lumberjacks"""
return get_objects(Lumberjack)

###utils
def gen_name(which="l"):
"""generate random name"""
name = ""
for _ in range(randint(1,4)):
name = name + choice("bcdfghjklmnprstvwxz") + choice("auiey")
if which == "b":
name = name[::-1]
return name.capitalize()

def worth(tree):
"""pieces for a tree"""
if tree.typeof == 'Elder Tree':
return 2
if tree.typeof == 'Tree':
return 1
return 0

def one_month():
"""a step of one month"""
events = []
global tick
tick = tick + 1
#each Tree can spawn a new sapling
for t in get_trees():
l_empty_spaces = c_empty_neighbors(t.coords)
percent = 10 if t.typeof == 'Tree' else \
20 if t.typeof == 'Elder Tree' else 0
if (randint(1,100) < percent):
if l_empty_spaces:
[x, y] = choice(l_empty_spaces)
grid[x][y] = [Tree([x,y])]
d_total['trees'] = d_total['trees'] + 1
t.grow()
d_oldest['tree'] = max(t.months, d_oldest['tree'])
#each lumberjack/bear moves
for l in get_lumberjacks():
l.months = l.months + 1
d_oldest['lumberjack'] = max((l.months, l.name), \
d_oldest['lumberjack'])
event = move_lumberjack(l)
if event:
events.append(event)
for b in get_bears():
b.months = b.months + 1
d_oldest['bear'] = max((b.months, b.name), d_oldest['bear'])
event = move_bear(b)
if event:
events.append(event)
return events

def print_grid():
"""print the grid
if more than 1 thing is at a place, print the last.
At 1 place, there is
- at most a tree and possibly several lumberjack
- or 1 bear
"""
print "-" * 2 * NGRID
print '\n'.join([' '.join([str(i[-1]) if i != [] else ' ' \
for i in line]) \
for line in grid])
print "-" * 2 * NGRID

def clean():
"""clear the console"""
os.system('cls' if os.name == 'nt' else 'clear')

def print_grid_and_events():
"""print grid and list of events"""
clean()
print_grid()
if VERBOSE:
print '\n'.join(events)
print "-" * 2 * NGRID

###populate the forest
l = c_empty_cells()
shuffle(l)
for x, y in l[:((NGRID*NGRID) / 2)]:
grid[x][y] = [Tree([x, y], 12, 'Tree')]
d_total['trees'] = d_total['trees'] + 1

l = c_empty_cells()
shuffle(l)
for x, y in l[:((NGRID*NGRID) / 10)]:
grid[x][y] = [Lumberjack([x, y])]
d_total['lumberjacks'] = d_total['lumberjacks'] + 1

l = c_empty_cells()
shuffle(l)
for x, y in l[:((NGRID*NGRID) / 10)]:
grid[x][y] = [Bear([x, y])]
d_total['bears'] = d_total['bears'] + 1

###time goes on
while (tick <= DURATION and len(get_trees())>0):
events = one_month()
#end of the year
if (tick % 12)==0:
events.append("End of the year")
#lumber tracking
nlumberjacks = len(get_lumberjacks())
events.append(str(n_lumbers) + " lumbers VS " +\
str(nlumberjacks) + " Lumberjacks")
if n_lumbers >= nlumberjacks:
n_hire = n_lumbers/nlumberjacks
events.append("we hire " + str(n_hire) +\
" new Lumberjack" + ("s" if (n_hire > 1) else ""))
for _ in range(n_hire):
events.append(new_animate(Lumberjack))
else:
events.append(fire_lumberjack())
d_year['n_lumber'] = max(d_year['n_lumber'], n_lumbers)
n_lumbers = 0
#maul tracking
events.append("maul this year: " + str(n_maul))
if n_maul == 0:
events.append(new_animate(Bear))
else:
events.append(capture_bear())
d_year['n_maul'] = max(d_year['n_maul'], n_maul)
n_maul = 0
print_grid_and_events()
sleep(SLEEPTIME)

print "-"*70
print "End of the game"
print "-"*70
print "month:" + str(tick - 1)
print "number of trees still alive: " + str(len(get_trees()))
print "number of lumberjacks still alive: " + str(len(get_lumberjacks()))
print "number of bears still alive: " + str(len(get_bears()))

print "-"*70
print "oldest Tree ever is/was: " + str(d_oldest['tree'])
print "oldest Lumberjack ever is/was: " + str(d_oldest['lumberjack'][0]) + \
" yo " + d_oldest['lumberjack'][1]
print "oldest Bear ever is/was: " + str(d_oldest['bear'][0]) + \
" yo " + d_oldest['bear'][1]
print "-"*70
print "max cut by a Lumberjack: " + str(d_most['n_cut'][0]) + \
" by " + str(d_most['n_cut'][1])
print "max lumber by a Lumberjack: " + str(d_most['n_lumber'][0]) + \
" by " + str(d_most['n_lumber'][1])
print "max maul by a Bear: " + str(d_most['n_maul'][0]) + \
" by " + str(d_most['n_maul'][1])
print "-"*70
print "max lumber in a year: " + str(d_year['n_lumber'])
print "max maul in a year: " + str(d_year['n_maul'])
print "-"*70
print "Total of:"
for i, j in d_total.items():
print i, str(j)


Some outputs:

------------------------------
x

I
I
x   i                     I

i i i   i       I I x i
i   I   I i I I   i i     o
i i I I I           i
i I   x i
I I   I I
I     I i
x
------------------------------
Dy is sent to hospital
Lehuniru cuts 1 Tree
------------------------------


End of the year

------------------------------
x

x

i I     I
i     I               x
I
i i
x                   I I
i I   i I     i       i
I         i I
i x i
I         i I
o
x
------------------------------
Fuha cuts 1 Tree
Ka cuts 1 Tree
Ky is sent to hospital
End of the year
11 lumbers VS 4 Lumberjacks
we hire 2 new Lumberjacks
a new Lumberjack enters the forest: Di
a new Lumberjack enters the forest: Dy
maul this year: 6
Evykut has been captured
------------------------------


End of the game

------------------------------
x
i
x     x

x                 x
x i     x
i               I
I i x x   I i           x
x   i   i
x i i i i I
i i I   I i I   i
I       i     i i
i   x   i
I   i I
I I   x i   I I         x
------------------------------
Vanabixy cuts 1 Tree
Fasiguvy cuts 1 Tree
------------------------------
----------------------------------------------------------------------
End of the game
----------------------------------------------------------------------
month:4800
number of trees still alive: 36
number of lumberjacks still alive: 15
number of bears still alive: 0
----------------------------------------------------------------------
oldest Tree ever is/was: 129
oldest Lumberjack ever is/was: 308 yo Cejuka
oldest Bear ever is/was: 288 yo Ekyx
----------------------------------------------------------------------
max cut by a Lumberjack: 44 by Cejuka
max lumber by a Lumberjack: 44 by Cejuka
max maul by a Bear: 52 by Ekyx
----------------------------------------------------------------------
max lumber in a year: 84
max maul in a year: 86
----------------------------------------------------------------------
Total of:
bears 211
cut 5054
fired 67
capture 211
lumberjacks 1177
lumbers 5054
maul 1095
trees 5090