<!DOCTYPE html>
<html>
<head>
<link data-require="jasmine@1.3.1" data-semver="1.3.1" rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css" />
<script data-require="jasmine@1.3.1" data-semver="1.3.1" src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script data-require="jasmine@1.3.1" data-semver="1.3.1" src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script data-require="json2@*" data-semver="0.0.2012100-8" src="//cdnjs.cloudflare.com/ajax/libs/json2/20121008/json2.js"></script>
<script data-require="jquery@2.0.3" data-semver="2.0.3" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="jasmine.css" />
<script src="script.js"></script>
<!--
<script src="tests.js"></script>
<script src="jasmineBoot.js"></script>
-->
</head>
<body>
<h1>A World of Sims</h1>
<fieldset>
<legend>Health Thresholds for Life Events</legend>
<div>
<label>Reproduction</label>
<input type="number" id="reproduction" />
</div>
<div>
<label>Death</label>
<input type="number" id="death" />
</div>
</fieldset>
<fieldset>
<legend>Health Changes on Encounters</legend>
<div>
<label class="wide">If heal when other heals</label>
<input type="number" id="healHeal" />
<label class="continue">...when other strikes</label>
<input type="number" id="healStrike" />
</div>
<div>
<label class="wide">If strike when other heals</label>
<input type="number" id="strikeHeal" />
<label class="continue">...when other strikes</label>
<input type="number" id="strikeStrike" />
</div>
</fieldset>
<div>
<input type="button" id="play" value="Play" onclick="play()" />
</div>
<svg id="world" width="600" height="600" style="border:1px solid grey; background-color: white">
<text x="20" y="80" transform="rotate(270 30, 80)">High</text>
<text x="20" y="340" transform="rotate(270 30, 340)" class="axis">Aggressiveness</text>
<text x="20" y="500" transform="rotate(270 30, 500)">Low</text>
<text x="90" y="570" >Negative</text>
<text x="245" y="570" class="axis">Responsiveness</text>
<text x="480" y="570">Positive</text>
</svg>
<script src="play.js"></script>
</body>
</html>
// Namespace
var Fws = Fws || {};
Fws.utilities = (function() {
return {
// Fisher-Yates shuffle from http://bost.ocks.org/mike/shuffle/
shuffle: function(ary) {
var m = ary.length, t, i;
// While there remain elements to shuffle…
while (m) {
// Pick a remaining element…
i = Math.floor(Math.random() * m--);
// And swap it with the current element.
t = ary[m];
ary[m] = ary[i];
ary[i] = t;
}
return ary;
}
}
})();
Fws.parameters = {
// When a sim reaches these health points, something happens.
lifeEventThresholds : {
reproduction: 3300,
death: -750
},
// Changes in health upon encountering another sim
healthChanges: {
ifHealWhenOtherHeals: 500,
ifHealWhenOtherStrikes: -600,
ifStrikeWhenOtherHeals: 100,
ifStrikeWhenOtherStrikes: 0
}
};
Fws.sim = (function() {
return {
messages: {
aggressivenessRequired: 'The aggressiveness parameter is required.',
responsivenessRequired: 'The responsiveness parameter is required.',
aggressivenessOutOfRange: 'Aggressiveness must be from 0 to 1, inclusive.',
responsivenessOutOfRange: 'Responsiveness must be from -1 to 1, inclusive.',
expectedAction: 'Expected one of the Fws.sim.actions values as a parameter.'
},
actions: {
strike: 'strike',
heal: 'heal',
},
getInitialHealth: function() { return 0; },
getReproductionThreshold: function() {
return Fws.parameters.lifeEventThresholds.reproduction;
},
getDeathThreshold: function() {
return Fws.parameters.lifeEventThresholds.death;
},
getHealthChangeIfHealWhenOtherHeals: function() {
return Fws.parameters.healthChanges.ifHealWhenOtherHeals;
},
getHealthChangeIfHealWhenOtherStrikes: function() {
return Fws.parameters.healthChanges.ifHealWhenOtherStrikes;
},
getHealthChangeIfStrikeWhenOtherHeals: function() {
return Fws.parameters.healthChanges.ifStrikeWhenOtherHeals;
},
getHealthChangeIfStrikeWhenOtherStrikes: function() {
return Fws.parameters.healthChanges.ifStrikeWhenOtherStrikes;
},
// Create a sim.
// aggressiveness - A value from 0 through 1 that is the probability this
// sim will strike, independent of the other sim's reputation.
// responsiveness - A value from -1 through 1 that indicates how likely this
// sim is to strike, given another sim's reputation. 0 means this sim
// does not care about the other. +1 means it matches the other.
// -1 means it does the opposite.
// initialReputation - Optional starting value for initialReputation.
// This value becomes meaningless after the first encounter. It is
// intended to facilitate unit testing. If not provided, the
// default is the aggressiveness value.
create: function(aggressiveness, responsiveness,initialReputation) {
if (aggressiveness === undefined) {
throw new Error(this.messages.aggressivenessRequired);
}
if (responsiveness === undefined) {
throw new Error(this.messages.responsivenessRequired);
}
if (aggressiveness < 0 || aggressiveness >1) {
throw new Error(this.messages.aggressivenessOutOfRange);
}
if (responsiveness < -1 || responsiveness >1) {
throw new Error(this.messages.responsivenessOutOfRange);
}
var health = this.getInitialHealth();
var numberOfEncounters = 0;
var numberOfStrikes = 0;
var reputation;
// Slope and intercept of a line whose domain is the other sim's
// reputation and whose range is the probability of striking.
var intercept = aggressiveness;
var slope = (responsiveness > 0)
? (1 - aggressiveness) * responsiveness
: aggressiveness * responsiveness;
return {
// Get the health level of this sim.
getHealth: function() {
return health;
},
incrementHealth: function(incr) {
health += incr;
},
incrementCounters: function(didStrike) {
++numberOfEncounters;
if (didStrike) {
++numberOfStrikes;
}
},
getAggressiveness: function() {
return aggressiveness;
},
getResponsiveness: function() {
return responsiveness;
},
// Get the reputation, which is the probability this sim will strike,
// based on previous encounters.
getReputation: function () {
return numberOfEncounters > 0
? numberOfStrikes / numberOfEncounters
: initialReputation !== undefined ? initialReputation : aggressiveness;
},
// Returns one of this.actions upon encountering the ohter sim.
getAction: function(otherSim) {
var r = Math.random();
return ( slope * otherSim.getReputation() + intercept ) > r
? Fws.sim.actions.strike
: Fws.sim.actions.heal;
}
};
},
getHealthChanges: function(actionA, actionB) {
if (actionA == this.actions.heal) {
if (actionB == this.actions.heal) {
return [ this.getHealthChangeIfHealWhenOtherHeals(), this.getHealthChangeIfHealWhenOtherHeals() ];
}
if (actionB == this.actions.strike) {
return [ this.getHealthChangeIfHealWhenOtherStrikes(), this.getHealthChangeIfStrikeWhenOtherHeals() ];
}
}
if (actionA == this.actions.strike) {
if (actionB == this.actions.heal) {
return [ this.getHealthChangeIfStrikeWhenOtherHeals(), this.getHealthChangeIfHealWhenOtherStrikes() ];
}
if (actionB == this.actions.strike) {
return [ this.getHealthChangeIfStrikeWhenOtherStrikes(), this.getHealthChangeIfStrikeWhenOtherStrikes() ];
}
}
throw new Error(this.messages.expectedAction);
},
encounter: function(simA, simB) {
var actionA = simA.getAction(simB);
var actionB = simB.getAction(simA);
var healthChanges = this.getHealthChanges(actionA, actionB);
simA.incrementHealth(healthChanges[0]);
simB.incrementHealth(healthChanges[1]);
simA.incrementCounters(actionA==this.actions.strike);
simB.incrementCounters(actionB==this.actions.strike);
}
};
})();
Fws.population = (function() {
return {
createEvenlyDistributed: function(numberInPopulation) {
var sims = [];
var sqrtPop = Math.floor(Math.sqrt(numberInPopulation));
var halfAggWidth = 0.5 * 1/sqrtPop;
var halfRespWidth = 0.5 * 2 / sqrtPop;
for (var agg=0; agg<sqrtPop; ++agg) {
for (var resp=0; resp<sqrtPop; ++resp) {
var aggressiveness = agg / sqrtPop + halfAggWidth;
var responsiveness = -1 + 2 * resp / sqrtPop + halfRespWidth;
sims.push(Fws.sim.create(aggressiveness, responsiveness));
}
}
while (sims.length < numberInPopulation) {
sims.push(Fws.sim.create(0.5,0));
}
return {
// Return a copy of the population.
getCopyOfSims: function (){
return sims.slice();
},
add: function(sim) {
sims.push(sim);
},
getSimCount: function() {
return sims.length;
},
getSim: function(index) {
return sims[index];
},
clone: function(sim) {
sims.push(Fws.sim.create(sim.getAggressiveness(), sim.getResponsiveness()));
},
randomlyEncounter: function() {
var encounterIndexes = new Array(sims.length);
for (var ix=0; ix<encounterIndexes.length; ++ix) {
encounterIndexes[ix] = ix;
}
Fws.utilities.shuffle(encounterIndexes);
for (ix=0; ix<encounterIndexes.length-1; ix += 2) {
Fws.sim.encounter(sims[encounterIndexes[ix]],sims[encounterIndexes[ix+1]]);
}
},
reproduceAndDie: function() {
for (var ix=sims.length-1; ix>=0; --ix) {
if (sims[ix].getHealth() >= Fws.sim.getReproductionThreshold()) {
this.clone(sims[ix]);
}
}
sims = sims.filter(function(s) {
return s.getHealth() > Fws.sim.getDeathThreshold();
});
},
// Returns a histogram as an array of arrays.
// histogram[a][r] gives the number of sims whose
// aggressiveness falls into the a'th division of the histogram's first index and whose
// responsiveness falls into the r'th division of the histogram's second index.
computeHistogram: function(aggressivenessResolution, responsivenessResolution) {
var aggressivenessCells = 1 / aggressivenessResolution;
var responsivenessCells = 2 / responsivenessResolution;
var histogram = [];
for (var i=0; i<aggressivenessCells; ++i) {
var array = new Array(responsivenessCells);
for (var j=0; j<array.length; ++j) {
array[j] = 0;
}
histogram.push(array);
}
var aggRangePerCell = 1 / aggressivenessCells;
var rspRangePerCell = 2 / responsivenessCells;
sims.forEach(function(s) {
var ixAgg = Math.min(Math.floor(s.getAggressiveness() / aggRangePerCell), aggressivenessCells-1);
var ixRsp = Math.min(Math.floor( (s.getResponsiveness()+1) / rspRangePerCell), responsivenessCells-1);
++(histogram[ixAgg][ixRsp]);
});
return histogram;
},
};
},
};
})();
/* Styles go here */
table {
width: 200px;
}
td {
height: 20px;
margin: 2px;
background-color: blue;
}
fieldset {
margin-bottom: 10px;
padding-left: 40px;
width: 535px;
}
legend {
font-weight: bold;
}
label {
width: 100px;
display: inline-block;
}
.wide {
width: 180px;
}
.continue {
width: auto;
margin-left: 20px;
}
input[type='number'] {
width: 70px;
text-align: right;
}
#play {
width: 100px;
height: 40px;
font-size: large;
font-weight: bold;
margin-bottom: 10px;
color: white;
background-color: rgb(91,183,91);
}
#play:disabled {
background-color: gray;
}
text {
fill: firebrick;
font-size: 20px;
font-family: sans-serif;
}
text.axis {
font-weight: bold;
}
describe('Fws.sim', function() {
var sim = Fws.sim;
var removeSpy = function(spy) {
spy.baseObj[spy.methodName] = spy.originalValue;
};
var doWithRandoms = function(arrayOfRandoms, func) {
var ixRandom = 0;
var spy = spyOn(Math,'random').andCallFake(function() {
return arrayOfRandoms[ixRandom];
});
for (ixRandom=0; ixRandom<arrayOfRandoms.length; ++ixRandom) {
func();
}
removeSpy(spy);
};
describe('create(aggressiveness, responsiveness [,initialReputation])', function() {
it('sets the initial health value correctly', function() {
expect(sim.create(0,0).getHealth()).toBe(sim.getInitialHealth());
});
it('defaults the initial reputation to the aggressiveness value', function() {
expect(sim.create(0.25,0).getReputation()).toBe(0.25);
});
it('can set the initial reputation with the third parameter', function() {
expect(sim.create(0,0,0.75).getReputation()).toBe(0.75);
});
it('requires the aggressiveness parameter', function(){
expect(function() {sim.create();}).toThrow(sim.messages.aggressivenessRequired);
});
it('requires the responsiveness parameter', function(){
expect(function() {sim.create(0);}).toThrow(sim.messages.responsivenessRequired);
});
it('requires the aggressiveness parameter to be in the range [0,1]', function() {
expect(function() {sim.create(0,0);}).not.toThrow();
expect(function() {sim.create(1,0);}).not.toThrow();
expect(function() {sim.create(-0.1,0);}).toThrow(sim.messages.aggressivenessOutOfRange);
expect(function() {sim.create(1.1,0);}).toThrow(sim.messages.aggressivenessOutOfRange);
});
it('requires the responsiveness parameter to be in the range [-1,1]', function(){
expect(function() {sim.create(0,-1); }).not.toThrow();
expect(function() {sim.create(0,1); }).not.toThrow();
expect(function() {sim.create(0,-1.1); }).toThrow(sim.messages.responsivenessOutOfRange);
expect(function() {sim.create(0,1.1); }).toThrow(sim.messages.responsivenessOutOfRange);
});
});
describe('getAction(otherSim)', function() {
var healer , striker;
var alwaysDoesAction = function(func, expectedAction) {
var randoms = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.99999999];
doWithRandoms(randoms, function() {
expect(func()).toBe(expectedAction);
});
};
var alwaysHeals = function(func) {
alwaysDoesAction(func, sim.actions.heal);
};
var alwaysStrikes = function(func) {
alwaysDoesAction(func, sim.actions.strike);
};
beforeEach(function() {
healer = sim.create(0,0);
spyOn(healer,'getReputation').andReturn(0);
striker = sim.create(1,0);
spyOn(striker,'getReputation').andReturn(1);
});
describe('with responsiveness set to 0', function() {
it('always returns strike if aggressiveness was set to 1', function() {
alwaysStrikes(function() { return striker.getAction(healer);});
alwaysStrikes(function() { return striker.getAction(striker);});
});
it('always returns heal if aggressiveness was set to 0', function() {
alwaysHeals(function() { return healer.getAction(striker);});
alwaysHeals(function() { return healer.getAction(healer);});
});
it('returns strike a fifth of the time if aggressiveness was set to 0.2', function() {
var a = sim.create(0.2,0);
doWithRandoms([ 0.2, 0.21, 0.3, 0.4, 0.999], function(r) {
var actual = a.getAction(striker);
var expected = r<0.2 ? sim.actions.strike : sim.actions.heal;
expect(actual).toBe(expected);
});
});
});
describe('with aggressiveness set to 0', function() {
it('heals healers and strikes strikers if responsiveness was set to 1', function() {
doWithRandoms([0, 0.5, 0.99], function() {
var a = sim.create(0,1);
expect(a.getAction(striker)).toBe(sim.actions.strike);
expect(a.getAction(healer)).toBe(sim.actions.heal);
});
});
it('heals everyone if responsiveness was set to -1', function() {
doWithRandoms([0, 0.5, 0.99], function() {
var a = sim.create(0,-1);
expect(a.getAction(striker)).toBe(sim.actions.heal);
expect(a.getAction(healer)).toBe(sim.actions.heal);
});
});
});
describe('with aggressiveness set to 0.2 and other sim with reputation of 0.75', function () {
var twentieths = [0,0.05,0.1,0.15,0.2,0.25, 0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9,0.95];
it('strikes 80% of the time if responsiveness was set to 1', function() {
var subjectSim = sim.create(0.2, 1);
doWithRandoms(twentieths, function() {
sim.encounter(subjectSim, sim.create(0,0,0.75));
});
expect(subjectSim.getReputation()).toBe(0.8);
});
it('strikes 5% of the time if responsiveness was set to -1', function() {
var subjectSim = sim.create(0.2, -1);
doWithRandoms(twentieths, function() {
sim.encounter(subjectSim, sim.create(0,0,0.75));
});
expect(subjectSim.getReputation()).toBe(0.05);
});
});
});
describe('getAggressiveness()', function() {
it('returns the value passed to the constructor', function() {
expect(sim.create(0.2,0).getAggressiveness()).toBe(0.2);
});
});
describe('getResponsiveness()', function() {
it('returns the value passed to the constructor', function() {
expect(sim.create(0,-0.5).getResponsiveness()).toBe(-0.5);
});
});
describe('getHealthChanges(actionA, actionB)', function() {
it('returns expected changes if both heal', function() {
var actual = sim.getHealthChanges(sim.actions.heal, sim.actions.heal);
var expected = [sim.getHealthChangeIfHealWhenOtherHeals(), sim.getHealthChangeIfHealWhenOtherHeals()];
expect(actual).toEqual(expected);
});
it('returns expected changes if A heals and B strikes', function() {
var actual = sim.getHealthChanges(sim.actions.heal, sim.actions.strike);
var expected = [sim.getHealthChangeIfHealWhenOtherStrikes(), sim.getHealthChangeIfStrikeWhenOtherHeals()];
expect(actual).toEqual(expected);
});
it('returns expected changes if both strike', function() {
var actual = sim.getHealthChanges(sim.actions.strike, sim.actions.strike);
var expected = [sim.getHealthChangeIfStrikeWhenOtherStrikes(), sim.getHealthChangeIfStrikeWhenOtherStrikes()];
expect(actual).toEqual(expected);
});
it('returns expected changes if A strikes and B heals', function() {
var actual = sim.getHealthChanges(sim.actions.strike, sim.actions.heal);
var expected = [ sim.getHealthChangeIfStrikeWhenOtherHeals(), sim.getHealthChangeIfHealWhenOtherStrikes()];
expect(actual).toEqual(expected);
});
it('throws if the first action is invalid', function() {
expect(function() {sim.getHealthChanges('invalid', sim.actions.heal); })
.toThrow(sim.messages.expectedAction);
});
it('throws if the second action is invalid', function() {
expect(function() {sim.getHealthChanges(sim.actions.heal, 'invalid'); })
.toThrow(sim.messages.expectedAction);
});
});
describe('encounter(sim1, sim2)', function() {
it('changes both healths per the getHealthChanges function', function() {
var a = sim.create(0,0);
var b = sim.create(0,0);
var initialHealth = a.getHealth();
spyOn(sim,'getHealthChanges').andReturn([1000, -1000]);
sim.encounter(a,b);
expect(a.getHealth()).toBe(initialHealth + 1000);
expect(b.getHealth()).toBe(initialHealth - 1000);
});
it('changes both reputations', function() {
var simStrikesThreeQuarters = sim.create(0.75,0);
var simStrikesOneQuarter = sim.create(0.25,0);
var randoms = [0.2, 0.5, 0.5, 0.8];
doWithRandoms(randoms, function() {
sim.encounter(simStrikesThreeQuarters, simStrikesOneQuarter);
});
expect(simStrikesThreeQuarters.getReputation()).toBe(0.75);
expect(simStrikesOneQuarter.getReputation()).toBe(0.25);
});
})
});
describe('Fws.population', function() {
var population = Fws.population;
var sim = Fws.sim;
describe('getCopyOfSims()', function() {
it('returns a copy of the population, not the original', function() {
var count = 10;
var pop = population.createEvenlyDistributed(count);
var copy1 = pop.getCopyOfSims();
var copy2 = pop.getCopyOfSims();
expect(copy1).not.toBe(copy2);
expect(copy1).toEqual(copy2);
});
});
describe('add(sim)', function() {
it('appends the sim to the end of the population', function() {
var initialCount = 4;
var pop = population.createEvenlyDistributed(initialCount);
var newSim = Fws.sim.create(0.222, 0.333);
pop.add(newSim);
expect(pop.getSimCount()).toBe(initialCount+1);
expect(pop.getSim(initialCount)).toBe(newSim);
});
});
describe('getSimCount()', function() {
it('returns the number of sims in the population', function() {
var pop = population.createEvenlyDistributed(3);
expect(pop.getSimCount()).toBe(3);
});
});
describe('getSim(index)', function() {
it('returns the sim at the index', function() {
var pop = population.createEvenlyDistributed(0);
var sim1 = Fws.sim.create(0.1, 0.2);
var sim2 = Fws.sim.create(0.3, 0.4);
pop.add(sim1);
pop.add(sim2);
expect(pop.getSim(0)).toBe(sim1);
expect(pop.getSim(1)).toBe(sim2);
});
});
describe('clone(sim)', function() {
it("adds a sim to the population with the source sim's aggressiveness and responsiveness, but a default reputation", function() {
var population = Fws.population.createEvenlyDistributed(100);
var defaultReputation = population.getSim(0).getReputation();
population.randomlyEncounter(); // To move reputations off the default.
var sourceSim = population.getSim(0);
population.clone(sourceSim);
expect(population.getSimCount()).toBe(101);
var newSim = population.getSim(100);
expect(newSim.getAggressiveness()).toBe(sourceSim.getAggressiveness());
expect(newSim.getResponsiveness()).toBe(sourceSim.getResponsiveness());
expect(newSim.getReputation()).toBe(defaultReputation);
});
});
describe('createEvenlyDistributed(numberInPopulation)', function() {
it('creates the requested number if it is a perfect square', function() {
var pop = population.createEvenlyDistributed(100);
expect(pop.getSimCount()).toBe(100);
});
it('creates the requested number if not a perfect square', function() {
var pop = population.createEvenlyDistributed(103);
expect(pop.getSimCount()).toBe(103);
});
it('evenly distributes the aggressiveness and responsiveness values if population is a perfect square', function() {
var pop = population.createEvenlyDistributed(16);
var ixSim = -1;
for (var a=0.125; a<=0.875; a+=0.25) {
for (var r=-0.75; r<=0.75; r+=0.5) {
++ixSim;
expect(pop.getSim(ixSim).getAggressiveness()).toBe(a);
expect(pop.getSim(ixSim).getResponsiveness()).toBe(r);
}
}
});
it('allocates any extras to aggressiveness=0.5 and responsiveness=0', function() {
var pop = population.createEvenlyDistributed(19);
var ixSim = -1;
// The first 16 should be evenly distributed.
for (var a=0.125; a<=0.875; a+=0.25) {
for (var r=-0.75; r<=0.75; r+=0.5) {
++ixSim<16;
expect(pop.getSim(ixSim).getAggressiveness()).toBe(a);
expect(pop.getSim(ixSim).getResponsiveness()).toBe(r);
}
}
// The last three should be in the middle.
while (++ixSim < pop.getSimCount()) {
expect(pop.getSim(ixSim).getAggressiveness()).toBe(0.5);
expect(pop.getSim(ixSim).getResponsiveness()).toBe(0);
}
});
});
describe('randomlyEncounter', function() {
var test = function(populationSize) {
var population = Fws.population.createEvenlyDistributed(populationSize);
// Replace the real shuffle with one that turns ABCDE into BADCE
spyOn(Fws.utilities,'shuffle').andCallFake( function(ary) {
for (var ix=0; ix<ary.length-1; ix+=2) {
var temp = ary[ix];
ary[ix] = ary[ix+1];
ary[ix+1] = temp;
}
});
var expectedEncounters = [];
for (var ix=0; ix<population.getSimCount()-1; ix+=2) {
expectedEncounters.push([population.getSim(ix+1),population.getSim(ix)]);
}
var actualEncounters = [];
spyOn(Fws.sim,'encounter').andCallFake(function(simA, simB) {
expect(simA).not.toBe(simB);
actualEncounters.push([simA, simB]);
});
population.randomlyEncounter();
expect(actualEncounters).toEqual(expectedEncounters);
};
it('if there is an even number in the population, pairs all off and causes them to encounter each other', function() {
test(10);
});
it('if there is an odd number in the population, selects one at random not to pair and pairs & encounters the rest', function() {
test(9);
});
});
describe('reproduceAndDie', function() {
it('duplicates all members of the population that have reached the reproduction threshold', function(){
var populationSize = 100;
var population = Fws.population.createEvenlyDistributed(populationSize);
population.randomlyEncounter();
var popSortedByHealth = population.getCopyOfSims().sort(function(a,b) {
return a.getHealth() - b.getHealth();
});
var distinctHealthsFound = 0;
var minHealthToReproduce;
var lastHealthFound;
for (var ix=0; ix<popSortedByHealth.length; ++ix) {
if (popSortedByHealth[ix].getHealth() !== lastHealthFound) {
if (distinctHealthsFound==2) {
break;
}
++distinctHealthsFound;
lastHealthFound = popSortedByHealth[ix].getHealth();
}
}
expect(lastHealthFound).not.toBeUndefined();
var expectedReproductions = 0;
for (ix=0; ix<population.getSimCount(); ++ix) {
if (population.getSim(ix).getHealth() >= lastHealthFound)
++expectedReproductions;
}
spyOn(Fws.sim,'getReproductionThreshold').andReturn(lastHealthFound);
spyOn(Fws.sim,'getDeathThreshold').andReturn(-10000000);
var cloneCount = 0;
var cloneSpy = spyOn(population,'clone').andCallFake(function(sim) {
++cloneCount;
expect(sim.getHealth()).not.toBeLessThan(Fws.sim.getReproductionThreshold());
});
population.reproduceAndDie();
expect(cloneCount).toBe(expectedReproductions);
});
it('eliminates all members of the population that have reached the death threshold', function() {
var populationSize = 100;
var population = Fws.population.createEvenlyDistributed(populationSize);
population.randomlyEncounter();
var popSortedByHealth = population.getCopyOfSims().sort(function(a,b) {
return b.getHealth() - a.getHealth();
});
var distinctHealthsFound = 0;
var maxHealthToDie;
var lastHealthFound;
for (var ix=0; ix<popSortedByHealth.length; ++ix) {
if (popSortedByHealth[ix].getHealth() !== lastHealthFound) {
if (distinctHealthsFound==2) {
break;
}
++distinctHealthsFound;
lastHealthFound = popSortedByHealth[ix].getHealth();
}
}
expect(lastHealthFound).not.toBeUndefined();
var expectedDeaths = 0;
for (ix=0; ix<population.getSimCount(); ++ix) {
if (population.getSim(ix).getHealth() <= lastHealthFound)
++expectedDeaths;
}
spyOn(Fws.sim,'getDeathThreshold').andReturn(lastHealthFound);
spyOn(Fws.sim,'getReproductionThreshold').andReturn(10000000);
population.reproduceAndDie();
expect(population.getSimCount()).toBe(populationSize-expectedDeaths);
});
});
describe('computeHistogram(aggressivenessResolution, responsivenessResolution', function() {
it('correctly allocates the population into the returned 2-dimensional array', function() {
var pop = population.createEvenlyDistributed(19);
pop.add(sim.create(0.125, -0.75)); // In cell [0][0]
pop.add(sim.create(0, -1)); // In cell [0][0]
pop.add(sim.create(1,1)); // In cell [3][3]
pop.add(sim.create(0.6, -0.2)); // In cell [2][1]
var histogram = pop.computeHistogram(0.25, 0.5);
expect(histogram[0][0]).toBe(3);
expect(histogram[0][1]).toBe(1);
expect(histogram[0][2]).toBe(1);
expect(histogram[0][3]).toBe(1);
expect(histogram[1][0]).toBe(1);
expect(histogram[1][1]).toBe(1);
expect(histogram[1][2]).toBe(1);
expect(histogram[1][3]).toBe(1);
expect(histogram[2][0]).toBe(1);
expect(histogram[2][1]).toBe(2);
expect(histogram[2][2]).toBe(4); // includes the 3 extra from 19 - 16.
expect(histogram[2][3]).toBe(1);
expect(histogram[3][0]).toBe(1);
expect(histogram[3][1]).toBe(1);
expect(histogram[3][2]).toBe(1);
expect(histogram[3][3]).toBe(2);
});
});
});
describe('experiment', function() {
var logHistogram = function(population) {
var histogram = population.computeHistogram(0.1, 0.2);
for (var a=histogram.length-1; a>=0; --a) {
var line = 'agg '+(a*0.1).toFixed(1)+': ';
for (var r=0; r<histogram[a].length; ++r) {
line += String(' '+histogram[a][r]).slice(-5);
}
console.log(line);
}
};
it ('runs', function () {
var population = Fws.population.createEvenlyDistributed(10000);
var generations = 50;
var modToShow = 10;
for (var g=0; g<generations; ++g) {
if (g % modToShow === 0 || g===generations-1) {
console.log("----- Generation " + g + ": " + population.getSimCount() + " sims -----");
logHistogram(population);
}
population.randomlyEncounter();
population.reproduceAndDie();
}
});
});
(function () {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var trivialReporter = new jasmine.TrivialReporter();
jasmineEnv.addReporter(trivialReporter);
jasmineEnv.specFilter = function (spec) {
return trivialReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function () {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();
#TrivialReporter {
position: static !important;
height: 500px;
overflow-y: scroll;
}
#blanket-main {
overflow-y: scroll !important;
height: 400px;
display: block;
height: 100%;
width: 100%;
}
(function initialize() {
$('#reproduction').val(Fws.parameters.lifeEventThresholds.reproduction);
$('#death').val(Fws.parameters.lifeEventThresholds.death);
$('#healHeal').val(Fws.parameters.healthChanges.ifHealWhenOtherHeals);
$('#healStrike').val(Fws.parameters.healthChanges.ifHealWhenOtherStrikes);
$('#strikeHeal').val(Fws.parameters.healthChanges.ifStrikeWhenOtherHeals);
$('#strikeStrike').val(Fws.parameters.healthChanges.ifStrikeWhenOtherStrikes);
})();
function play() {
// Set world parameters from inputs
(function() {
Fws.parameters.lifeEventThresholds.reproduction = parseInt($('#reproduction').val(),10);
Fws.parameters.lifeEventThresholds.death = parseInt($('#death').val(),10);
Fws.parameters.healthChanges.ifHealWhenOtherHeals = parseInt($('#healHeal').val(),10);
Fws.parameters.healthChanges.ifHealWhenOtherStrikes = parseInt($('#healStrike').val(),10);
Fws.parameters.healthChanges.ifStrikeWhenOtherHeals = parseInt($('#strikeHeal').val(),10);
Fws.parameters.healthChanges.ifStrikeWhenOtherStrikes = parseInt($('#strikeStrike').val(),10);
})();
function enablePlayButton(wantEnable) {
$('#play').prop({ disabled: !wantEnable});
}
enablePlayButton(false);
var population = Fws.population.createEvenlyDistributed(10000);
var histogram = population.computeHistogram(0.1, 0.2);
var svg = d3.select('svg');
var spacing = 50;
var aggCount = histogram.length;
var rspCount = histogram[0].length;
var flatHistogram = [];
var generations = 20;
var copyToFlatHistogram = function(hist) {
var ixFlat = -1;
for (var ixAgg=aggCount-1; ixAgg>=0; --ixAgg) {
for (var ixRsp=0; ixRsp<rspCount; ++ixRsp) {
flatHistogram[++ixFlat] = histogram[ixAgg][ixRsp];
}
}
};
var computeRadius = function(numberOfSims) {
var popOfBigCircle = 8000;
var radiusOfBigCircle = spacing/2;
return Math.sqrt(numberOfSims/popOfBigCircle) * radiusOfBigCircle;
};
copyToFlatHistogram(histogram);
svg.selectAll('circle')
.data(flatHistogram)
.enter()
.append('circle')
.style('fill','navy')
.style('opacity', 0.5)
.attr('cy', function(d,i) { return Math.floor(i/rspCount)*spacing + spacing;})
.attr('cx', function(d,i) { return (i%rspCount)*spacing + spacing*2;})
.attr('r',computeRadius);
var doUpdate = function() {
population.randomlyEncounter();
population.reproduceAndDie();
histogram = population.computeHistogram(0.1, 0.2);
copyToFlatHistogram(histogram);
svg.selectAll('circle')
.data(flatHistogram)
.attr('r',computeRadius);
};
var gen=0;
d3.timer(function() {
doUpdate();
//console.log('gen ' + gen + ' population: ' + population.getSimCount());
var done = (++gen >= 500 || population.getSimCount()>250000);
if (done) {
console.log('Ending population: ' + population.getSimCount());
enablePlayButton(true);
}
return done;
});
};