diff --git a/Changelog.md b/Changelog.md index 758168d..b8036e9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,32 @@ # Changelog +## 1.0.2 (12/21/2021) + +### UI Enhancements: +- New tab "World Controls" + - Relocated grid controls and auto reset to this tab + - Button to generate random walls with perlin noise + - Button to reset the environment with many randomly generated organisms + - Option to not clear walls on reset + - Option to pause on total extinction +- "Simulation controls" tab renamed to "Evolution Controls" +- Button to save/load Evolution Controls in a `.json` file +- Button to randomize the organism in the editor window +- Can now use drag view tool while rendering is off +- Reorganized "About" tab and left panel, embedded explanation video + +### Simulation Enhancements: +- New evolution control `Extra Mover Reproduction Cost`, which adds additional food cost for movers to reproduce +- Combined `Movers can rotate` and `Offspring rotate` evolution controls into `Rotation enabled` +- Fully max out simulation speed when slider is all the way to the right + +### Bug Fixes: +- Armor is no longer ignored when checking for clear reproduction space +- Chart data is now properly loaded/discarded when paused + + +Thanks to contributors: @Chrispykins @M4YX0R + ## 1.0.1 (12/4/2021) ### UI Enhancements: diff --git a/dist/css/style.css b/dist/css/style.css index 3b15873..5c7d20c 100644 --- a/dist/css/style.css +++ b/dist/css/style.css @@ -57,11 +57,15 @@ body{ img { image-rendering: -moz-crisp-edges; - image-rendering: -webkit-crisp-edges; - image-rendering: pixelated; - image-rendering: crisp-edges; - width: 60%; + image-rendering: -webkit-crisp-edges; + image-rendering: pixelated; + image-rendering: crisp-edges; + object-fit: cover; + width: 85%; + max-width: 500px; + max-height: 40%; border-radius: 10px; + overflow: hidden; } button { @@ -75,6 +79,7 @@ button { display: inline-block; font-size: 16px; min-width: 30px; + margin: 2px; } button:hover{ background-color: #81d2c7; @@ -133,6 +138,10 @@ button:active{ background-color: #81d2c7; color: black; } +.open-tab { + background-color: #66a39b; + color: black; +} .tab { grid-template-columns: repeat(2, 1fr); @@ -255,4 +264,19 @@ button:active{ } #maximize-hot-control { right: 10px; +} + +.grid-size-in { + width: 75px; +} + +#video { + height: 200px; + margin: auto; + margin-bottom: 0; + padding-bottom: 0; +} + +#reset-with-editor-org{ + margin-top: 5px; } \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index 99fba39..d2fe274 100644 --- a/dist/index.html +++ b/dist/index.html @@ -24,56 +24,34 @@ - -

Simulation Speed

+ Life Engine +

Simulation Speed

Target FPS: 60

- -
- - -
-

Auto reset count:

-

Grid Size

- - - - -
- - - - -
-
- +
-

About

+

About

Editor

-

Simulation Controls

-

Stats

-

Challenges

+

World Controls

+

Evolution Controls

+

Statistics

- Life Engine -

The Life Engine is a virtual ecosystem that allows organisms to grow, spread, and compete.

-

Each organism is made up by a structure of cells, which provide different benefits based on their color.

- -
-
-

Cell Types

+

The Life Engine

+

The Life Engine is a virtual ecosystem that allows organisms to reproduce, compete, and evolve.

+

Each organism is made up of different colored cells. Hover over each color to learn what it does.

@@ -83,14 +61,13 @@
-
-
-

Hover over each color to learn what it does. For a more in depth explanation of the simulation, view the - readme and you can explore the source code. -

+

+
+
+
@@ -118,6 +95,8 @@ + +
@@ -147,7 +126,7 @@ -
+

Brain

@@ -172,23 +151,55 @@

Move Away From: killer

- + + + + +
+
+

Grid Size

+ + + + +
+ + + + +
+ +

Reset Options

+ + +

Auto reset count:

+ + +
+ + + +
+
+
+
+ +
+ +
-

Simulation Controls

+

Evolution Controls



- - -
- - + +
@@ -198,6 +209,9 @@
+
+ +
@@ -221,7 +235,10 @@
- + + + +
@@ -246,22 +263,6 @@
-
-
-

Challenges

- - - - - - -
-
-

Select a Challenge

-
-

Challenge yourself to create interesting ecosystems and organisms. There is no formal way to win or lose, its just for fun!

-
-
diff --git a/src/Controllers/ControlModes.js b/src/Controllers/ControlModes.js index 882e500..3213e4e 100644 --- a/src/Controllers/ControlModes.js +++ b/src/Controllers/ControlModes.js @@ -6,7 +6,7 @@ const Modes = { Select: 4, Edit: 5, Clone: 6, - Drag: 7 + Drag: 7, } module.exports = Modes; \ No newline at end of file diff --git a/src/Controllers/ControlPanel.js b/src/Controllers/ControlPanel.js index 5396a9e..f57b94b 100644 --- a/src/Controllers/ControlPanel.js +++ b/src/Controllers/ControlPanel.js @@ -1,6 +1,8 @@ const Hyperparams = require("../Hyperparameters"); const Modes = require("./ControlModes"); const StatsPanel = require("../Stats/StatsPanel"); +const RandomOrganismGenerator = require("../Organism/RandomOrganismGenerator") +const WorldConfig = require("../WorldConfig"); class ControlPanel { constructor(engine) { @@ -8,11 +10,10 @@ class ControlPanel { this.defineMinMaxControls(); this.defineHotkeys(); this.defineEngineSpeedControls(); - this.defineGridSizeControls(); this.defineTabNavigation(); this.defineHyperparameterControls(); + this.defineWorldControls(); this.defineModeControls(); - this.defineChallenges(); this.fps = engine.fps; this.organism_record=0; this.env_controller = this.engine.env.controller; @@ -114,38 +115,57 @@ class ControlPanel { defineEngineSpeedControls(){ this.slider = document.getElementById("slider"); this.slider.oninput = function() { - this.fps = this.slider.value + const max_fps = 300; + this.fps = this.slider.value; + if (this.fps>=max_fps) this.fps = 1000; if (this.engine.running) { this.changeEngineSpeed(this.fps); } - $('#fps').text("Target FPS: "+this.fps); + let text = this.fps >= max_fps ? 'MAX' : this.fps; + $('#fps').text("Target FPS: "+text); }.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(); - } - else if (!this.engine.running){ - this.engine.start(this.fps); - } + // toggle pause + this.setPaused(this.engine.running); }.bind(this)); + $('.headless').click(function() { $('.headless').find("i").toggleClass("fa fa-eye"); $('.headless').find("i").toggleClass("fa fa-eye-slash"); - if (Hyperparams.headless){ + if (WorldConfig.headless){ $('#headless-notification').css('display', 'none'); this.engine.env.renderFull(); } else { $('#headless-notification').css('display', 'block'); } - Hyperparams.headless = !Hyperparams.headless; + WorldConfig.headless = !WorldConfig.headless; }.bind(this)); } - defineGridSizeControls() { + defineTabNavigation() { + this.tab_id = 'about'; + var self = this; + $('.tabnav-item').click(function() { + $('.tab').css('display', 'none'); + var tab = '#'+this.id+'.tab'; + $(tab).css('display', 'grid'); + $('.tabnav-item').removeClass('open-tab') + $('#'+this.id+'.tabnav-item').addClass('open-tab'); + self.engine.organism_editor.is_active = (this.id == 'editor'); + self.stats_panel.stopAutoRender(); + if (this.id === 'stats') { + self.stats_panel.startAutoRender(); + } + else if (this.id === 'editor') { + self.editor_controller.refreshDetailsPanel(); + } + self.tab_id = this.id; + }); + } + + defineWorldControls() { $('#fill-window').change(function() { if (this.checked) $('.col-row-input').css('display' ,'none'); @@ -168,24 +188,23 @@ class ControlPanel { 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'; - $(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(); - } - else if (this.id === 'editor') { - self.editor_controller.refreshDetailsPanel(); - } - self.tab_id = this.id; + $('#auto-reset').change(function() { + WorldConfig.auto_reset = this.checked; + }); + $('#auto-pause').change(function() { + WorldConfig.auto_pause = this.checked; + }); + $('#clear-walls-reset').change(function() { + WorldConfig.clear_walls_on_reset = this.checked; + }); + $('#reset-with-editor-org').click( () => { + let env = this.engine.env; + if (!env.reset(true, false)) return; + let center = env.grid_map.getCenter(); + let org = this.editor_controller.env.getCopyOfOrg(); + this.env_controller.add_new_species = true; + this.env_controller.dropOrganism(org, center[0], center[1]) }); } @@ -197,11 +216,8 @@ class ControlPanel { Hyperparams.lifespanMultiplier = $('#lifespan-multiplier').val(); }.bind(this)); - $('#mover-rot').change(function() { - Hyperparams.moversCanRotate = this.checked; - }); - $('#offspring-rot').change(function() { - Hyperparams.offspringRotate = this.checked; + $('#rot-enabled').change(function() { + Hyperparams.rotationEnabled = this.checked; }); $('#insta-kill').change(function() { Hyperparams.instaKill = this.checked; @@ -212,6 +228,9 @@ class ControlPanel { $('#food-drop-rate').change(function() { Hyperparams.foodDropProb = $('#food-drop-rate').val(); }); + $('#extra-mover-cost').change(function() { + Hyperparams.extraMoverFoodCost = parseInt($('#extra-mover-cost').val()); + }); $('#evolved-mutation').change( function() { if (this.checked) { @@ -252,14 +271,40 @@ class ControlPanel { $('#reset-rules').click(() => { this.setHyperparamDefaults(); }); + $('#save-controls').click(() => { + let data = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(Hyperparams)); + let downloadEl = document.getElementById('download-el'); + downloadEl.setAttribute("href", data); + downloadEl.setAttribute("download", "controls.json"); + downloadEl.click(); + }); + $('#load-controls').click(() => { + $('#upload-el').click(); + }); + $('#upload-el').change((e)=>{ + let files = e.target.files; + if (!files.length) {return;}; + let reader = new FileReader(); + reader.onload = (e) => { + let result=JSON.parse(e.target.result); + Hyperparams.loadJsonObj(result); + this.updateHyperparamUIValues(); + // have to clear the value so change() will be triggered if the same file is uploaded again + $('#upload-el')[0].value = ''; + }; + reader.readAsText(files[0]); + }); } setHyperparamDefaults() { Hyperparams.setDefaults(); + this.updateHyperparamUIValues(); + } + + updateHyperparamUIValues(){ $('#food-prod-prob').val(Hyperparams.foodProdProb); $('#lifespan-multiplier').val(Hyperparams.lifespanMultiplier); - $('#mover-rot').prop('checked', Hyperparams.moversCanRotate); - $('#offspring-rot').prop('checked', Hyperparams.offspringRotate); + $('#rot-enabled').prop('checked', Hyperparams.rotationEnabled); $('#insta-kill').prop('checked', Hyperparams.instaKill); $('#evolved-mutation').prop('checked', !Hyperparams.useGlobalMutability); $('#add-prob').val(Hyperparams.addProb); @@ -268,6 +313,7 @@ class ControlPanel { $('#movers-produce').prop('checked', Hyperparams.moversCanProduce); $('#food-blocks').prop('checked', Hyperparams.foodBlocksReproduction); $('#food-drop-rate').val(Hyperparams.foodDropProb); + $('#extra-mover-cost').val(Hyperparams.extraMoverFoodCost); $('#look-range').val(Hyperparams.lookRange); if (!Hyperparams.useGlobalMutability) { @@ -311,7 +357,6 @@ class ControlPanel { $('.edit-mode-btn').removeClass('selected'); $('.'+this.id).addClass('selected'); }); - $('.reset-view').click( function(){ this.env_controller.resetView(); }.bind(this)); @@ -324,19 +369,24 @@ class ControlPanel { $('#clear-env').click( () => { env.reset(true, false); this.stats_panel.reset(); - env.auto_reset = false; - $('#auto-reset').prop('checked', false);; - }); - $('#auto-reset').change(function() { - env.auto_reset = this.checked; }); + $('#random-walls').click( function() { + this.env_controller.randomizeWalls(); + }.bind(this)); $('#clear-walls').click( function() { this.engine.env.clearWalls(); }.bind(this)); $('#clear-editor').click( function() { this.engine.organism_editor.clear(); this.editor_controller.setEditorPanel(); - }.bind(this)) + }.bind(this)); + $('#generate-random').click( function() { + this.engine.organism_editor.createRandom(); + this.editor_controller.refreshDetailsPanel(); + }.bind(this)); + $('.reset-random').click( function() { + this.engine.organism_editor.resetWithRandomOrgs(this.engine.env); + }.bind(this)); window.onbeforeunload = function (e) { e = e || window.event; @@ -348,11 +398,22 @@ class ControlPanel { }; } - defineChallenges() { - $('.challenge-btn').click(function() { - $('#challenge-title').text($(this).text()); - $('#challenge-description').text($(this).val()); - }); + setPaused(paused) { + + if (paused) { + + $('.pause-button').find("i").removeClass("fa-pause"); + $('.pause-button').find("i").addClass("fa-play"); + if (this.engine.running) + this.engine.stop(); + } + else if (!paused) { + + $('.pause-button').find("i").addClass("fa-pause"); + $('.pause-button').find("i").removeClass("fa-play"); + if (!this.engine.running) + this.engine.start(this.fps); + } } setMode(mode) { @@ -383,7 +444,7 @@ class ControlPanel { } updateHeadlessIcon(delta_time) { - if (this.paused) + if (!this.engine.running) return; const min_opacity = 0.4; var op = this.headless_opacity + (this.opacity_change_rate*delta_time/1000); @@ -403,7 +464,7 @@ class ControlPanel { $('#fps-actual').text("Actual FPS: " + Math.floor(this.engine.actual_fps)); $('#reset-count').text("Auto reset count: " + this.engine.env.reset_count); this.stats_panel.updateDetails(); - if (Hyperparams.headless) + if (WorldConfig.headless) this.updateHeadlessIcon(delta_time); } diff --git a/src/Controllers/EditorController.js b/src/Controllers/EditorController.js index f0c101e..d5c2cb7 100644 --- a/src/Controllers/EditorController.js +++ b/src/Controllers/EditorController.js @@ -112,6 +112,7 @@ class EditorController extends CanvasController{ clearDetailsPanel() { $('#organism-details').css('display', 'none'); $('#edit-organism-details').css('display', 'none'); + $('#randomize-organism-details').css('display', 'none'); } refreshDetailsPanel() { @@ -213,6 +214,11 @@ class EditorController extends CanvasController{ var reaction = this.env.organism.brain.decisions[name]; $('#reaction-edit').val(reaction); } + + setRandomizePanel() { + this.clearDetailsPanel(); + $('#randomize-organism-details').css('display', 'block'); + } } module.exports = EditorController; diff --git a/src/Controllers/EnvironmentController.js b/src/Controllers/EnvironmentController.js index 828fe08..93f3ddb 100644 --- a/src/Controllers/EnvironmentController.js +++ b/src/Controllers/EnvironmentController.js @@ -4,7 +4,8 @@ const Modes = require("./ControlModes"); const CellStates = require("../Organism/Cell/CellStates"); const Neighbors = require("../Grid/Neighbors"); const FossilRecord = require("../Stats/FossilRecord"); -const Hyperparams = require("../Hyperparameters"); +const WorldConfig = require("../WorldConfig"); +const Perlin = require("../Utils/Perlin"); class EnvironmentController extends CanvasController{ constructor(env, canvas) { @@ -51,8 +52,34 @@ class EnvironmentController extends CanvasController{ this.scale = 1; } + /* + Iterate over grid from 0,0 to env.num_cols,env.num_rows and create random walls using perlin noise to create a more organic shape. + */ + randomizeWalls(thickness=1) { + this.env.clearWalls(); + const noise_threshold = -0.017; + let avg_noise = 0; + let resolution = 20; + Perlin.seed(); + + for (let r = 0; r < this.env.num_rows; r++) { + for (let c = 0; c < this.env.num_cols; c++) { + let xval = c/this.env.num_cols*(resolution/this.env.renderer.cell_size*(this.env.num_cols/this.env.num_rows)); + let yval = r/this.env.num_rows*(resolution/this.env.renderer.cell_size*(this.env.num_rows/this.env.num_cols)); + let noise = Perlin.get(xval, yval); + avg_noise += noise/(this.env.num_rows*this.env.num_cols); + if (noise > noise_threshold && noise < noise_threshold + thickness/resolution) { + let cell = this.env.grid_map.cellAt(c, r); + if (cell != null) { + if(cell.owner != null) cell.owner.die(); + this.env.changeCell(c, r, CellStates.wall, null); + } + } + } + } + } + updateMouseLocation(offsetX, offsetY){ - super.updateMouseLocation(offsetX, offsetY); } @@ -71,7 +98,7 @@ class EnvironmentController extends CanvasController{ } performModeAction() { - if (Hyperparams.headless) + if (WorldConfig.headless && this.mode != Modes.Drag) return; var mode = this.mode; var right_click = this.right_click; @@ -114,21 +141,7 @@ 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); - new_org.species.addPop(); - } + this.dropOrganism(this.org_to_clone, this.mouse_c, this.mouse_r); } break; case Modes.Drag: @@ -152,6 +165,29 @@ class EnvironmentController extends CanvasController{ } } + dropOrganism(organism, col, row) { + + // close the organism and drop it in the world + var new_org = new Organism(col, row, this.env, organism); + + if (new_org.isClear(col, row)) { + 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); + } + + this.env.addOrganism(new_org); + new_org.species.addPop(); + return true; + } + return false; + } + dropCellType(col, row, state, killBlocking=false) { for (var loc of Neighbors.allSelf){ var c=col + loc[0]; diff --git a/src/Environments/OrganismEditor.js b/src/Environments/OrganismEditor.js index b969774..6aadc0b 100644 --- a/src/Environments/OrganismEditor.js +++ b/src/Environments/OrganismEditor.js @@ -5,6 +5,7 @@ const Renderer = require('../Rendering/Renderer'); const CellStates = require('../Organism/Cell/CellStates'); const EditorController = require("../Controllers/EditorController"); const Species = require('../Stats/Species'); +const RandomOrganismGenerator = require('../Organism/RandomOrganismGenerator') class OrganismEditor extends Environment{ constructor() { @@ -87,6 +88,31 @@ class OrganismEditor extends Environment{ this.organism.updateGrid(); this.organism.species = new Species(this.organism.anatomy, null, 0); } + + createRandom() { + this.grid_map.fillGrid(CellStates.empty); + + this.organism = RandomOrganismGenerator.generate(this); + this.organism.updateGrid(); + this.organism.species = new Species(this.organism.anatomy, null, 0); + } + + resetWithRandomOrgs(env) { + let reset_confirmed = env.reset(true, false); + if (!reset_confirmed) return; + let numOrganisms = parseInt($('#num-random-orgs').val()); + + let size = Math.ceil(8); + + for (let i=0; i 0) { + if (WorldConfig.auto_pause) + $('.pause-button')[0].click(); + else if(WorldConfig.auto_reset) { + this.reset_count++; + this.reset(false); + } } } @@ -104,7 +109,8 @@ class WorldEnvironment extends Environment{ clearWalls() { for(var wall of this.walls){ - if (this.grid_map.cellAt(wall.col, wall.row).state == CellStates.wall) + let wcell = this.grid_map.cellAt(wall.col, wall.row); + if (wcell && wcell.state == CellStates.wall) this.changeCell(wall.col, wall.row, CellStates.empty, null); } } @@ -132,17 +138,17 @@ class WorldEnvironment extends Environment{ reset(confirm_reset=true, reset_life=true) { if (confirm_reset && !confirm('The current environment will be lost. Proceed?')) - return; + return false; this.organisms = []; - this.grid_map.fillGrid(CellStates.empty); + this.grid_map.fillGrid(CellStates.empty, !WorldConfig.clear_walls_on_reset); this.renderer.renderFullGrid(this.grid_map.grid); this.total_mutability = 0; this.total_ticks = 0; FossilRecord.clear_record(); if (reset_life) this.OriginOfLife(); - + return true; } resizeGridColRow(cell_size, cols, rows) { @@ -154,9 +160,9 @@ class WorldEnvironment extends Environment{ resizeFillWindow(cell_size) { this.renderer.cell_size = cell_size; this.renderer.fillWindow('env'); - 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.num_cols = Math.ceil(this.renderer.width / cell_size); + this.num_rows = Math.ceil(this.renderer.height / cell_size); + this.grid_map.resize(this.num_cols, this.num_rows, cell_size); } } diff --git a/src/Grid/GridMap.js b/src/Grid/GridMap.js index 24f167c..84db4ca 100644 --- a/src/Grid/GridMap.js +++ b/src/Grid/GridMap.js @@ -21,9 +21,10 @@ class GridMap { } } - fillGrid(state) { + fillGrid(state, ignore_walls=false) { for (var col of this.grid) { for (var cell of col) { + if (ignore_walls && cell.state===CellStates.wall) continue; cell.setType(state); cell.owner = null; cell.cell_owner = null; diff --git a/src/Hyperparameters.js b/src/Hyperparameters.js index 4199cd9..9579f11 100644 --- a/src/Hyperparameters.js +++ b/src/Hyperparameters.js @@ -2,8 +2,6 @@ const Neighbors = require("./Grid/Neighbors"); const Hyperparams = { setDefaults: function() { - this.headless = false; - this.lifespanMultiplier = 100; this.foodProdProb = 5; this.killableNeighbors = Neighbors.adjacent; @@ -16,8 +14,7 @@ const Hyperparams = { this.changeProb = 33; this.removeProb = 33; - this.moversCanRotate = true; - this.offspringRotate = true; + this.rotationEnabled = true; this.foodBlocksReproduction = true; this.moversCanProduce = false; @@ -27,7 +24,15 @@ const Hyperparams = { this.lookRange = 20; this.foodDropProb = 0; + + this.extraMoverFoodCost = 0; }, + + loadJsonObj(obj) { + for (let key in obj) { + this[key] = obj[key]; + } + } } Hyperparams.setDefaults(); diff --git a/src/Organism/Anatomy.js b/src/Organism/Anatomy.js index 8175024..b1c3cb6 100644 --- a/src/Organism/Anatomy.js +++ b/src/Organism/Anatomy.js @@ -91,6 +91,22 @@ class Anatomy { getRandomCell() { return this.cells[Math.floor(Math.random() * this.cells.length)]; } + + getNeighborsOfCell(col, row) { + + var neighbors = []; + + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + + var neighbor = this.getLocalCell(col + x, row + y); + if (neighbor) + neighbors.push(neighbor) + } + } + + return neighbors; + } } module.exports = Anatomy; \ No newline at end of file diff --git a/src/Organism/Cell/CellStates.js b/src/Organism/Cell/CellStates.js index 4a01aa7..9bf54f8 100644 --- a/src/Organism/Cell/CellStates.js +++ b/src/Organism/Cell/CellStates.js @@ -95,6 +95,7 @@ const CellStates = { return this.living[Math.floor(Math.random() * this.living.length)]; } } + CellStates.defineLists(); module.exports = CellStates; diff --git a/src/Organism/Cell/GridCell.js b/src/Organism/Cell/GridCell.js index 10d637b..fd3bdff 100644 --- a/src/Organism/Cell/GridCell.js +++ b/src/Organism/Cell/GridCell.js @@ -5,7 +5,7 @@ const Hyperparams = require("../../Hyperparameters"); class Cell{ constructor(state, col, row, x, y){ this.owner = null; // owner organism - this.cell_owner = null; // owner cell of ^that organism + this.cell_owner = null; // specific body cell of the owner organism that occupies this grid cell this.setType(state); this.col = col; this.row = row; diff --git a/src/Organism/Organism.js b/src/Organism/Organism.js index 672c302..2d00ee6 100644 --- a/src/Organism/Organism.js +++ b/src/Organism/Organism.js @@ -17,7 +17,7 @@ class Organism { this.anatomy = new Anatomy(this) this.direction = Directions.down; // direction of movement this.rotation = Directions.up; // direction of rotation - this.can_rotate = Hyperparams.moversCanRotate; + this.can_rotate = Hyperparams.rotationEnabled; this.move_count = 0; this.move_range = 4; this.ignore_brain_for = 0; @@ -47,11 +47,10 @@ class Organism { // amount of food required before it can reproduce foodNeeded() { - return this.anatomy.cells.length; + return this.anatomy.is_mover ? this.anatomy.cells.length + Hyperparams.extraMoverFoodCost : this.anatomy.cells.length; } lifespan() { - // console.log(Hyperparams.lifespanMultiplier) return this.anatomy.cells.length * Hyperparams.lifespanMultiplier; } @@ -63,7 +62,7 @@ class Organism { //produce mutated child //check nearby locations (is there room and a direct path) var org = new Organism(0, 0, this.env, this); - if(Hyperparams.offspringRotate){ + if(Hyperparams.rotationEnabled){ org.rotation = Directions.getRandomDirection(); } var prob = this.mutability; @@ -118,7 +117,7 @@ class Organism { org.species.addPop(); } } - this.food_collected -= this.foodNeeded(); + Math.max(this.food_collected -= this.foodNeeded(), 0); } @@ -237,14 +236,13 @@ class Organism { return cell != null && (cell.state == CellStates.empty || cell.owner == this || cell.owner == parent || cell.state == CellStates.food); } - isClear(col, row, rotation=this.rotation, ignore_armor=false) { + isClear(col, row, rotation=this.rotation) { for(var loccell of this.anatomy.cells) { var cell = this.getRealCell(loccell, col, row, rotation); 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)){ + if (cell.owner==this || cell.state==CellStates.empty || (!Hyperparams.foodBlocksReproduction && cell.state==CellStates.food)){ continue; } return false; diff --git a/src/Organism/Perception/Brain.js b/src/Organism/Perception/Brain.js index e52d8a3..20c6258 100644 --- a/src/Organism/Perception/Brain.js +++ b/src/Organism/Perception/Brain.js @@ -32,8 +32,12 @@ class Brain { this.decisions[CellStates.eye.name] = Decision.neutral; } - randomizeDecisions() { + randomizeDecisions(randomize_all=false) { // randomize the non obvious decisions + if (randomize_all) { + this.decisions[CellStates.food.name] = Decision.getRandom(); + this.decisions[CellStates.killer.name] = Decision.getRandom(); + } this.decisions[CellStates.mouth.name] = Decision.getRandom(); this.decisions[CellStates.producer.name] = Decision.getRandom(); this.decisions[CellStates.mover.name] = Decision.getRandom(); @@ -79,4 +83,6 @@ class Brain { } } +Brain.Decision = Decision; + module.exports = Brain; \ No newline at end of file diff --git a/src/Organism/RandomOrganismGenerator.js b/src/Organism/RandomOrganismGenerator.js new file mode 100644 index 0000000..ff3735d --- /dev/null +++ b/src/Organism/RandomOrganismGenerator.js @@ -0,0 +1,68 @@ +const CellStates = require("./Cell/CellStates"); +const Organism = require("./Organism"); +const Brain = require("./Perception/Brain") + +class RandomOrganismGenerator { + + static generate(env) { + + var center = env.grid_map.getCenter(); + var organism = new Organism(center[0], center[1], env, null); + organism.anatomy.addDefaultCell(CellStates.mouth, 0, 0); + + var outermostLayer = RandomOrganismGenerator.organismLayers; + var x, y; + + // iterate from center to edge of organism + // layer 0 is the central cell of the organism + for (var layer = 1; layer <= outermostLayer; layer++) { + + var someCellSpawned = false; + var spawnChance = RandomOrganismGenerator.cellSpawnChance * 1 - ((layer - 1) / outermostLayer); + + // top + y = -layer; + for (x = -layer; x <= layer; x++) + someCellSpawned = RandomOrganismGenerator.trySpawnCell(organism, x, y, spawnChance); + + // bottom + y = layer; + for (x = -layer; x <= layer; x++) + someCellSpawned = RandomOrganismGenerator.trySpawnCell(organism, x, y, spawnChance); + + // left + x = -layer; + for (y = -layer + 1; y <= layer - 1; y++) + someCellSpawned = RandomOrganismGenerator.trySpawnCell(organism, x, y, spawnChance); + + // right + x = layer; + for (y = -layer + 1; y < layer - 1; y++) + someCellSpawned = RandomOrganismGenerator.trySpawnCell(organism, x, y, spawnChance); + + if (!someCellSpawned) + break; + } + + // randomize the organism's brain + organism.brain.randomizeDecisions(true); + + return organism; + } + + static trySpawnCell(organism, x, y, spawnChance) { + + var neighbors = organism.anatomy.getNeighborsOfCell(x, y); + if (neighbors.length && Math.random() < spawnChance) { + organism.anatomy.addRandomizedCell(CellStates.getRandomLivingType(), x, y); + return true; + } + return false; + } + +} + +RandomOrganismGenerator.organismLayers = 4; +RandomOrganismGenerator.cellSpawnChance = 0.75; + +module.exports = RandomOrganismGenerator; \ No newline at end of file diff --git a/src/Stats/Charts/ChartController.js b/src/Stats/Charts/ChartController.js index 999fdf4..16b3ed8 100644 --- a/src/Stats/Charts/ChartController.js +++ b/src/Stats/Charts/ChartController.js @@ -45,28 +45,33 @@ class ChartController { } 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; + let record_size = FossilRecord.tick_record.length; + let data_points = this.data[0].dataPoints; + let newest_t = -1; + if (data_points.length>0) { + newest_t = this.data[0].dataPoints[data_points.length-1].x; } - if (newest_t < FossilRecord.tick_record[r_len-1]) { - this.addNewest(); + let to_add = 0; + let cur_t = FossilRecord.tick_record[record_size-1]; + // first count up the number of new datapoints the chart is missing + while (cur_t !== newest_t) { + to_add++; + cur_t = FossilRecord.tick_record[record_size-to_add-1] } - if (oldest_t < FossilRecord.tick_record[0]) { + // then add them in order + this.addNewest(to_add) + + // remove oldest datapoints until the chart is the same size as the saved records + while (data_points.length > FossilRecord.tick_record.length) { this.removeOldest(); } } - addNewest() { - var i = FossilRecord.tick_record.length-1; - this.addDataPoint(i); - } - - addDataPoint(i) { - alert("Must override addDataPoint") + addNewest(to_add) { + for (let i=to_add; i>0; i--) { + let j = FossilRecord.tick_record.length-i; + this.addDataPoint(j); + } } removeOldest() { @@ -75,6 +80,10 @@ class ChartController { } } + addDataPoint(i) { + alert("Must override addDataPoint") + } + clear() { this.data.length = 0; this.chart.render(); diff --git a/src/Stats/FossilRecord.js b/src/Stats/FossilRecord.js index c33b822..3189f7a 100644 --- a/src/Stats/FossilRecord.js +++ b/src/Stats/FossilRecord.js @@ -82,8 +82,7 @@ const FossilRecord = { 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) { + while (this.tick_record.length > this.record_size_limit) { this.tick_record.shift(); this.pop_counts.shift(); this.species_counts.shift(); diff --git a/src/Utils/Perlin.js b/src/Utils/Perlin.js new file mode 100644 index 0000000..ab9b5ca --- /dev/null +++ b/src/Utils/Perlin.js @@ -0,0 +1,46 @@ +let perlin = { + rand_vect: function(){ + let theta = Math.random() * 2 * Math.PI; + return {x: Math.cos(theta), y: Math.sin(theta)}; + }, + dot_prod_grid: function(x, y, vx, vy){ + let g_vect; + let d_vect = {x: x - vx, y: y - vy}; + if (this.gradients[[vx,vy]]){ + g_vect = this.gradients[[vx,vy]]; + } else { + g_vect = this.rand_vect(); + this.gradients[[vx, vy]] = g_vect; + } + return d_vect.x * g_vect.x + d_vect.y * g_vect.y; + }, + smootherstep: function(x){ + return 6*x**5 - 15*x**4 + 10*x**3; + }, + interp: function(x, a, b){ + return a + this.smootherstep(x) * (b-a); + }, + seed: function(){ + this.gradients = {}; + this.memory = {}; + }, + get: function(x, y) { + if (this.memory.hasOwnProperty([x,y])) + return this.memory[[x,y]]; + let xf = Math.floor(x); + let yf = Math.floor(y); + //interpolate + let tl = this.dot_prod_grid(x, y, xf, yf); + let tr = this.dot_prod_grid(x, y, xf+1, yf); + let bl = this.dot_prod_grid(x, y, xf, yf+1); + let br = this.dot_prod_grid(x, y, xf+1, yf+1); + let xt = this.interp(x-xf, tl, tr); + let xb = this.interp(x-xf, bl, br); + let v = this.interp(y-yf, xt, xb); + this.memory[[x,y]] = v; + return v; + } +} +perlin.seed(); + +module.exports = perlin; \ No newline at end of file diff --git a/src/WorldConfig.js b/src/WorldConfig.js new file mode 100644 index 0000000..2c8f432 --- /dev/null +++ b/src/WorldConfig.js @@ -0,0 +1,8 @@ +const WorldConfig = { + headless: false, + clear_walls_on_reset: false, + auto_reset: true, + auto_pause: false, +} + +module.exports = WorldConfig; \ No newline at end of file