From 8df3accff7660e149871355dd2209ada73a4d559 Mon Sep 17 00:00:00 2001 From: MaxRobinsonTheGreat Date: Fri, 15 Apr 2022 13:13:19 -0500 Subject: [PATCH] added world saving/loading --- dist/index.html | 13 +++++- src/Controllers/ControlPanel.js | 39 ++++++++++++++++ src/Environments/WorldEnvironment.js | 56 +++++++++++++++++++++++ src/Grid/GridMap.js | 28 ++++++++++++ src/Organism/Cell/BodyCells/KillerCell.js | 1 - src/Organism/Organism.js | 13 ++---- src/Stats/FossilRecord.js | 48 ++++++++++++++----- src/Stats/Species.js | 3 +- src/Utils/SerializeHelper.js | 8 ++++ 9 files changed, 184 insertions(+), 25 deletions(-) diff --git a/dist/index.html b/dist/index.html index 70f160b..8e89ab6 100644 --- a/dist/index.html +++ b/dist/index.html @@ -20,7 +20,7 @@
- +
@@ -189,6 +189,15 @@
+
+ + + .json +
+ + + +
@@ -270,7 +279,7 @@
- + diff --git a/src/Controllers/ControlPanel.js b/src/Controllers/ControlPanel.js index 11e81eb..46cda00 100644 --- a/src/Controllers/ControlPanel.js +++ b/src/Controllers/ControlPanel.js @@ -47,6 +47,8 @@ class ControlPanel { defineHotkeys() { $('body').keydown( (e) => { + let focused = document.activeElement; + if (focused.tagName === "INPUT" && focused.type === "text") return; switch (e.key.toLowerCase()) { // hot bar controls case 'a': @@ -205,6 +207,43 @@ class ControlPanel { this.env_controller.add_new_species = true; this.env_controller.dropOrganism(org, center[0], center[1]) }); + $('#save-env').click( () => { + let was_running = this.engine.running; + this.setPaused(true); + let env = this.engine.env.serialize(); + let data = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(env)); + let downloadEl = document.getElementById('download-el'); + downloadEl.setAttribute("href", data); + downloadEl.setAttribute("download", $('#save-env-name').val()+".json"); + downloadEl.click(); + if (was_running) + this.setPaused(false); + }); + $('#load-env').click(() => { + $('#upload-env').click(); + }); + $('#upload-env').change((e)=>{ + let files = e.target.files; + if (!files.length) {return;}; + let reader = new FileReader(); + reader.onload = (e) => { + try { + let was_running = this.engine.running; + this.setPaused(true); + let env = JSON.parse(e.target.result); + this.engine.env.loadRaw(env); + if (was_running) + this.setPaused(false); + this.updateHyperparamUIValues(); + this.env_controller.resetView(); + } catch(except) { + console.error(except) + alert('Failed to load world'); + } + $('#upload-env')[0].value = ''; + }; + reader.readAsText(files[0]); + }); } defineHyperparameterControls() { diff --git a/src/Environments/WorldEnvironment.js b/src/Environments/WorldEnvironment.js index 6f47b27..8ad7276 100644 --- a/src/Environments/WorldEnvironment.js +++ b/src/Environments/WorldEnvironment.js @@ -7,6 +7,8 @@ const EnvironmentController = require('../Controllers/EnvironmentController'); const Hyperparams = require('../Hyperparameters.js'); const FossilRecord = require('../Stats/FossilRecord'); const WorldConfig = require('../WorldConfig'); +const SerializeHelper = require('../Utils/SerializeHelper'); +const Species = require('../Stats/Species'); class WorldEnvironment extends Environment{ constructor(cell_size) { @@ -164,6 +166,60 @@ class WorldEnvironment extends Environment{ this.num_rows = Math.ceil(this.renderer.height / cell_size); this.grid_map.resize(this.num_cols, this.num_rows, cell_size); } + + serialize() { + let env = SerializeHelper.copyNonObjects(this); + env.grid = this.grid_map.serialize(); + env.organisms = []; + for (let org of this.organisms){ + env.organisms.push(org.serialize()); + } + env.fossil_record = FossilRecord.serialize(); + env.controls = Hyperparams; + return env; + } + + loadRaw(env) { // species name->stats map, evolution controls, + this.organisms = []; + FossilRecord.clear_record(); + this.resizeGridColRow(this.grid_map.cell_size, env.grid.cols, env.grid.rows) + this.grid_map.loadRaw(env.grid); + + // create species map + let species = {}; + for (let name in env.fossil_record.species) { + let s = new Species(null, null, 0); + SerializeHelper.overwriteNonObjects(env.fossil_record.species[name], s) + species[name] = s; // the species needs an anatomy obj still + } + + for (let orgRaw of env.organisms) { + let org = new Organism(orgRaw.col, orgRaw.row, this); + if (!orgRaw.anatomy) console.log(orgRaw) + org.loadRaw(orgRaw); + this.addOrganism(org); + let s = species[orgRaw.species_name]; + if (!s){ + console.log("Created:", orgRaw.species_name) + console.log(org.anatomy) + s = new Species(org.anatomy, null, env.total_ticks); + species[orgRaw.species_name] = s; + } + if (!s.anatomy) { + //if the species doesn't have anatomy we need to initialize it + s.anatomy = org.anatomy; + s.calcAnatomyDetails(); + } + org.species = s; + } + for (let name in species) + FossilRecord.addSpeciesObj(species[name]); + FossilRecord.loadRaw(env.fossil_record); + SerializeHelper.overwriteNonObjects(env, this); + if ($('#override-controls').is(':checked')) + Hyperparams.loadJsonObj(env.controls) + this.renderer.renderFullGrid(this.grid_map.grid); + } } module.exports = WorldEnvironment; diff --git a/src/Grid/GridMap.js b/src/Grid/GridMap.js index 84db4ca..6c69773 100644 --- a/src/Grid/GridMap.js +++ b/src/Grid/GridMap.js @@ -78,6 +78,34 @@ class GridMap { r = 0; return [c, r]; } + + serialize() { + // Rather than store every single cell, we will store non organism cells (food+walls) + // and assume everything else is empty. Organism cells will be set when the organism + // list is loaded. This reduces filesize and complexity. + let grid = {cols:this.cols, rows:this.rows}; + grid.food = []; + grid.walls = []; + for (let col of this.grid) { + for (let cell of col) { + if (cell.state===CellStates.wall || cell.state===CellStates.food){ + let c = {c: cell.col, r: cell.row}; // no need to store state + if (cell.state===CellStates.food) + grid.food.push(c) + else + grid.walls.push(c) + } + } + } + return grid; + } + + loadRaw(grid) { + for (let f of grid.food) + this.setCellType(f.c, f.r, CellStates.food); + for (let w of grid.walls) + this.setCellType(w.c, w.r, CellStates.wall); + } } module.exports = GridMap; diff --git a/src/Organism/Cell/BodyCells/KillerCell.js b/src/Organism/Cell/BodyCells/KillerCell.js index ca82218..c7dcd92 100644 --- a/src/Organism/Cell/BodyCells/KillerCell.js +++ b/src/Organism/Cell/BodyCells/KillerCell.js @@ -18,7 +18,6 @@ class KillerCell extends BodyCell{ } killNeighbor(n_cell) { - // console.log(n_cell) if(n_cell == null || n_cell.owner == null || n_cell.owner == this.org || !n_cell.owner.living || n_cell.state == CellStates.armor) return; var is_hit = n_cell.state == CellStates.killer; // has to be calculated before death diff --git a/src/Organism/Organism.js b/src/Organism/Organism.js index 0461f27..80f8404 100644 --- a/src/Organism/Organism.js +++ b/src/Organism/Organism.js @@ -34,7 +34,6 @@ class Organism { this.move_range = parent.move_range; this.mutability = parent.mutability; this.species = parent.species; - // this.birth_distance = parent.birth_distance; for (var c of parent.anatomy.cells){ //deep copy parent cells this.anatomy.addInheritCell(c); @@ -321,21 +320,17 @@ class Organism { serialize() { let org = SerializeHelper.copyNonObjects(this); org.anatomy = this.anatomy.serialize(); - if (this.brain) + if (this.anatomy.is_mover && this.anatomy.has_eyes) org.brain = this.brain.serialize(); + org.species_name = this.species.name; return org; } loadRaw(org) { - for (let key in org) - if (typeof org[key] !== 'object') - this[key] = org[key]; + SerializeHelper.overwriteNonObjects(org, this); this.anatomy.loadRaw(org.anatomy) - console.log(org) - if (org.brain) { - console.log('load brain') + if (org.brain) this.brain.copy(org.brain) - } } } diff --git a/src/Stats/FossilRecord.js b/src/Stats/FossilRecord.js index e9007fa..17ec347 100644 --- a/src/Stats/FossilRecord.js +++ b/src/Stats/FossilRecord.js @@ -1,4 +1,5 @@ const CellStates = require("../Organism/Cell/CellStates"); +const SerializeHelper = require("../Utils/SerializeHelper"); const Species = require("./Species"); const FossilRecord = { @@ -30,7 +31,6 @@ const FossilRecord = { }, fossilize: function(species) { - // console.log("Extinction") species.end_tick = this.env.total_ticks; for (i in this.extant_species) { var s = this.extant_species[i]; @@ -44,16 +44,12 @@ const FossilRecord = { } // 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; } } }, resurrect: function(species) { - // console.log("Resurrecting species") if (species.extinct) { for (i in this.extinct_species) { var s = this.extinct_species[i]; @@ -68,12 +64,13 @@ const FossilRecord = { setData() { // all parallel arrays - this.tick_record = [0]; - this.pop_counts = [0]; - this.species_counts = [0]; - this.av_mut_rates = [0]; - this.av_cells = [0]; - this.av_cell_counts = [this.calcCellCountAverages()]; + this.tick_record = []; + this.pop_counts = []; + this.species_counts = []; + this.av_mut_rates = []; + this.av_cells = []; + this.av_cell_counts = []; + this.updateData(); }, updateData() { @@ -122,12 +119,39 @@ const FossilRecord = { this.av_cell_counts.push(cell_counts); }, - clear_record: function() { + clear_record() { this.extant_species = []; this.extinct_species = []; this.setData(); }, + serialize() { + this.updateData(); + let record = SerializeHelper.copyNonObjects(this); + record.records = { + tick_record:this.tick_record, + pop_counts:this.pop_counts, + species_counts:this.species_counts, + av_mut_rates:this.av_mut_rates, + av_cells:this.av_cells, + av_cell_counts:this.av_cell_counts, + }; + let species = {}; + for (let s of this.extant_species) { + species[s.name] = SerializeHelper.copyNonObjects(s); + delete species[s.name].name; // the name will be used as the key, so remove it from the value + } + record.species = species; + return record; + }, + + loadRaw(record) { + SerializeHelper.overwriteNonObjects(record, this); + for (let key in record.records) { + this[key] = record.records[key]; + } + } + } FossilRecord.init(); diff --git a/src/Stats/Species.js b/src/Stats/Species.js index 050e70b..6f5b5f9 100644 --- a/src/Stats/Species.js +++ b/src/Stats/Species.js @@ -14,12 +14,13 @@ class Species { this.cumulative_pop = 1; this.start_tick = start_tick; this.end_tick = -1; - this.name = '_' + Math.random().toString(36).substr(2, 9); + this.name = Math.random().toString(36).substr(2, 10); this.extinct = false; this.calcAnatomyDetails(); } calcAnatomyDetails() { + if (!this.anatomy) return; var cell_counts = {}; for (let c of CellStates.living) { cell_counts[c.name] = 0; diff --git a/src/Utils/SerializeHelper.js b/src/Utils/SerializeHelper.js index c96c8c2..62930a9 100644 --- a/src/Utils/SerializeHelper.js +++ b/src/Utils/SerializeHelper.js @@ -6,6 +6,14 @@ const SerializeHelper = { newobj[key] = obj[key]; } return newobj; + }, + overwriteNonObjects(copyFrom, copyTo) { + for (let key in copyFrom) { + if (typeof copyFrom[key] !== 'object' && typeof copyTo[key] !== 'object') { + // only overwrite if neither are objects + copyTo[key] = copyFrom[key]; + } + } } }