diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..a2fcfa7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,11 @@ +--- +name: Feature request (Please Use discussion tab instead) +about: Request a new feature for the life engine +title: '' +labels: +assignees: '' + +--- + + +# Do not make feature requests here, please go to the discussions tab: https://github.com/MaxRobinsonTheGreat/LifeEngine/discussions diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..844c04a --- /dev/null +++ b/Changelog.md @@ -0,0 +1,58 @@ +# Changelog + +## 1.0.2 (current development) + +### UI Enhancements: +- New tab for world controls + - Relocated grid controls, auto reset to this tab + - Button to generate random walls with perlin noise + - Options for starting state, including simple producer and empty state + - Option to not clear walls when resetting + - Option to pause on total extinction +- Combined `Movers can rotate` and `Offspring rotate` simulation controls into `Rotation enabled` +- Can now drag view while rendering is off + +### Simulation Enhancements: +- + +### Bug Fixes: +- Armor is no longer ignored when checking for clear reproduction space + + +Thanks to contributors: + +## 1.0.1 (12/4/2021) + +### UI Enhancements: +- Hotkeys/improved zoom controls: [#47](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/47) + - `A` reset view + - `S/middle mouse button` pan + - `D` drop walls + - `F` drop food + - `G` click to kill + - `H` headless rendering toggle + - `Spacebar/J` pause/play toggle + - `Z` select organism + - `X` edit organism + - `C` drop organism + - `V` toggle hud + - `B` destroy all walls + - `Q` min/max control panel +- Improved mutation probability controls: [#43](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/43) +- Ability to edit individual organism's mutability: [#46](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/46) +- Added clear button and improved reset warnings: [#64](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/64) +- Control Panel is minimized by default: [#64](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/64) + +### Simulation Enhancements: +- Default food prodcution probability increased from 4->5 + +### Bug Fixes: +- Fixed actual FPS display: [#45](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/45) +- Fixed slow down/crash on very long runs: [#63](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/63) +- Spelling Fix: [#31](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/31) + + +Thanks to contributors: @TrevorSayre @EvaisaGiac @Chrispykins + +## 1.0.0 +Initial release. diff --git a/dist/css/style.css b/dist/css/style.css index 86af6b0..0a1747d 100644 --- a/dist/css/style.css +++ b/dist/css/style.css @@ -37,7 +37,7 @@ body{ bottom: 0; position: fixed; background-color: #3a4b68; - display: grid; + display: none; grid-template-columns: repeat(3, 1fr); /* opacity: 0.8; */ } @@ -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,10 +79,14 @@ button { display: inline-block; font-size: 16px; min-width: 30px; + margin: 2px; } button:hover{ background-color: #81d2c7; } +button:active{ + background-color: #595e77; +} .icon-links { font-size: 35px; @@ -130,6 +138,10 @@ button:hover{ background-color: #81d2c7; color: black; } +.open-tab { + background-color: #66a39b; + color: black; +} .tab { grid-template-columns: repeat(2, 1fr); @@ -163,10 +175,7 @@ button:hover{ height: 30px; margin-top: 5px; } -.edit-mode-btn:hover{ - background-color: #81d2c7; -} -.edit-mode-btn#drag-view { +.edit-mode-btn.selected { background-color: #81d2c7; } .randomize-button { @@ -239,7 +248,7 @@ button:hover{ position: fixed; bottom: 10px; padding-left: 10px; - display: none; + /* display: none; */ } #headless-notification { @@ -258,4 +267,15 @@ button:hover{ } #maximize-hot-control { right: 10px; +} + +.grid-size-in { + width: 75px; +} + +#video { + height: 200px; + margin: auto; + margin-bottom: 0; + padding-bottom: 0; } \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index bd57ef2..829704c 100644 --- a/dist/index.html +++ b/dist/index.html @@ -19,78 +19,56 @@
- - - - - - + + + + +
-

Simulation Speed

+ Life Engine +

Simulation Speed

- - + +

Target FPS: 60

- - - -
-

Auto reset count:

-
-

Grid Size

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

About

+

About

Editor

+

World Controls

Simulation Controls

-

Stats

-

Challenges

- +

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.

-
+
-
-
-

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. -

+

+
+
+
@@ -98,10 +76,10 @@
- - + + + -
@@ -113,7 +91,7 @@
-
+
@@ -144,6 +122,10 @@
+
+ + +

Brain

@@ -190,6 +172,43 @@
+
+
+

Grid Size

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

Reset Options

+ +
+ + +

Auto reset count:

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

Simulation Controls

@@ -199,11 +218,8 @@
- - -
- - + +
@@ -213,6 +229,9 @@
+
+ +
@@ -236,7 +255,10 @@
- + + + +
@@ -280,20 +302,20 @@
- - - - - - - + + + + + + +
- +
- \ No newline at end of file + diff --git a/src/Controllers/CanvasController.js b/src/Controllers/CanvasController.js index 95a03d9..e14cb2a 100644 --- a/src/Controllers/CanvasController.js +++ b/src/Controllers/CanvasController.js @@ -9,6 +9,7 @@ class CanvasController{ this.mouse_c; this.mouse_r; this.left_click = false; + this.middle_click = false; this.right_click = false; this.cur_cell = null; this.cur_org = null; @@ -30,16 +31,21 @@ class CanvasController{ evt.preventDefault(); this.updateMouseLocation(evt.offsetX, evt.offsetY) this.mouseUp(); - this.left_click=false; - this.right_click=false; + if (evt.button == 0) + this.left_click = false; + if (evt.button == 1) + this.middle_click = false; + if (evt.button == 2) + this.right_click = false; }.bind(this)); this.canvas.addEventListener('mousedown', function(evt) { evt.preventDefault(); this.updateMouseLocation(evt.offsetX, evt.offsetY) - if (evt.button == 0) { + if (evt.button == 0) this.left_click = true; - } + if (evt.button == 1) + this.middle_click = true; if (evt.button == 2) this.right_click = true; this.mouseDown(); @@ -50,11 +56,25 @@ class CanvasController{ }); this.canvas.addEventListener('mouseleave', function(){ - this.right_click = false; - this.left_click = false; + this.left_click = false; + this.middle_click = false; + this.right_click = false; this.env.renderer.clearAllHighlights(true); }.bind(this)); + this.canvas.addEventListener('mouseenter', function(evt) { + + this.left_click = !!(evt.buttons & 1); + this.right_click = !!(evt.buttons & 2); + this.middle_click = !!(evt.buttons & 4); + + this.updateMouseLocation(evt.offsetX, evt.offsetY); + this.start_x = this.mouse_x; + this.start_y = this.mouse_y; + + + }.bind(this)) + } updateMouseLocation(offsetX, offsetY) { diff --git a/src/Controllers/ControlPanel.js b/src/Controllers/ControlPanel.js index 2174bf8..fc86ce6 100644 --- a/src/Controllers/ControlPanel.js +++ b/src/Controllers/ControlPanel.js @@ -2,15 +2,17 @@ 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) { this.engine = engine; this.defineMinMaxControls(); + this.defineHotkeys(); this.defineEngineSpeedControls(); - this.defineGridSizeControls(); this.defineTabNavigation(); this.defineHyperparameterControls(); + this.defineWorldControls(); this.defineModeControls(); this.defineChallenges(); this.fps = engine.fps; @@ -22,7 +24,8 @@ class ControlPanel { this.stats_panel = new StatsPanel(this.engine.env); this.headless_opacity = 1; this.opacity_change_rate = -0.8; - //this.paused=false; + this.paused=false; + this.setHyperparamDefaults(); } defineMinMaxControls(){ @@ -42,48 +45,85 @@ class ControlPanel { this.stats_panel.startAutoRender(); } }); - const V_KEY = 118; - $('body').keypress( (e) => { - if (e.which === V_KEY) { - 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); - } - else { - $('.control-panel').css('display', 'none'); - $('.hot-controls').css('display', 'none'); - } - this.no_hud = !this.no_hud; + } + + defineHotkeys() { + $('body').keydown( (e) => { + switch (e.key.toLowerCase()) { + // hot bar controls + case 'a': + $('.reset-view')[0].click(); + break; + case 's': + $('#drag-view').click(); + break; + case 'd': + $('#wall-drop').click(); + break; + case 'f': + $('#food-drop').click(); + break; + case 'g': + $('#click-kill').click(); + break; + case 'h': + $('.headless')[0].click(); + break; + case 'j': + case ' ': + e.preventDefault(); + $('.pause-button')[0].click(); + break; + // miscellaneous hotkeys + case 'q': // minimize/maximize control panel + e.preventDefault(); + if (this.control_panel_active) + $('#minimize').click(); + else + $('#maximize').click(); + break; + case 'z': + $('#select').click(); + break; + case 'x': + $('#edit').click(); + break; + case 'c': + $('#drop-org').click(); + break; + case 'v': // toggle hud + 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); + } + else { + $('.control-panel').css('display', 'none'); + $('.hot-controls').css('display', 'none'); + } + this.no_hud = !this.no_hud; + break; + case 'b': + $('#clear-walls').click(); } }); - // 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(){ 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() { @@ -94,18 +134,39 @@ class ControlPanel { $('.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'); @@ -128,22 +189,20 @@ 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(); - } - 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; + }) + + $('#start-state').change ( function() { + WorldConfig.start_state = $("#start-state").val(); + }.bind(this)); } defineHyperparameterControls() { @@ -154,11 +213,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; @@ -169,6 +225,10 @@ class ControlPanel { $('#food-drop-rate').change(function() { Hyperparams.foodDropProb = $('#food-drop-rate').val(); }); + $('#extra-mover-cost').change(function() { + console.log(parseInt($('#extra-mover-cost').val())) + Hyperparams.extraMoverFoodCost = parseInt($('#extra-mover-cost').val()); + }); $('#evolved-mutation').change( function() { if (this.checked) { @@ -188,15 +248,12 @@ class ControlPanel { switch(this.id){ case "add-prob": Hyperparams.addProb = this.value; - Hyperparams.balanceMutationProbs(1); break; case "change-prob": Hyperparams.changeProb = this.value; - Hyperparams.balanceMutationProbs(2); break; case "remove-prob": Hyperparams.removeProb = this.value; - Hyperparams.balanceMutationProbs(3); break; } $('#add-prob').val(Math.floor(Hyperparams.addProb)); @@ -209,31 +266,62 @@ class ControlPanel { $('#food-blocks').change( function() { Hyperparams.foodBlocksReproduction = this.checked; }); - $('#reset-rules').click( function() { - Hyperparams.setDefaults(); - $('#food-prod-prob').val(Hyperparams.foodProdProb); - $('#lifespan-multiplier').val(Hyperparams.lifespanMultiplier); - $('#mover-rot').prop('checked', Hyperparams.moversCanRotate); - $('#offspring-rot').prop('checked', Hyperparams.offspringRotate); - $('#insta-kill').prop('checked', Hyperparams.instaKill); - $('#evolved-mutation').prop('checked', !Hyperparams.useGlobalMutability); - $('#add-prob').val(Hyperparams.addProb); - $('#change-prob').val(Hyperparams.changeProb); - $('#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'); - } - else { - $('.global-mutation-in').css('display', 'block'); - $('#avg-mut').css('display', 'none'); - } + $('#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); + $('#rot-enabled').prop('checked', Hyperparams.rotationEnabled); + $('#insta-kill').prop('checked', Hyperparams.instaKill); + $('#evolved-mutation').prop('checked', !Hyperparams.useGlobalMutability); + $('#add-prob').val(Hyperparams.addProb); + $('#change-prob').val(Hyperparams.changeProb); + $('#remove-prob').val(Hyperparams.removeProb); + $('#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) { + $('.global-mutation-in').css('display', 'none'); + $('#avg-mut').css('display', 'block'); + } + else { + $('.global-mutation-in').css('display', 'block'); + $('#avg-mut').css('display', 'none'); + } } defineModeControls() { @@ -257,23 +345,18 @@ class ControlPanel { break; case "edit": self.setMode(Modes.Edit); - self.editor_controller.setEditorPanel(); break; case "randomize": self.setMode(Modes.Randomize); self.editor_controller.setRandomizePanel(); 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); } - $('.edit-mode-btn').css('background-color', '#9099c2'); - $('#'+this.id).css('background-color', '#81d2c7'); + $('.edit-mode-btn').removeClass('selected'); + $('.'+this.id).addClass('selected'); }); $('.reset-view').click( function(){ @@ -282,16 +365,18 @@ class ControlPanel { var env = this.engine.env; $('#reset-env').click( function() { - this.engine.env.reset(); + env.reset(); this.stats_panel.reset(); }.bind(this)); - $('#auto-reset').change(function() { - env.auto_reset = this.checked; + $('#clear-env').click( () => { + env.reset(true, false); + this.stats_panel.reset(); }); + $('#random-walls').click( function() { + this.env_controller.randomizeWalls(); + }.bind(this)); $('#clear-walls').click( function() { - if (confirm("Are you sure you want to clear all the walls?")) { - this.engine.env.clearWalls(); - } + this.engine.env.clearWalls(); }.bind(this)); $('#clear-editor').click( function() { this.engine.organism_editor.clear(); @@ -315,6 +400,16 @@ class ControlPanel { this.setPaused(true); this.engine.organism_editor.createRandomWorld(this.engine.env); }.bind(this)); + }.bind(this)) + + window.onbeforeunload = function (e) { + e = e || window.event; + let return_str = 'this will cause a confirmation on page close' + if (e) { + e.returnValue = return_str; + } + return return_str; + }; } defineChallenges() { @@ -345,6 +440,17 @@ class ControlPanel { setMode(mode) { this.env_controller.mode = mode; this.editor_controller.mode = mode; + + if (mode == Modes.Edit) { + this.editor_controller.setEditorPanel(); + } + + if (mode == Modes.Clone) { + this.env_controller.org_to_clone = this.engine.organism_editor.getCopyOfOrg(); + this.env_controller.add_new_species = this.editor_controller.new_species; + this.editor_controller.new_species = false; + // console.log(this.env_controller.add_new_species) + } } setEditorOrganism(org) { @@ -354,17 +460,17 @@ class ControlPanel { } changeEngineSpeed(change_val) { - this.engine.stop(); - this.engine.start(change_val) + this.engine.restart(change_val) this.fps = this.engine.fps; } updateHeadlessIcon(delta_time) { if (this.engine.running) return; + const min_opacity = 0.4; var op = this.headless_opacity + (this.opacity_change_rate*delta_time/1000); - if (op <= 0.4){ - op=0.4; + if (op <= min_opacity){ + op=min_opacity; this.opacity_change_rate = -this.opacity_change_rate; } else if (op >= 1){ @@ -379,7 +485,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 2a47afa..d5c2cb7 100644 --- a/src/Controllers/EditorController.js +++ b/src/Controllers/EditorController.js @@ -93,6 +93,10 @@ class EditorController extends CanvasController{ $('#move-range-edit').change ( function() { this.env.organism.move_range = parseInt($('#move-range-edit').val()); }.bind(this)); + + $('#mutation-rate-edit').change ( function() { + this.env.organism.mutability = parseInt($('#mutation-rate-edit').val()); + }.bind(this)); $('#observation-type-edit').change ( function() { this.setBrainEditorValues($('#observation-type-edit').val()); this.setBrainDetails(); @@ -111,6 +115,13 @@ class EditorController extends CanvasController{ $('#randomize-organism-details').css('display', 'none'); } + refreshDetailsPanel() { + if (this.mode === Modes.Edit) + this.setEditorPanel(); + else + this.setDetailsPanel(); + } + setDetailsPanel() { this.clearDetailsPanel(); var org = this.env.organism; @@ -118,7 +129,8 @@ class EditorController extends CanvasController{ $('.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) { + + if (Hyperparams.useGlobalMutability) { $('#mutation-rate').css('display', 'none'); } else { @@ -142,6 +154,14 @@ class EditorController extends CanvasController{ if (this.setMoveRangeVisibility()){ $('#move-range-edit').val(org.move_range); } + + $('#mutation-rate-edit').val(org.mutability); + if (Hyperparams.useGlobalMutability) { + $('#mutation-rate-cont').css('display', 'none'); + } + else { + $('#mutation-rate-cont').css('display', 'block'); + } if (this.setBrainPanelVisibility()){ this.setBrainEditorValues($('#observation-type-edit').val()); diff --git a/src/Controllers/EnvironmentController.js b/src/Controllers/EnvironmentController.js index cf0c644..ea16248 100644 --- a/src/Controllers/EnvironmentController.js +++ b/src/Controllers/EnvironmentController.js @@ -4,12 +4,13 @@ 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) { super(env, canvas); - this.mode = Modes.Drag; + this.mode = Modes.FoodDrop; this.org_to_clone = null; this.add_new_species = false; this.defineZoomControls(); @@ -28,22 +29,14 @@ class EnvironmentController extends CanvasController{ // Restrict scale scale = Math.max(0.5, this.scale+(sign*zoom_speed)); - if (scale != 0.5) { - var cur_top = parseInt($('#env-canvas').css('top')); - var cur_left = parseInt($('#env-canvas').css('left')); - if (sign == 1) { - // If we're zooming in, zoom towards wherever the mouse is - var diff_x = ((this.canvas.width/2-cur_left/this.scale) - this.mouse_x)*this.scale/1.5; - var diff_y = ((this.canvas.height/2-cur_top/this.scale) - this.mouse_y)*this.scale/1.5; - } - else { - // If we're zooming out, zoom out towards the center - var diff_x = -cur_left/scale; - var diff_y = -cur_top/scale; - } - $('#env-canvas').css('top', (cur_top+diff_y)+'px'); - $('#env-canvas').css('left', (cur_left+diff_x)+'px'); - } + var cur_top = parseInt($('#env-canvas').css('top')); + var cur_left = parseInt($('#env-canvas').css('left')); + + var diff_x = (this.canvas.width/2 - this.mouse_x) * (scale - this.scale); + var diff_y = (this.canvas.height/2 - this.mouse_y) * (scale - this.scale); + + $('#env-canvas').css('top', (cur_top+diff_y)+'px'); + $('#env-canvas').css('left', (cur_left+diff_x)+'px'); // Apply scale transform el.style.transform = `scale(${scale})`; @@ -59,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); } @@ -79,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; @@ -135,6 +154,15 @@ class EnvironmentController extends CanvasController{ break; } } + else if (this.middle_click) { + //drag on middle click + var cur_top = parseInt($('#env-canvas').css('top'), 10); + var cur_left = parseInt($('#env-canvas').css('left'), 10); + var new_top = cur_top + ((this.mouse_y - this.start_y)*this.scale); + var new_left = cur_left + ((this.mouse_x - this.start_x)*this.scale); + $('#env-canvas').css('top', new_top+'px'); + $('#env-canvas').css('left', new_left+'px'); + } } dropOrganism(organism, col, row) { diff --git a/src/Engine.js b/src/Engine.js index a74f693..ce32212 100644 --- a/src/Engine.js +++ b/src/Engine.js @@ -3,7 +3,9 @@ const ControlPanel = require('./Controllers/ControlPanel'); const OrganismEditor = require('./Environments/OrganismEditor'); const ColorScheme = require('./Rendering/ColorScheme'); -const render_speed = 60; +// If the simulation speed is below this value, a new interval will be created to handle ui rendering +// at a reasonable speed. If it is above, the simulation interval will be used to update the ui. +const min_render_speed = 60; class Engine { constructor(){ @@ -14,8 +16,13 @@ class Engine { this.colorscheme = new ColorScheme(this.env, this.organism_editor); this.colorscheme.loadColorScheme(); this.env.OriginOfLife(); - this.last_update = Date.now(); - this.delta_time = 0; + + this.sim_last_update = Date.now(); + this.sim_delta_time = 0; + + this.ui_last_update = Date.now(); + this.ui_delta_time = 0; + this.actual_fps = 0; this.running = false; } @@ -24,40 +31,57 @@ class Engine { if (fps <= 0) fps = 1; this.fps = fps; - this.game_loop = setInterval(function(){this.updateDeltaTime();this.environmentUpdate();}.bind(this), 1000/fps); + this.sim_loop = setInterval(()=>{ + this.updateSimDeltaTime(); + this.environmentUpdate(); + }, 1000/fps); this.running = true; - if (this.fps >= render_speed) { - if (this.render_loop != null) { - clearInterval(this.render_loop); - this.render_loop = null; + if (this.fps >= min_render_speed) { + if (this.ui_loop != null) { + clearInterval(this.ui_loop); + this.ui_loop = null; } } else - this.setRenderLoop(); + this.setUiLoop(); } stop() { - clearInterval(this.game_loop); + clearInterval(this.sim_loop); this.running = false; - this.setRenderLoop(); + this.setUiLoop(); } - setRenderLoop() { - if (this.render_loop == null) { - this.render_loop = setInterval(function(){this.updateDeltaTime();this.necessaryUpdate();}.bind(this), 1000/render_speed); + restart(fps) { + clearInterval(this.sim_loop); + this.start(fps); + } + + setUiLoop() { + if (!this.ui_loop) { + this.ui_loop = setInterval(()=> { + this.updateUIDeltaTime(); + this.necessaryUpdate(); + }, 1000/min_render_speed); } } - updateDeltaTime() { - this.delta_time = Date.now() - this.last_update; - this.last_update = Date.now(); + updateSimDeltaTime() { + this.sim_delta_time = Date.now() - this.sim_last_update; + this.sim_last_update = Date.now(); + if (!this.ui_loop) // if the ui loop isn't running, use the sim delta time + this.ui_delta_time = this.sim_delta_time; } + updateUIDeltaTime() { + this.ui_delta_time = Date.now() - this.ui_last_update; + this.ui_last_update = Date.now(); + } environmentUpdate() { - this.env.update(this.delta_time); - this.actual_fps = 1/this.delta_time*1000; - if(this.render_loop == null){ + this.actual_fps = (1000/this.sim_delta_time); + this.env.update(this.sim_delta_time); + if(this.ui_loop == null) { this.necessaryUpdate(); } @@ -65,7 +89,7 @@ class Engine { necessaryUpdate() { this.env.render(); - this.controlpanel.update(this.delta_time); + this.controlpanel.update(this.ui_delta_time); this.organism_editor.update(); } diff --git a/src/Environments/WorldEnvironment.js b/src/Environments/WorldEnvironment.js index 1470b0e..1b0c6e2 100644 --- a/src/Environments/WorldEnvironment.js +++ b/src/Environments/WorldEnvironment.js @@ -6,19 +6,19 @@ const CellStates = require('../Organism/Cell/CellStates'); const EnvironmentController = require('../Controllers/EnvironmentController'); const Hyperparams = require('../Hyperparameters.js'); const FossilRecord = require('../Stats/FossilRecord'); +const WorldConfig = require('../WorldConfig'); class WorldEnvironment extends Environment{ constructor(cell_size) { super(); this.renderer = new Renderer('env-canvas', 'env', cell_size); this.controller = new EnvironmentController(this, this.renderer.canvas); - var grid_rows = Math.ceil(this.renderer.height / cell_size); - var grid_cols = Math.ceil(this.renderer.width / cell_size); - this.grid_map = new GridMap(grid_cols, grid_rows, cell_size); + this.num_rows = Math.ceil(this.renderer.height / cell_size); + this.num_cols = Math.ceil(this.renderer.width / cell_size); + this.grid_map = new GridMap(this.num_cols, this.num_rows, cell_size); this.organisms = []; this.walls = []; this.total_mutability = 0; - this.auto_reset = true; this.largest_cell_count = 0; this.reset_count = 0; this.total_ticks = 0; @@ -45,7 +45,7 @@ class WorldEnvironment extends Environment{ } render() { - if (Hyperparams.headless) { + if (WorldConfig.headless) { this.renderer.cells_to_render.clear(); return; } @@ -58,24 +58,37 @@ class WorldEnvironment extends Environment{ } removeOrganisms(org_indeces) { + let start_pop = this.organisms.length; for (var i of org_indeces.reverse()){ this.total_mutability -= this.organisms[i].mutability; this.organisms.splice(i, 1); } - if (this.organisms.length == 0 && this.auto_reset){ - this.reset_count++; - this.reset(); + if (this.organisms.length === 0 && start_pop > 0) { + if (WorldConfig.auto_pause) + $('.pause-button')[0].click(); + else if(WorldConfig.auto_reset) { + this.reset_count++; + this.reset(false); + } } } OriginOfLife() { var center = this.grid_map.getCenter(); - var org = new Organism(center[0], center[1], this); - 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); + switch (WorldConfig.start_state){ + case 'simple-prod': + var org = new Organism(center[0], center[1], this); + 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); + break; + case 'random-orgs': + break; + case 'no-orgs': + break; + } } addOrganism(organism) { @@ -104,7 +117,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); } } @@ -130,18 +144,18 @@ class WorldEnvironment extends Environment{ } } - reset() { - this.clear(); - this.OriginOfLife(); - } + reset(confirm_reset=true, reset_life=true) { + if (confirm_reset && !confirm('The current environment will be lost. Proceed?')) + return; - clear() { 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(); } resizeGridColRow(cell_size, cols, rows) { @@ -153,9 +167,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 d7c34ed..9579f11 100644 --- a/src/Hyperparameters.js +++ b/src/Hyperparameters.js @@ -2,11 +2,8 @@ const Neighbors = require("./Grid/Neighbors"); const Hyperparams = { setDefaults: function() { - this.headless = false; - this.lifespanMultiplier = 100; - this.foodProdProb = 4; - this.foodProdProbScalar = 4; + this.foodProdProb = 5; this.killableNeighbors = Neighbors.adjacent; this.edibleNeighbors = Neighbors.adjacent; this.growableNeighbors = Neighbors.adjacent; @@ -17,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; @@ -28,23 +24,13 @@ const Hyperparams = { this.lookRange = 20; this.foodDropProb = 0; + + this.extraMoverFoodCost = 0; }, - balanceMutationProbs : function(choice) { - if (choice == 1) { - var remaining = 100 - this.addProb; - this.changeProb = remaining/2; - this.removeProb = remaining/2; - } - else if (choice == 2) { - var remaining = 100 - this.changeProb; - this.addProb = remaining/2; - this.removeProb = remaining/2; - } - else { - var remaining = 100 - this.removeProb; - this.changeProb = remaining/2; - this.addProb = remaining/2; + loadJsonObj(obj) { + for (let key in obj) { + this[key] = obj[key]; } } } 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 7046a59..a1db6b6 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,47 +117,42 @@ class Organism { org.species.addPop(); } } - this.food_collected -= this.foodNeeded(); + Math.max(this.food_collected -= this.foodNeeded(), 0); } mutate() { - var choice = Math.floor(Math.random() * 100); - var mutated = false; - if (choice <= Hyperparams.addProb) { - // add cell - // console.log("add cell") - - var branch = this.anatomy.getRandomCell(); - var state = CellStates.getRandomLivingType();//branch.state; - var growth_direction = Neighbors.all[Math.floor(Math.random() * Neighbors.all.length)] - var c = branch.loc_col+growth_direction[0]; - var r = branch.loc_row+growth_direction[1]; + let mutated = false; + if (this.calcRandomChance(Hyperparams.addProb)) { + let branch = this.anatomy.getRandomCell(); + let state = CellStates.getRandomLivingType();//branch.state; + let growth_direction = Neighbors.all[Math.floor(Math.random() * Neighbors.all.length)] + let c = branch.loc_col+growth_direction[0]; + let r = branch.loc_row+growth_direction[1]; if (this.anatomy.canAddCellAt(c, r)){ mutated = true; this.anatomy.addRandomizedCell(state, c, r); } } - else if (choice <= Hyperparams.addProb + Hyperparams.changeProb){ - // change cell - var cell = this.anatomy.getRandomCell(); - var state = CellStates.getRandomLivingType(); - // console.log("change cell", state) + if (this.calcRandomChance(Hyperparams.changeProb)){ + let cell = this.anatomy.getRandomCell(); + let state = CellStates.getRandomLivingType(); this.anatomy.replaceCell(state, cell.loc_col, cell.loc_row); mutated = true; } - else if (choice <= Hyperparams.addProb + Hyperparams.changeProb + Hyperparams.removeProb){ - // remove cell - // console.log("remove cell") - + if (this.calcRandomChance(Hyperparams.removeProb)){ if(this.anatomy.cells.length > 1) { - var cell = this.anatomy.getRandomCell(); + let cell = this.anatomy.getRandomCell(); mutated = this.anatomy.removeCell(cell.loc_col, cell.loc_row); } } return mutated; } + calcRandomChance(prob) { + return (Math.random() * 100) < prob; + } + attemptMove() { var direction = Directions.scalars[this.direction]; var direction_c = direction[0]; @@ -248,8 +242,7 @@ class Organism { 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/Rendering/Renderer.js b/src/Rendering/Renderer.js index 647a9a8..41d04c5 100644 --- a/src/Rendering/Renderer.js +++ b/src/Rendering/Renderer.js @@ -105,8 +105,4 @@ class Renderer { } } -// $("body").mousemove(function(e) { -// console.log("hello"); -// }); - module.exports = Renderer; diff --git a/src/Stats/FossilRecord.js b/src/Stats/FossilRecord.js index 16e58dd..c33b822 100644 --- a/src/Stats/FossilRecord.js +++ b/src/Stats/FossilRecord.js @@ -41,7 +41,9 @@ const FossilRecord = { if (species.cumulative_pop < this.min_pop) { return false; } - this.extinct_species.push(s); + // disabled for now, causes memory problems on long runs + // this.extinct_species.push(s); + // console.log("Extant species:", this.extant_species.length) // console.log("Extinct species:", this.extinct_species.length) return true; diff --git a/src/Stats/Species.js b/src/Stats/Species.js index 37daadd..f251844 100644 --- a/src/Stats/Species.js +++ b/src/Stats/Species.js @@ -3,17 +3,11 @@ const CellStates = require("../Organism/Cell/CellStates"); class Species { constructor(anatomy, ancestor, start_tick) { this.anatomy = anatomy; - this.ancestor = ancestor; + // this.ancestor = ancestor; // garbage collect ancestors to avoid memory problems 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(); 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..1923453 --- /dev/null +++ b/src/WorldConfig.js @@ -0,0 +1,9 @@ +const WorldConfig = { + headless: false, + clear_walls_on_reset: false, + start_state: 'simple-prod', + auto_reset: true, + auto_pause: false, +} + +module.exports = WorldConfig; \ No newline at end of file