diff --git a/dist/css/style.css b/dist/css/style.css index f78c279..f98f464 100644 --- a/dist/css/style.css +++ b/dist/css/style.css @@ -9,7 +9,7 @@ body{ * { margin: 0; padding: 0; - font-family: Arial, Helvetica, sans-serif; + font-family: Arial, Helvetica, sans-serif; } #env { @@ -39,12 +39,12 @@ body{ background-color: #3a4b68; display: grid; grid-template-columns: repeat(3, 1fr); - opacity: 0.8; + /* opacity: 0.8; */ } -.control-panel:hover { +/* .control-panel:hover { opacity: 1; -} +} */ .control-set { margin: 5px; @@ -226,6 +226,12 @@ button:hover{ margin: 5px; } +#chartContainer { + height: 200px; + width: 100%; +} + + .hot-controls { position: fixed; bottom: 10px; @@ -233,6 +239,16 @@ button:hover{ display: none; } +#headless-notification { + display: none; + position: fixed; + color: white; + font-size: 200px; + left: 0; + top: 0; + padding-left: 10px; +} + #minimize { margin: 10px; float: right; diff --git a/dist/index.html b/dist/index.html index e581c6f..56477dd 100644 --- a/dist/index.html +++ b/dist/index.html @@ -7,6 +7,7 @@ + @@ -28,6 +29,7 @@

Simulation Speed

+

Target FPS: 60

@@ -50,10 +52,6 @@
-
@@ -91,6 +89,10 @@ readme.

+
@@ -220,13 +222,24 @@
-

Stats

-

Organism count:

-

Highest count:

+

Statistics

+

Total Population:

+

Number of Species:

+

Largest Organism Ever:

Average Mutation Rate:

-

Largest Organism:

+ + +

+ +
+
+
-
@@ -254,7 +267,10 @@ - + +
+
+
diff --git a/src/Controllers/ControlPanel.js b/src/Controllers/ControlPanel.js index e283874..ab5eaa5 100644 --- a/src/Controllers/ControlPanel.js +++ b/src/Controllers/ControlPanel.js @@ -1,5 +1,6 @@ const Hyperparams = require("../Hyperparameters"); const Modes = require("./ControlModes"); +const StatsPanel = require("../Stats/StatsPanel"); class ControlPanel { constructor(engine) { @@ -17,6 +18,10 @@ class ControlPanel { this.editor_controller = this.engine.organism_editor.controller; this.env_controller.setControlPanel(this); this.editor_controller.setControlPanel(this); + this.stats_panel = new StatsPanel(this.engine.env); + this.headless_opacity = 1; + this.opacity_change_rate = -0.8; + this.paused=false; } defineMinMaxControls(){ @@ -26,11 +31,15 @@ class ControlPanel { $('.control-panel').css('display', 'none'); $('.hot-controls').css('display', 'block'); this.control_panel_active = false; + this.stats_panel.stopAutoRender(); }); $('#maximize').click ( () => { $('.control-panel').css('display', 'grid'); $('.hot-controls').css('display', 'none'); this.control_panel_active = true; + if (this.tab_id == 'stats') { + this.stats_panel.startAutoRender(); + } }); const V_KEY = 118; $('body').keypress( (e) => { @@ -38,6 +47,9 @@ class ControlPanel { if (this.no_hud) { let control_panel_display = this.control_panel_active ? 'grid' : 'none'; let hot_control_display = !this.control_panel_active ? 'block' : 'none'; + if (this.control_panel_active && this.tab_id == 'stats') { + this.stats_panel.startAutoRender(); + }; $('.control-panel').css('display', control_panel_display); $('.hot-controls').css('display', hot_control_display); } @@ -48,6 +60,19 @@ class ControlPanel { this.no_hud = !this.no_hud; } }); + // var self = this; + // $('#minimize').click ( function() { + // $('.control-panel').css('display', 'none'); + // $('.hot-controls').css('display', 'block'); + + // }.bind(this)); + // $('#maximize').click ( function() { + // $('.control-panel').css('display', 'grid'); + // $('.hot-controls').css('display', 'none'); + // if (self.tab_id == 'stats') { + // self.stats_panel.startAutoRender(); + // } + // }); } defineEngineSpeedControls(){ @@ -56,13 +81,13 @@ class ControlPanel { this.fps = this.slider.value if (this.engine.running) { this.changeEngineSpeed(this.fps); - } $('#fps').text("Target FPS: "+this.fps); }.bind(this); $('.pause-button').click(function() { $('.pause-button').find("i").toggleClass("fa fa-pause"); $('.pause-button').find("i").toggleClass("fa fa-play"); + this.paused = !this.paused; if (this.engine.running) { this.engine.stop(); } @@ -70,6 +95,18 @@ class ControlPanel { this.engine.start(this.fps); } }.bind(this)); + $('.headless').click(function() { + $('.headless').find("i").toggleClass("fa fa-eye"); + $('.headless').find("i").toggleClass("fa fa-eye-slash"); + if (Hyperparams.headless){ + $('#headless-notification').css('display', 'none'); + this.engine.env.renderFull(); + } + else { + $('#headless-notification').css('display', 'block'); + } + Hyperparams.headless = !Hyperparams.headless; + }.bind(this)); } defineGridSizeControls() { @@ -91,24 +128,31 @@ class ControlPanel { var rows = $('#row-input').val(); this.engine.env.resizeGridColRow(cell_size, cols, rows); } + this.engine.env.reset(); + this.stats_panel.reset(); }.bind(this)); } defineTabNavigation() { + this.tab_id = 'about'; var self = this; $('.tabnav-item').click(function() { $('.tab').css('display', 'none'); var tab = '#'+this.id+'.tab'; - self.engine.organism_editor.is_active = (this.id == 'editor'); $(tab).css('display', 'grid'); + self.engine.organism_editor.is_active = (this.id == 'editor'); + self.stats_panel.stopAutoRender(); + if (this.id == 'stats') { + self.stats_panel.startAutoRender(); + } + self.tab_id = this.id; }); } defineHyperparameterControls() { $('#food-prod-prob').change(function() { - var food_prob = - Hyperparams.foodProdProb = $('#food-prod-prob').val();; + Hyperparams.foodProdProb = $('#food-prod-prob').val(); }.bind(this)); $('#lifespan-multiplier').change(function() { Hyperparams.lifespanMultiplier = $('#lifespan-multiplier').val(); @@ -142,7 +186,7 @@ class ControlPanel { Hyperparams.useGlobalMutability = !this.checked; }); $('#global-mutation').change( function() { - Hyperparams.globalMutability = $('#global-mutation').val(); + Hyperparams.globalMutability = parseInt($('#global-mutation').val()); }); $('.mut-prob').change( function() { switch(this.id){ @@ -182,6 +226,9 @@ class ControlPanel { $('#remove-prob').val(Hyperparams.removeProb); $('#movers-produce').prop('checked', Hyperparams.moversCanProduce); $('#food-blocks').prop('checked', Hyperparams.foodBlocksReproduction); + $('#food-drop-rate').val(Hyperparams.foodDropProb); + $('#look-range').val(Hyperparams.lookRange); + if (!Hyperparams.useGlobalMutability) { $('.global-mutation-in').css('display', 'none'); $('#avg-mut').css('display', 'block'); @@ -199,7 +246,7 @@ class ControlPanel { $('#cell-selections').css('display', 'none'); $('#organism-options').css('display', 'none'); self.editor_controller.setDetailsPanel(); - switch(this.id){ + switch(this.id) { case "food-drop": self.setMode(Modes.FoodDrop); break; @@ -219,6 +266,9 @@ class ControlPanel { case "drop-org": self.setMode(Modes.Clone); self.env_controller.org_to_clone = self.engine.organism_editor.getCopyOfOrg(); + self.env_controller.add_new_species = self.editor_controller.new_species; + self.editor_controller.new_species = false; + // console.log(self.env_controller.add_new_species) break; case "drag-view": self.setMode(Modes.Drag); @@ -234,6 +284,7 @@ class ControlPanel { var env = this.engine.env; $('#reset-env').click( function() { this.engine.env.reset(); + this.stats_panel.reset(); }.bind(this)); $('#auto-reset').change(function() { env.auto_reset = this.checked; @@ -273,16 +324,29 @@ class ControlPanel { this.fps = this.engine.fps; } - update() { + updateHeadlessIcon(delta_time) { + if (this.paused) + return; + var op = this.headless_opacity + (this.opacity_change_rate*delta_time/1000); + if (op <= 0.4){ + op=0.4; + this.opacity_change_rate = -this.opacity_change_rate; + } + else if (op >= 1){ + op=1; + this.opacity_change_rate = -this.opacity_change_rate; + } + this.headless_opacity = op; + $('#headless-notification').css('opacity',(op*100)+'%'); + } + + update(delta_time) { $('#fps-actual').text("Actual FPS: " + Math.floor(this.engine.actual_fps)); - var org_count = this.engine.env.organisms.length; - $('#org-count').text("Organism count: " + org_count); - if (org_count > this.organism_record) - this.organism_record = org_count; - $('#org-record').text("Highest count: " + this.organism_record); - $('#avg-mut').text("Average Mutation Rate: " + Math.round(this.engine.env.averageMutability() * 100) / 100); - $('#largest-org').text("Largest Organism: " + this.engine.env.largest_cell_count + " cells"); $('#reset-count').text("Auto reset count: " + this.engine.env.reset_count); + this.stats_panel.updateDetails(); + if (Hyperparams.headless) + this.updateHeadlessIcon(delta_time); + } } diff --git a/src/Controllers/EditorController.js b/src/Controllers/EditorController.js index 5b55c6d..e7c2152 100644 --- a/src/Controllers/EditorController.js +++ b/src/Controllers/EditorController.js @@ -10,6 +10,7 @@ class EditorController extends CanvasController{ this.mode = Modes.None; this.edit_cell_type = null; this.highlight_org = false; + this.new_species = false; this.defineCellTypeSelection(); this.defineEditorDetails(); } @@ -26,7 +27,7 @@ class EditorController extends CanvasController{ mouseUp(){} getCurLocalCell(){ - return this.env.organism.getLocalCell(this.mouse_c-this.env.organism.c, this.mouse_r-this.env.organism.r); + return this.env.organism.anatomy.getLocalCell(this.mouse_c-this.env.organism.c, this.mouse_r-this.env.organism.r); } editOrganism() { @@ -41,15 +42,17 @@ class EditorController extends CanvasController{ else this.env.addCellToOrg(this.mouse_c, this.mouse_r, this.edit_cell_type); } - if (this.right_click) + else if (this.right_click) this.env.removeCellFromOrg(this.mouse_c, this.mouse_r); + + this.new_species = true; this.setBrainPanelVisibility(); this.setMoveRangeVisibility(); this.updateDetails(); } updateDetails() { - $('.cell-count').text("Cell count: "+this.env.organism.cells.length); + $('.cell-count').text("Cell count: "+this.env.organism.anatomy.cells.length); } defineCellTypeSelection() { @@ -111,7 +114,7 @@ class EditorController extends CanvasController{ this.clearDetailsPanel(); var org = this.env.organism; - $('.cell-count').text("Cell count: "+org.cells.length); + $('.cell-count').text("Cell count: "+org.anatomy.cells.length); $('#move-range').text("Move Range: "+org.move_range); $('#mutation-rate').text("Mutation Rate: "+org.mutability); if (Hyperparams.useGlobalMutability) { @@ -134,7 +137,7 @@ class EditorController extends CanvasController{ this.clearDetailsPanel(); var org = this.env.organism; - $('.cell-count').text("Cell count: "+org.cells.length); + $('.cell-count').text("Cell count: "+org.anatomy.cells.length); if (this.setMoveRangeVisibility()){ $('#move-range-edit').val(org.move_range); } @@ -149,7 +152,7 @@ class EditorController extends CanvasController{ setBrainPanelVisibility() { var org = this.env.organism; - if (org.has_eyes && org.is_mover) { + if (org.anatomy.has_eyes && org.anatomy.is_mover) { $('.brain-details').css('display', 'block'); return true; } @@ -175,7 +178,7 @@ class EditorController extends CanvasController{ setMoveRangeVisibility() { var org = this.env.organism; - if (org.is_mover) { + if (org.anatomy.is_mover) { $('#move-range-cont').css('display', 'block'); $('#move-range').css('display', 'block'); return true; diff --git a/src/Controllers/EnvironmentController.js b/src/Controllers/EnvironmentController.js index ec4dbae..451d2a9 100644 --- a/src/Controllers/EnvironmentController.js +++ b/src/Controllers/EnvironmentController.js @@ -3,12 +3,15 @@ const Organism = require('../Organism/Organism'); const Modes = require("./ControlModes"); const CellStates = require("../Organism/Cell/CellStates"); const Neighbors = require("../Grid/Neighbors"); +const FossilRecord = require("../Stats/FossilRecord"); +const Hyperparams = require("../Hyperparameters"); class EnvironmentController extends CanvasController{ constructor(env, canvas) { super(env, canvas); this.mode = Modes.Drag; this.org_to_clone = null; + this.add_new_species = false; this.defineZoomControls(); this.scale = 1; } @@ -76,6 +79,8 @@ class EnvironmentController extends CanvasController{ } performModeAction() { + if (Hyperparams.headless) + return; var mode = this.mode; var right_click = this.right_click; var left_click = this.left_click; @@ -118,8 +123,19 @@ class EnvironmentController extends CanvasController{ case Modes.Clone: if (this.org_to_clone != null){ var new_org = new Organism(this.mouse_c, this.mouse_r, this.env, this.org_to_clone); + if (this.add_new_species){ + FossilRecord.addSpeciesObj(new_org.species); + new_org.species.start_tick = this.env.total_ticks; + this.add_new_species = false; + new_org.species.population = 0; + } + else if (this.org_to_clone.species.extinct){ + FossilRecord.resurrect(this.org_to_clone.species); + } + if (new_org.isClear(this.mouse_c, this.mouse_r)){ - this.env.addOrganism(new_org) + this.env.addOrganism(new_org); + new_org.species.addPop(); } } break; diff --git a/src/Engine.js b/src/Engine.js index 7143629..a74f693 100644 --- a/src/Engine.js +++ b/src/Engine.js @@ -5,7 +5,7 @@ const ColorScheme = require('./Rendering/ColorScheme'); const render_speed = 60; -class Engine{ +class Engine { constructor(){ this.fps = 60; this.env = new WorldEnvironment(5); @@ -23,10 +23,8 @@ class Engine{ start(fps=60) { if (fps <= 0) fps = 1; - if (fps > 300) - fps = 300; this.fps = fps; - this.game_loop = setInterval(function(){this.environmentUpdate();}.bind(this), 1000/fps); + this.game_loop = setInterval(function(){this.updateDeltaTime();this.environmentUpdate();}.bind(this), 1000/fps); this.running = true; if (this.fps >= render_speed) { if (this.render_loop != null) { @@ -46,14 +44,17 @@ class Engine{ setRenderLoop() { if (this.render_loop == null) { - this.render_loop = setInterval(function(){this.necessaryUpdate();}.bind(this), 1000/render_speed); + this.render_loop = setInterval(function(){this.updateDeltaTime();this.necessaryUpdate();}.bind(this), 1000/render_speed); } } + updateDeltaTime() { + this.delta_time = Date.now() - this.last_update; + this.last_update = Date.now(); + } + environmentUpdate() { - this.delta_time = Date.now() - this.last_update; - this.last_update = Date.now(); this.env.update(this.delta_time); this.actual_fps = 1/this.delta_time*1000; if(this.render_loop == null){ @@ -64,7 +65,7 @@ class Engine{ necessaryUpdate() { this.env.render(); - this.controlpanel.update(); + this.controlpanel.update(this.delta_time); this.organism_editor.update(); } diff --git a/src/Environments/OrganismEditor.js b/src/Environments/OrganismEditor.js index 9294549..b969774 100644 --- a/src/Environments/OrganismEditor.js +++ b/src/Environments/OrganismEditor.js @@ -4,7 +4,7 @@ const GridMap = require('../Grid/GridMap'); const Renderer = require('../Rendering/Renderer'); const CellStates = require('../Organism/Cell/CellStates'); const EditorController = require("../Controllers/EditorController"); -const Directions = require('../Organism/Directions'); +const Species = require('../Stats/Species'); class OrganismEditor extends Environment{ constructor() { @@ -37,14 +37,15 @@ class OrganismEditor extends Environment{ var center = this.grid_map.getCenter(); var loc_c = c - center[0]; var loc_r = r - center[1]; - var prev_cell = this.organism.getLocalCell(loc_c, loc_r) + var prev_cell = this.organism.anatomy.getLocalCell(loc_c, loc_r) if (prev_cell != null) { - var new_cell = this.organism.replaceCell(state, prev_cell.loc_col, prev_cell.loc_row, false); + var new_cell = this.organism.anatomy.replaceCell(state, prev_cell.loc_col, prev_cell.loc_row, false); this.changeCell(c, r, state, new_cell); } - else if (this.organism.canAddCellAt(loc_c, loc_r)){ - this.changeCell(c, r, state, this.organism.addDefaultCell(state, loc_c, loc_r)); + else if (this.organism.anatomy.canAddCellAt(loc_c, loc_r)){ + this.changeCell(c, r, state, this.organism.anatomy.addDefaultCell(state, loc_c, loc_r)); } + this.organism.species = new Species(this.organism.anatomy, null, 0); } removeCellFromOrg(c, r) { @@ -55,20 +56,22 @@ class OrganismEditor extends Environment{ alert("Cannot remove center cell"); return; } - var prev_cell = this.organism.getLocalCell(loc_c, loc_r) + var prev_cell = this.organism.anatomy.getLocalCell(loc_c, loc_r) if (prev_cell != null) { - if (this.organism.removeCell(loc_c, loc_r)) { + if (this.organism.anatomy.removeCell(loc_c, loc_r)) { this.changeCell(c, r, CellStates.empty, null); + this.organism.species = new Species(this.organism.anatomy, null, 0); } } } - setOrganismToCopyOf(orig_org){ + setOrganismToCopyOf(orig_org) { this.grid_map.fillGrid(CellStates.empty); var center = this.grid_map.getCenter(); this.organism = new Organism(center[0], center[1], this, orig_org); this.organism.updateGrid(); this.controller.updateDetails(); + this.controller.new_species = false; } getCopyOfOrg() { @@ -80,8 +83,9 @@ class OrganismEditor extends Environment{ this.grid_map.fillGrid(CellStates.empty); var center = this.grid_map.getCenter(); this.organism = new Organism(center[0], center[1], this, null); - this.organism.addDefaultCell(CellStates.mouth, 0, 0); + this.organism.anatomy.addDefaultCell(CellStates.mouth, 0, 0); this.organism.updateGrid(); + this.organism.species = new Species(this.organism.anatomy, null, 0); } } diff --git a/src/Environments/WorldEnvironment.js b/src/Environments/WorldEnvironment.js index 26a73c1..da7bb40 100644 --- a/src/Environments/WorldEnvironment.js +++ b/src/Environments/WorldEnvironment.js @@ -5,7 +5,7 @@ const Organism = require('../Organism/Organism'); const CellStates = require('../Organism/Cell/CellStates'); const EnvironmentController = require('../Controllers/EnvironmentController'); const Hyperparams = require('../Hyperparameters.js'); -const Cell = require('../Organism/Cell/GridCell'); +const FossilRecord = require('../Stats/FossilRecord'); class WorldEnvironment extends Environment{ constructor(cell_size) { @@ -21,6 +21,9 @@ class WorldEnvironment extends Environment{ this.auto_reset = true; this.largest_cell_count = 0; this.reset_count = 0; + this.total_ticks = 0; + this.data_update_rate = 100; + FossilRecord.setEnv(this); } update() { @@ -35,13 +38,25 @@ class WorldEnvironment extends Environment{ this.generateFood(); } this.removeOrganisms(to_remove); + this.total_ticks ++; + if (this.total_ticks % this.data_update_rate == 0) { + FossilRecord.updateData(); + } } render() { + if (Hyperparams.headless) { + this.renderer.cells_to_render.clear(); + return; + } this.renderer.renderCells(); this.renderer.renderHighlights(); } + renderFull() { + this.renderer.renderFullGrid(this.grid_map.grid); + } + removeOrganisms(org_indeces) { for (var i of org_indeces.reverse()){ this.total_mutability -= this.organisms[i].mutability; @@ -56,26 +71,27 @@ class WorldEnvironment extends Environment{ OriginOfLife() { var center = this.grid_map.getCenter(); var org = new Organism(center[0], center[1], this); - // org.addDefaultCell(CellStates.eye, 0, 0); - // org.addDefaultCell(CellStates.mouth, 1, 1); - // org.addDefaultCell(CellStates.mover, 1, -1); - org.addDefaultCell(CellStates.mouth, 0, 0); - org.addDefaultCell(CellStates.producer, 1, 1); - org.addDefaultCell(CellStates.producer, -1, -1); + org.anatomy.addDefaultCell(CellStates.mouth, 0, 0); + org.anatomy.addDefaultCell(CellStates.producer, 1, 1); + org.anatomy.addDefaultCell(CellStates.producer, -1, -1); this.addOrganism(org); + FossilRecord.addSpecies(org, null); } addOrganism(organism) { organism.updateGrid(); this.total_mutability += organism.mutability; this.organisms.push(organism); - if (organism.cells.length > this.largest_cell_count) - this.largest_cell_count = organism.cells.length; + if (organism.anatomy.cells.length > this.largest_cell_count) + this.largest_cell_count = organism.anatomy.cells.length; } averageMutability() { if (this.organisms.length < 1) return 0; + if (Hyperparams.useGlobalMutability) { + return Hyperparams.globalMutability; + } return this.total_mutability / this.organisms.length; } @@ -114,11 +130,13 @@ class WorldEnvironment extends Environment{ } } - reset(clear_walls=true) { + reset() { this.organisms = []; this.grid_map.fillGrid(CellStates.empty); this.renderer.renderFullGrid(this.grid_map.grid); this.total_mutability = 0; + this.total_ticks = 0; + FossilRecord.clear_record(); this.OriginOfLife(); } @@ -126,7 +144,6 @@ class WorldEnvironment extends Environment{ this.renderer.cell_size = cell_size; this.renderer.fillShape(rows*cell_size, cols*cell_size); this.grid_map.resize(cols, rows, cell_size); - this.reset(); } resizeFillWindow(cell_size) { @@ -135,7 +152,6 @@ class WorldEnvironment extends Environment{ var cols = Math.ceil(this.renderer.width / cell_size); var rows = Math.ceil(this.renderer.height / cell_size); this.grid_map.resize(cols, rows, cell_size); - this.reset(); } } diff --git a/src/Hyperparameters.js b/src/Hyperparameters.js index c5c7be1..d7c34ed 100644 --- a/src/Hyperparameters.js +++ b/src/Hyperparameters.js @@ -2,6 +2,8 @@ const Neighbors = require("./Grid/Neighbors"); const Hyperparams = { setDefaults: function() { + this.headless = false; + this.lifespanMultiplier = 100; this.foodProdProb = 4; this.foodProdProbScalar = 4; diff --git a/src/Organism/Anatomy.js b/src/Organism/Anatomy.js new file mode 100644 index 0000000..8175024 --- /dev/null +++ b/src/Organism/Anatomy.js @@ -0,0 +1,96 @@ +const CellStates = require("./Cell/CellStates"); +const BodyCellFactory = require("./Cell/BodyCells/BodyCellFactory"); + +class Anatomy { + constructor(owner) { + this.owner = owner; + this.cells = []; + this.is_producer = false; + this.is_mover = false; + this.has_eyes = false; + this.birth_distance = 4; + } + + canAddCellAt(c, r) { + for (var cell of this.cells) { + if (cell.loc_col == c && cell.loc_row == r){ + return false; + } + } + return true; + } + + addDefaultCell(state, c, r) { + var new_cell = BodyCellFactory.createDefault(this.owner, state, c, r); + this.cells.push(new_cell); + return new_cell; + } + + addRandomizedCell(state, c, r) { + if (state==CellStates.eye && !this.has_eyes) { + this.owner.brain.randomizeDecisions(); + } + var new_cell = BodyCellFactory.createRandom(this.owner, state, c, r); + this.cells.push(new_cell); + return new_cell; + } + + addInheritCell(parent_cell) { + var new_cell = BodyCellFactory.createInherited(this.owner, parent_cell); + this.cells.push(new_cell); + return new_cell; + } + + replaceCell(state, c, r, randomize=true) { + this.removeCell(c, r, true); + if (randomize) { + return this.addRandomizedCell(state, c, r); + } + else { + return this.addDefaultCell(state, c, r); + } + } + + removeCell(c, r, allow_center_removal=false) { + if (c == 0 && r == 0 && !allow_center_removal) + return false; + for (var i=0; i 1) { - var cell = this.cells[Math.floor(Math.random() * this.cells.length)]; - mutated = this.removeCell(cell.loc_col, cell.loc_row); + if(this.anatomy.cells.length > 1) { + var cell = this.anatomy.getRandomCell(); + mutated = this.anatomy.removeCell(cell.loc_col, cell.loc_row); } } return mutated; @@ -237,7 +166,7 @@ class Organism { var new_c = this.c + direction_c; var new_r = this.r + direction_r; if (this.isClear(new_c, new_r)) { - for (var cell of this.cells) { + for (var cell of this.anatomy.cells) { var real_c = this.c + cell.rotatedCol(this.rotation); var real_r = this.r + cell.rotatedRow(this.rotation); this.env.changeCell(real_c, real_r, CellStates.empty, null); @@ -258,7 +187,7 @@ class Organism { } var new_rotation = Directions.getRandomDirection(); if(this.isClear(this.c, this.r, new_rotation)){ - for (var cell of this.cells) { + for (var cell of this.anatomy.cells) { var real_c = this.c + cell.rotatedCol(this.rotation); var real_r = this.r + cell.rotatedRow(this.rotation); this.env.changeCell(real_c, real_r, CellStates.empty, null); @@ -277,7 +206,7 @@ class Organism { this.move_count = 0; } - // assumes either c1==c2 or r1==r2, returns true if there is a clear path from point a to b + // assumes either c1==c2 or r1==r2, returns true if there is a clear path from point 1 to 2 isStraightPath(c1, r1, c2, r2, parent){ if (c1 == c2) { if (r1 > r2){ @@ -314,11 +243,12 @@ class Organism { } isClear(col, row, rotation=this.rotation, ignore_armor=false) { - for(var loccell of this.cells) { + for(var loccell of this.anatomy.cells) { var cell = this.getRealCell(loccell, col, row, rotation); - if(cell==null) { + if (cell==null) { return false; } + // console.log(cell.owner == this) if (cell.owner==this || cell.state==CellStates.empty || (!Hyperparams.foodBlocksReproduction && cell.state==CellStates.food) || (ignore_armor && loccell.state==CellStates.armor && cell.state==CellStates.food)){ continue; } @@ -335,16 +265,17 @@ class Organism { } die() { - for (var cell of this.cells) { + for (var cell of this.anatomy.cells) { var real_c = this.c + cell.rotatedCol(this.rotation); var real_r = this.r + cell.rotatedRow(this.rotation); this.env.changeCell(real_c, real_r, CellStates.food, null); } + this.species.decreasePop(); this.living = false; } updateGrid() { - for (var cell of this.cells) { + for (var cell of this.anatomy.cells) { var real_c = this.c + cell.rotatedCol(this.rotation); var real_r = this.r + cell.rotatedRow(this.rotation); this.env.changeCell(real_c, real_r, cell.state, cell); @@ -360,13 +291,13 @@ class Organism { if (this.food_collected >= this.foodNeeded()) { this.reproduce(); } - for (var cell of this.cells) { + for (var cell of this.anatomy.cells) { cell.performFunction(); if (!this.living) return this.living } - if (this.is_mover) { + if (this.anatomy.is_mover) { this.move_count++; var changed_dir = false; if (this.ignore_brain_for == 0){ diff --git a/src/Rendering/Renderer.js b/src/Rendering/Renderer.js index e60e457..647a9a8 100644 --- a/src/Rendering/Renderer.js +++ b/src/Rendering/Renderer.js @@ -52,7 +52,7 @@ class Renderer { } renderOrganism(org) { - for(var org_cell of org.cells) { + for(var org_cell of org.anatomy.cells) { var cell = org.getRealCell(org_cell); this.renderCell(cell); } @@ -75,7 +75,7 @@ class Renderer { } highlightOrganism(org) { - for(var org_cell of org.cells) { + for(var org_cell of org.anatomy.cells) { var cell = org.getRealCell(org_cell); this.cells_to_highlight.add(cell); } diff --git a/src/Stats/Charts/CellsChart.js b/src/Stats/Charts/CellsChart.js new file mode 100644 index 0000000..9a10c5b --- /dev/null +++ b/src/Stats/Charts/CellsChart.js @@ -0,0 +1,55 @@ +const CellStates = require("../../Organism/Cell/CellStates"); +const FossilRecord = require("../FossilRecord"); +const ChartController = require("./ChartController"); + +class CellsChart extends ChartController { + constructor() { + super("Organism Size / Composition", + "Avg. Number of Cells per Organism", + "Note: to maintain efficiency, species with very small populations are discarded when collecting cell statistics."); + } + + setData() { + this.clear(); + //this.mouth, this.producer, this.mover, this.killer, this.armor, this.eye + this.data.push({ + type: "line", + markerType: "none", + color: 'black', + showInLegend: true, + name: "pop1", + legendText: "Avg. organism size", + dataPoints: [] + } + ); + for (var c of CellStates.living) { + this.data.push({ + type: "line", + markerType: "none", + color: c.color, + showInLegend: true, + name: c.name, + legendText: "Avg. " + c.name + " cells", + dataPoints: [] + } + ); + } + this.addAllDataPoints(); + + + } + + addDataPoint(i) { + var t = FossilRecord.tick_record[i]; + var p = FossilRecord.av_cells[i]; + this.data[0].dataPoints.push({x:t, y:p}); + var j=1; + for (var name in FossilRecord.av_cell_counts[i]) { + var count = FossilRecord.av_cell_counts[i][name]; + this.data[j].dataPoints.push({x:t,y:count}) + j++; + } + } +} + +module.exports = CellsChart; \ No newline at end of file diff --git a/src/Stats/Charts/ChartController.js b/src/Stats/Charts/ChartController.js new file mode 100644 index 0000000..999fdf4 --- /dev/null +++ b/src/Stats/Charts/ChartController.js @@ -0,0 +1,84 @@ +const FossilRecord = require("../FossilRecord"); + +class ChartController { + constructor(title, y_axis="", note="") { + this.data = []; + this.chart = new CanvasJS.Chart("chartContainer", { + zoomEnabled: true, + title:{ + text: title + }, + axisX:{ + title: "Ticks", + minimum: 0, + }, + axisY:{ + title: y_axis, + minimum: 0, + }, + data: this.data + }); + this.chart.render(); + $('#chart-note').text(note); + } + + setData() { + alert("Must override updateData!"); + } + + setMinimum() { + var min = 0; + if (this.data[0].dataPoints != []) + min = this.data[0].dataPoints[0].x; + this.chart.options.axisX.minimum = min; + } + + addAllDataPoints(){ + for (var i in FossilRecord.tick_record) { + this.addDataPoint(i) + } + } + + render() { + this.setMinimum(); + this.chart.render(); + } + + updateData() { + var r_len = FossilRecord.tick_record.length; + var newest_t = -1; + var oldest_t = 0; + if (this.data[0].dataPoints.length>0) { + newest_t = this.data[0].dataPoints[this.data[0].dataPoints.length-1].x; + newest_t = this.data[0].dataPoints[0].x; + } + if (newest_t < FossilRecord.tick_record[r_len-1]) { + this.addNewest(); + } + if (oldest_t < FossilRecord.tick_record[0]) { + this.removeOldest(); + } + } + + addNewest() { + var i = FossilRecord.tick_record.length-1; + this.addDataPoint(i); + } + + addDataPoint(i) { + alert("Must override addDataPoint") + } + + removeOldest() { + for (var dps of this.data) { + dps.dataPoints.shift(); + } + } + + clear() { + this.data.length = 0; + this.chart.render(); + } +} + +module.exports = ChartController; \ No newline at end of file diff --git a/src/Stats/Charts/MutationChart.js b/src/Stats/Charts/MutationChart.js new file mode 100644 index 0000000..f211bdb --- /dev/null +++ b/src/Stats/Charts/MutationChart.js @@ -0,0 +1,31 @@ +const FossilRecord = require("../FossilRecord"); +const ChartController = require("./ChartController"); + +class MutationChart extends ChartController { + constructor() { + super("Mutation Rate"); + } + + setData() { + this.clear(); + this.data.push({ + type: "line", + markerType: "none", + color: 'black', + showInLegend: true, + name: "pop1", + legendText: "Average Mutation Rate", + dataPoints: [] + } + ); + this.addAllDataPoints(); + } + + addDataPoint(i) { + var t = FossilRecord.tick_record[i]; + var p = FossilRecord.av_mut_rates[i]; + this.data[0].dataPoints.push({x:t, y:p}); + } +} + +module.exports = MutationChart; \ No newline at end of file diff --git a/src/Stats/Charts/PopulationChart.js b/src/Stats/Charts/PopulationChart.js new file mode 100644 index 0000000..99a02af --- /dev/null +++ b/src/Stats/Charts/PopulationChart.js @@ -0,0 +1,31 @@ +const FossilRecord = require("../FossilRecord"); +const ChartController = require("./ChartController"); + +class PopulationChart extends ChartController { + constructor() { + super("Population"); + } + + setData() { + this.clear(); + this.data.push({ + type: "line", + markerType: "none", + color: 'black', + showInLegend: true, + name: "pop1", + legendText: "Total Population", + dataPoints: [] + } + ); + this.addAllDataPoints(); + } + + addDataPoint(i) { + var t = FossilRecord.tick_record[i]; + var p = FossilRecord.pop_counts[i]; + this.data[0].dataPoints.push({x:t, y:p}); + } +} + +module.exports = PopulationChart; \ No newline at end of file diff --git a/src/Stats/Charts/SpeciesChart.js b/src/Stats/Charts/SpeciesChart.js new file mode 100644 index 0000000..50c6b84 --- /dev/null +++ b/src/Stats/Charts/SpeciesChart.js @@ -0,0 +1,31 @@ +const FossilRecord = require("../FossilRecord"); +const ChartController = require("./ChartController"); + +class SpeciesChart extends ChartController { + constructor() { + super("Species"); + } + + setData() { + this.clear(); + this.data.push({ + type: "line", + markerType: "none", + color: 'black', + showInLegend: true, + name: "spec", + legendText: "Number of Species", + dataPoints: [] + } + ); + this.addAllDataPoints(); + } + + addDataPoint(i) { + var t = FossilRecord.tick_record[i]; + var p = FossilRecord.species_counts[i]; + this.data[0].dataPoints.push({x:t, y:p}); + } +} + +module.exports = SpeciesChart; \ No newline at end of file diff --git a/src/Stats/FossilRecord.js b/src/Stats/FossilRecord.js new file mode 100644 index 0000000..16e58dd --- /dev/null +++ b/src/Stats/FossilRecord.js @@ -0,0 +1,133 @@ +const CellStates = require("../Organism/Cell/CellStates"); +const Species = require("./Species"); + +const FossilRecord = { + init: function(){ + this.extant_species = []; + this.extinct_species = []; + + // if an organism has fewer than this cumulative pop, discard them on extinction + this.min_discard = 10; + + this.record_size_limit = 500; // store this many data points + }, + + setEnv: function(env) { + this.env = env; + this.setData(); + }, + + addSpecies: function(org, ancestor) { + // console.log("Adding Species") + var new_species = new Species(org.anatomy, ancestor, this.env.total_ticks); + this.extant_species.push(new_species); + org.species = new_species; + return new_species; + }, + + addSpeciesObj: function(species) { + // console.log("Adding Species") + this.extant_species.push(species); + return species; + }, + + fossilize: function(species) { + // console.log("Extinction") + species.end_tick = this.env.total_ticks; + for (i in this.extant_species) { + var s = this.extant_species[i]; + if (s == species) { + this.extant_species.splice(i, 1); + if (species.cumulative_pop < this.min_pop) { + return false; + } + this.extinct_species.push(s); + // console.log("Extant species:", this.extant_species.length) + // console.log("Extinct species:", this.extinct_species.length) + return true; + } + } + }, + + resurrect: function(species) { + // console.log("Resurrecting species") + if (species.extinct) { + for (i in this.extinct_species) { + var s = this.extinct_species[i]; + if (s == species) { + this.extinct_species.splice(i, 1); + this.extant_species.push(species); + species.extinct = false; + } + } + } + }, + + setData() { + // all parallel arrays + this.tick_record = [0]; + this.pop_counts = [0]; + this.species_counts = [0]; + this.av_mut_rates = [0]; + this.av_cells = [0]; + this.av_cell_counts = [this.calcCellCountAverages()]; + }, + + updateData() { + var tick = this.env.total_ticks; + this.tick_record.push(tick); + this.pop_counts.push(this.env.organisms.length); + this.species_counts.push(this.extant_species.length); + this.av_mut_rates.push(this.env.averageMutability()); + this.calcCellCountAverages(); + + if (this.tick_record.length > this.record_size_limit) { + this.tick_record.shift(); + this.pop_counts.shift(); + this.species_counts.shift(); + this.av_mut_rates.shift(); + this.av_cells.shift(); + this.av_cell_counts.shift(); + } + }, + + calcCellCountAverages() { + var total_org = 0; + var cell_counts = {}; + for (let c of CellStates.living) { + cell_counts[c.name] = 0; + } + var first=true; + for (let s of this.extant_species) { + if (s.cumulative_pop < this.min_discard && !first){ + continue; + } + for (let name in s.cell_counts) { + cell_counts[name] += s.cell_counts[name] * s.population; + } + total_org += s.population; + first=false; + } + if (total_org == 0) + return cell_counts; + + var total_cells = 0; + for (let c in cell_counts) { + total_cells += cell_counts[c]; + cell_counts[c] /= total_org; + } + this.av_cells.push(total_cells / total_org); + this.av_cell_counts.push(cell_counts); + }, + + clear_record: function() { + this.extant_species = []; + this.extinct_species = []; + this.setData(); + }, + +} + +FossilRecord.init(); + +module.exports = FossilRecord; \ No newline at end of file diff --git a/src/Stats/Species.js b/src/Stats/Species.js new file mode 100644 index 0000000..37daadd --- /dev/null +++ b/src/Stats/Species.js @@ -0,0 +1,52 @@ +const CellStates = require("../Organism/Cell/CellStates"); + +class Species { + constructor(anatomy, ancestor, start_tick) { + this.anatomy = anatomy; + this.ancestor = ancestor; + this.population = 1; + this.cumulative_pop = 1; + this.start_tick = start_tick; + this.end_tick = -1; + this.color = Math.floor(Math.random()*16777215).toString(16); + if (ancestor != null) { + // needs to be reworked, maybe removed + var mutator = Math.floor(Math.random()*16777215)-8000000; + this.color = (mutator + parseInt(ancestor.color, 16)).toString(16); + } + this.name = '_' + Math.random().toString(36).substr(2, 9); + this.extinct = false; + this.calcAnatomyDetails(); + } + + calcAnatomyDetails() { + var cell_counts = {}; + for (let c of CellStates.living) { + cell_counts[c.name] = 0; + } + for (let cell of this.anatomy.cells) { + cell_counts[cell.state.name]+=1; + } + this.cell_counts=cell_counts; + } + + addPop() { + this.population++; + this.cumulative_pop++; + } + + decreasePop() { + this.population--; + if (this.population <= 0) { + this.extinct = true; + const FossilRecord = require("./FossilRecord"); + FossilRecord.fossilize(this); + } + } + + lifespan() { + return this.end_tick - this.start_tick; + } +} + +module.exports = Species; \ No newline at end of file diff --git a/src/Stats/StatsPanel.js b/src/Stats/StatsPanel.js new file mode 100644 index 0000000..4487861 --- /dev/null +++ b/src/Stats/StatsPanel.js @@ -0,0 +1,66 @@ +const PopulationChart = require("./Charts/PopulationChart"); +const SpeciesChart = require("./Charts/SpeciesChart"); +const MutationChart = require("./Charts/MutationChart"); +const CellsChart = require("./Charts/CellsChart"); +const FossilRecord = require("./FossilRecord"); + + +const ChartSelections = [PopulationChart, SpeciesChart, CellsChart, MutationChart]; + +class StatsPanel { + constructor(env) { + this.defineControls(); + this.chart_selection = 0; + this.setChart(); + this.env = env; + this.last_reset_count=env.reset_count; + } + + setChart(selection=this.chart_selection) { + this.chart_controller = new ChartSelections[selection](); + this.chart_controller.setData(); + this.chart_controller.render(); + } + + startAutoRender() { + this.setChart(); + this.render_loop = setInterval(function(){this.updateChart();}.bind(this), 1000); + } + + stopAutoRender() { + clearInterval(this.render_loop); + } + + defineControls() { + $('#chart-option').change ( function() { + this.chart_selection = $("#chart-option")[0].selectedIndex; + this.setChart(); + }.bind(this)); + } + + updateChart() { + if (this.last_reset_count < this.env.reset_count){ + this.reset() + } + this.last_reset_count = this.env.reset_count; + this.chart_controller.updateData(); + this.chart_controller.render(); + } + + updateDetails() { + var org_count = this.env.organisms.length; + $('#org-count').text("Total Population: " + org_count); + $('#species-count').text("Number of Species: " + FossilRecord.extant_species.length); + $('#largest-org').text("Largest Organism Ever: " + this.env.largest_cell_count + " cells"); + $('#avg-mut').text("Average Mutation Rate: " + Math.round(this.env.averageMutability() * 100) / 100); + + + } + + reset() { + this.setChart(); + } + +} + +module.exports = StatsPanel; \ No newline at end of file