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!
<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) { Forest.add.tree(new Tree(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) { Forest.add.lumberjack(new Lumberjack(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) { Forest.add.bear(new Bear(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; }; this.add = { 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); };}]);