<!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;
  });
};