diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..9615d8a --- /dev/null +++ b/Changelog.md @@ -0,0 +1,21 @@ +# Changelog + +## Current Dev (1.0.1) +Changes that are in the develop branch + +### Bug Fixes: +- Infinite FPS: [#42](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/45) +- Spelling Fix: [#31](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/31) + +### UI Enhancements: +- Hotkeys/improved zoom controls: [#47](https://github.com/MaxRobinsonTheGreat/LifeEngine/pull/47) +- 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) + +### Simulation Enhancements: +- + +Thanks to contributors: @TrevorSayre @EvaisaGiac @Chrispykins + +## 1.0.0 +Initial release. diff --git a/dist/css/style.css b/dist/css/style.css index f98f464..1714871 100644 --- a/dist/css/style.css +++ b/dist/css/style.css @@ -79,6 +79,9 @@ button { button:hover{ background-color: #81d2c7; } +button:active{ + background-color: #595e77; +} .icon-links { font-size: 35px; @@ -163,10 +166,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; } #clear-walls { diff --git a/dist/index.html b/dist/index.html index 6748fe2..0166f53 100644 --- a/dist/index.html +++ b/dist/index.html @@ -19,17 +19,17 @@
- - - - - - + + + + + +

Simulation Speed

- - + +

Target FPS: 60

@@ -62,7 +62,7 @@

Simulation Controls

Stats

Challenges

- +
@@ -98,9 +98,9 @@
- - - + + +
@@ -143,6 +143,10 @@
+
+ + +

Brain

@@ -261,19 +265,19 @@
- - - - - - - + + + + + + +
- +
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 ab5eaa5..5ec9e54 100644 --- a/src/Controllers/ControlPanel.js +++ b/src/Controllers/ControlPanel.js @@ -6,6 +6,7 @@ class ControlPanel { constructor(engine) { this.engine = engine; this.defineMinMaxControls(); + this.defineHotkeys(); this.defineEngineSpeedControls(); this.defineGridSizeControls(); this.defineTabNavigation(); @@ -41,38 +42,72 @@ 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(){ @@ -143,9 +178,12 @@ class ControlPanel { $(tab).css('display', 'grid'); self.engine.organism_editor.is_active = (this.id == 'editor'); self.stats_panel.stopAutoRender(); - if (this.id == 'stats') { + if (this.id === 'stats') { self.stats_panel.startAutoRender(); } + else if (this.id === 'editor') { + self.editor_controller.refreshDetailsPanel(); + } self.tab_id = this.id; }); } @@ -192,15 +230,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)); @@ -261,20 +296,15 @@ class ControlPanel { break; case "edit": self.setMode(Modes.Edit); - self.editor_controller.setEditorPanel(); break; 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(){ @@ -310,6 +340,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) { @@ -319,8 +360,7 @@ class ControlPanel { } changeEngineSpeed(change_val) { - this.engine.stop(); - this.engine.start(change_val) + this.engine.restart(change_val) this.fps = this.engine.fps; } diff --git a/src/Controllers/EditorController.js b/src/Controllers/EditorController.js index e7c2152..f0c101e 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(); @@ -110,6 +114,13 @@ class EditorController extends CanvasController{ $('#edit-organism-details').css('display', 'none'); } + refreshDetailsPanel() { + if (this.mode === Modes.Edit) + this.setEditorPanel(); + else + this.setDetailsPanel(); + } + setDetailsPanel() { this.clearDetailsPanel(); var org = this.env.organism; @@ -117,7 +128,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 { @@ -141,6 +153,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 451d2a9..73aa0fc 100644 --- a/src/Controllers/EnvironmentController.js +++ b/src/Controllers/EnvironmentController.js @@ -28,22 +28,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})`; @@ -149,6 +141,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'); + } } dropCellType(col, row, state, killBlocking=false) { 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/Hyperparameters.js b/src/Hyperparameters.js index d7c34ed..64073f8 100644 --- a/src/Hyperparameters.js +++ b/src/Hyperparameters.js @@ -29,24 +29,6 @@ const Hyperparams = { this.foodDropProb = 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; - } - } } Hyperparams.setDefaults(); diff --git a/src/Organism/Organism.js b/src/Organism/Organism.js index 7046a59..672c302 100644 --- a/src/Organism/Organism.js +++ b/src/Organism/Organism.js @@ -123,42 +123,37 @@ class Organism { } 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]; 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;