Atualizado conforme solicitação popular
Comportamento geral
O programa agora é um pouco interativo.
O código fonte é completamente parametrizado, para que você possa ajustar mais alguns parâmetros internos com o seu editor de texto favorito.
Você pode alterar o tamanho da floresta.
É necessário um mínimo de 2 para ter espaço suficiente para colocar uma árvore, um lenhador e um urso em 3 locais diferentes, e o máximo é arbitrariamente fixado em 100 (o que fará o computador médio rastejar).
Você também pode alterar a velocidade da simulação.
A exibição é atualizada a cada 20 ms; portanto, um intervalo de tempo maior produzirá animações mais refinadas.
Os botões permitem parar / iniciar a simulação ou executá-la por um mês ou um ano.
O movimento dos habitantes da floresta agora é um pouco animado. Eventos de corte e corte de árvores também são figurados.
Um log de alguns eventos também é exibido. Mais algumas mensagens estão disponíveis se você alterar o nível de verbosidade, mas isso o inundaria com notificações "Bob corta mais uma árvore".
Eu preferiria não fazer isso se fosse você, mas não sou, então ...
Ao lado do playground, um conjunto de gráficos em escala automática é desenhado:
- populações de ursos e lenhadores
- número total de árvores, divididas em mudas, árvores maduras e mais velhas
A legenda também exibe as quantidades atuais de cada item.
Estabilidade do sistema
Os gráficos mostram que as condições iniciais não escalam tão graciosamente. Se a floresta é muito grande, muitos ursos dizimam a população de lenhadores até que muitos amantes de panquecas sejam colocados atrás das grades. Isso causa uma explosão inicial de árvores mais velhas, que por sua vez ajuda a população de lenhadores a se recuperar.
Parece que 15 é o tamanho mínimo para a floresta sobreviver. Uma floresta de tamanho 10 geralmente será destruída após algumas centenas de anos. Qualquer tamanho acima de 30 produzirá um mapa quase cheio de árvores. Entre 15 e 30, você pode ver a população de árvores oscilando significativamente.
Alguns pontos de regra discutíveis
Nos comentários do post original, parece que vários bípedes não devem ocupar o mesmo lugar. Isso contradiz de alguma forma a regra de um caipira vagando em um amador de panqueca.
De qualquer forma, não segui essa diretriz. Qualquer célula da floresta pode conter qualquer número de desidratantes (e exatamente zero ou uma árvore). Isso pode ter algumas conseqüências na eficiência do lenhador: suspeito que permita que eles cavem mais facilmente um grupo de árvores mais velhas. Quanto aos ursos, não espero que isso faça muita diferença.
Também optei por sempre ter pelo menos um lenhador na floresta, apesar do argumento de que a população caipira poderia chegar a zero (disparar o último lenhador no mapa se a colheita fosse realmente ruim, o que nunca acontecerá, a menos que a floresta tenha sido destruída). cortada até a extinção).
Tweaking
Para alcançar a estabilidade, adicionei dois parâmetros de ajustes:
1) taxa de crescimento dos lenhadores
um coeficiente aplicado à fórmula que fornece o número de lenhadores extras contratados quando há madeira suficiente. Defina como 1 para voltar à definição original, mas achei um valor de cerca de 0,5 que permitia que a floresta (especialmente as árvores mais velhas) se desenvolvesse melhor.
2) critério de remoção de ursos
um coeficiente que define a porcentagem mínima de lenhadores atacados para enviar um urso ao zoológico. Defina como 0 para voltar à definição original, mas essa eliminação drástica do urso basicamente limitará a população a um ciclo de oscilação de 0-1. Eu o defino em 0,15 (ou seja, um urso é removido apenas se 15% ou mais dos lenhadores foram atacados este ano). Isso permite uma população moderada de ursos, suficiente para impedir que os caipiras limpem a área, mas ainda permitindo que uma parte considerável da floresta seja cortada.
Como nota lateral, a simulação nunca para (mesmo após os 400 anos necessários). Poderia facilmente fazê-lo, mas não o faz.
O código
O código está totalmente contido em uma única página HTML.
Ele deve ser codificado em UTF-8 para exibir os símbolos unicode adequados para ursos e lenhadores.
Para sistemas com problemas de Unicode (por exemplo, Ubuntu): encontre as seguintes linhas:
jack :{ pic: '🙎', color:'#bc0e11' },
bear :{ pic: '🐻', color:'#422f1e' }},
e alterar os pictogramas para personagens mais fácil de display ( #
, *
, qualquer que seja)
<!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+" ";
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>
Qual o proximo?
Mais comentários ainda são bem-vindos.
NB: Estou ciente de que o número de árvores jovens / maduras / mudas ainda é um pouco confuso, mas que diabos.
Além disso, acho document.getElementById mais legível que $, portanto, não é necessário reclamar da falta de jQueryisms. É jQuery grátis de propósito. Cada um na sua, certo?
Note that you will never reduce your Lumberjack labor force below 0
no item 3. da lista da seção de lenhador, talvez mude para 1 para ficar de acordo com o que você menciona na seção de ursos?