diff --git a/src/Controllers/ControlPanel.js b/src/Controllers/ControlPanel.js index 8ba733a9..5a6e498d 100644 --- a/src/Controllers/ControlPanel.js +++ b/src/Controllers/ControlPanel.js @@ -120,7 +120,14 @@ class ControlPanel { defineEngineSpeedControls(){ this.slider = document.getElementById("slider"); - this.slider.oninput = function() { + function debounce(func, wait=100) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + } + this.slider.oninput = debounce(function() { const max_fps = 300; this.fps = parseInt(this.slider.value); if (this.fps>=max_fps) this.fps = 1000; @@ -129,7 +136,7 @@ class ControlPanel { } let text = this.fps >= max_fps ? 'MAX' : this.fps; $('#fps').text("Target FPS: "+text); - }.bind(this); + }.bind(this)); $('.pause-button').click(function() { // toggle pause diff --git a/src/Environments/WorldEnvironment.js b/src/Environments/WorldEnvironment.js index afe3f7f0..3b2d303c 100644 --- a/src/Environments/WorldEnvironment.js +++ b/src/Environments/WorldEnvironment.js @@ -26,21 +26,22 @@ class WorldEnvironment extends Environment{ this.total_ticks = 0; this.data_update_rate = 100; FossilRecord.setEnv(this); + this.spatialGrid = new Map(); + this.gridSize = 20; // Adjust based on typical organism size } update() { - var to_remove = []; - for (var i in this.organisms) { - var org = this.organisms[i]; + // Iterate backwards to safely remove elements + for (let i = this.organisms.length - 1; i >= 0; i--) { + const org = this.organisms[i]; if (!org.living || !org.update()) { - to_remove.push(i); + this.removeOrganisms([i]); } } - this.removeOrganisms(to_remove); if (Hyperparams.foodDropProb > 0) { this.generateFood(); } - this.total_ticks ++; + this.total_ticks++; if (this.total_ticks % this.data_update_rate == 0) { FossilRecord.updateData(); } @@ -60,11 +61,32 @@ 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; + const start_pop = this.organisms.length; + // Sort indices in descending order to avoid shifting issues + const sortedIndices = [...org_indeces].sort((a, b) => b - a); + let removedMutability = 0; + + sortedIndices.forEach(i => { + const org = this.organisms[i]; + removedMutability += org.mutability; + + // Add cleanup for all adjacent grid cells + for(let dc = -1; dc <= 1; dc++) { + for(let dr = -1; dr <= 1; dr++) { + const key = `${Math.floor((org.c+dc)/this.gridSize)},${Math.floor((org.r+dr)/this.gridSize)}`; + if(this.spatialGrid.has(key)) { + this.spatialGrid.get(key).delete(org); + } + } + } + this.organisms.splice(i, 1); - } + }); + + this.total_mutability -= removedMutability; + if (this.total_mutability < 0) this.total_mutability = 0; + + // Add back population check if (this.organisms.length === 0 && start_pop > 0) { if (WorldConfig.auto_pause) $('.pause-button')[0].click(); @@ -91,6 +113,7 @@ class WorldEnvironment extends Environment{ this.organisms.push(organism); if (organism.anatomy.cells.length > this.largest_cell_count) this.largest_cell_count = organism.anatomy.cells.length; + this.updateSpatialGrid(organism); } canAddOrganism() { @@ -108,9 +131,10 @@ class WorldEnvironment extends Environment{ changeCell(c, r, state, owner) { super.changeCell(c, r, state, owner); - this.renderer.addToRender(this.grid_map.cellAt(c, r)); + const cell = this.grid_map.cellAt(c, r); + this.renderer.addToRender(cell); if(state == CellStates.wall) - this.walls.push(this.grid_map.cellAt(c, r)); + this.walls.push(cell); } clearWalls() { @@ -237,6 +261,17 @@ class WorldEnvironment extends Environment{ Hyperparams.loadJsonObj(env.controls) this.renderer.renderFullGrid(this.grid_map.grid); } + + updateSpatialGrid(org) { + const key = `${Math.floor(org.c/this.gridSize)},${Math.floor(org.r/this.gridSize)}`; + if(!this.spatialGrid.has(key)) this.spatialGrid.set(key, new Set()); + this.spatialGrid.get(key).add(org); + } + + getNearbyOrganisms(c, r) { + const key = `${Math.floor(c/this.gridSize)},${Math.floor(r/this.gridSize)}`; + return this.spatialGrid.get(key) || new Set(); + } } module.exports = WorldEnvironment; diff --git a/src/Grid/GridMap.js b/src/Grid/GridMap.js index 54478963..e2f675c6 100644 --- a/src/Grid/GridMap.js +++ b/src/Grid/GridMap.js @@ -7,54 +7,47 @@ class GridMap { } resize(cols, rows, cell_size) { - this.grid = []; + this.grid = new Array(cols * rows); // 1D array this.cols = cols; this.rows = rows; this.cell_size = cell_size; - for(var c=0; c= this.cols || row >= this.rows) return null; + return this.grid[col * this.rows + row]; } setCellType(col, row, state) { if (!this.isValidLoc(col, row)) { return; } - this.grid[col][row].setType(state); + this.grid[col * this.rows + row].setType(state); } setCellOwner(col, row, cell_owner) { if (!this.isValidLoc(col, row)) { return; } - this.grid[col][row].cell_owner = cell_owner; + this.grid[col * this.rows + row].cell_owner = cell_owner; if (cell_owner != null) - this.grid[col][row].owner = cell_owner.org; + this.grid[col * this.rows + row].owner = cell_owner.org; else - this.grid[col][row].owner = null; + this.grid[col * this.rows + row].owner = null; } isValidLoc(col, row){ @@ -80,27 +73,28 @@ class GridMap { } 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 = {cell_size:this.cell_size, 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) - } + for (const cell of this.grid) { + if (cell.state === CellStates.wall || cell.state === CellStates.food) { + const c = {c: cell.col, r: cell.row}; + cell.state === CellStates.food ? grid.food.push(c) : grid.walls.push(c); } } return grid; } loadRaw(grid) { + // Handle both 1D and legacy 2D formats + if(grid.grid) { // Legacy 2D format + grid.food = grid.grid.flatMap(col => + col.filter(c => c.state === 'food').map(c => ({c: c.col, r: c.row})) + ); + grid.walls = grid.grid.flatMap(col => + col.filter(c => c.state === 'wall').map(c => ({c: c.col, r: c.row})) + ); + } for (let f of grid.food) this.setCellType(f.c, f.r, CellStates.food); for (let w of grid.walls) diff --git a/src/Organism/Organism.js b/src/Organism/Organism.js index b3178c62..33754a61 100644 --- a/src/Organism/Organism.js +++ b/src/Organism/Organism.js @@ -355,6 +355,30 @@ class Organism { this.brain.copy(org.brain) } + move(col, row) { + // Update spatial grid before changing position + const oldKey = `${Math.floor(this.c/this.env.gridSize)},${Math.floor(this.r/this.env.gridSize)}`; + if(this.env.spatialGrid.has(oldKey)) { + this.env.spatialGrid.get(oldKey).delete(this); + } + + this.c = col; + this.r = row; + + // Update spatial grid with new position + const newKey = `${Math.floor(col/this.env.gridSize)},${Math.floor(row/this.env.gridSize)}`; + if(!this.env.spatialGrid.has(newKey)) { + this.env.spatialGrid.set(newKey, new Set()); + } + this.env.spatialGrid.get(newKey).add(this); + + this.updateGrid(); + } + + setColRow(col, row) { + this.move(col, row); + } + } module.exports = Organism; diff --git a/src/Organism/Perception/Brain.js b/src/Organism/Perception/Brain.js index 4c60b031..2ae5822b 100644 --- a/src/Organism/Perception/Brain.js +++ b/src/Organism/Perception/Brain.js @@ -52,19 +52,30 @@ class Brain { } decide() { + if(this.observations.length === 0) return false; + + // Find closest observation in O(n) time + let closest = this.observations[0]; + for(const obs of this.observations) { + if(obs.distance < closest.distance) { + closest = obs; + } + } + var decision = Decision.neutral; - var closest = Hyperparams.lookRange + 1; var move_direction = 0; - for (var obs of this.observations) { - if (obs.cell == null || obs.cell.owner == this.owner) { - continue; - } - if (obs.distance < closest) { - decision = this.decisions[obs.cell.state.name]; - move_direction = obs.direction; - closest = obs.distance; - } + if (closest.cell && closest.cell.owner === this.owner) { + decision = this.decisions[closest.cell.state.name]; + move_direction = closest.direction; + } else if (closest.cell) { + decision = this.decisions[closest.cell.state.name]; + move_direction = closest.direction; + } + // If cell is null (edge of map), use empty cell decision + else { + decision = this.decisions[CellStates.empty.name]; } + this.observations = []; if (decision == Decision.chase) { this.owner.changeDirection(move_direction); diff --git a/src/Rendering/Renderer.js b/src/Rendering/Renderer.js index 41d04c58..5f822a3c 100644 --- a/src/Rendering/Renderer.js +++ b/src/Rendering/Renderer.js @@ -14,6 +14,7 @@ class Renderer { this.cells_to_render = new Set(); this.cells_to_highlight = new Set(); this.highlighted_cells = new Set(); + this.dirtyCells = new Set(); } fillWindow(container_id) { @@ -33,18 +34,17 @@ class Renderer { } renderFullGrid(grid) { - for (var col of grid) { - for (var cell of col){ - this.renderCell(cell); - } + for (const cell of grid) { + this.renderCell(cell); } } renderCells() { - for (var cell of this.cells_to_render) { - this.renderCell(cell); + this.ctx.beginPath(); + for(const cell of this.dirtyCells) { + cell.state.render(this.ctx, cell, this.cell_size); } - this.cells_to_render.clear(); + this.dirtyCells.clear(); } renderCell(cell) { @@ -52,17 +52,14 @@ class Renderer { } renderOrganism(org) { - for(var org_cell of org.anatomy.cells) { - var cell = org.getRealCell(org_cell); - this.renderCell(cell); - } + org.anatomy.cells.forEach(org_cell => { + const cell = org.getRealCell(org_cell); + if(cell) this.dirtyCells.add(cell); + }); } addToRender(cell) { - if (this.highlighted_cells.has(cell)){ - this.cells_to_highlight.add(cell); - } - this.cells_to_render.add(cell); + this.dirtyCells.add(cell); } renderHighlights() { diff --git a/src/Utils/Perlin.js b/src/Utils/Perlin.js index ab9b5ca8..cc3e7214 100644 --- a/src/Utils/Perlin.js +++ b/src/Utils/Perlin.js @@ -21,12 +21,12 @@ let perlin = { return a + this.smootherstep(x) * (b-a); }, seed: function(){ - this.gradients = {}; - this.memory = {}; + this.gradients = new Map(); + this.memory = new Map(); }, get: function(x, y) { - if (this.memory.hasOwnProperty([x,y])) - return this.memory[[x,y]]; + const key = `${x.toFixed(2)}|${y.toFixed(2)}`; + if(this.memory.has(key)) return this.memory.get(key); let xf = Math.floor(x); let yf = Math.floor(y); //interpolate @@ -37,7 +37,7 @@ let perlin = { 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; + this.memory.set(key, v); return v; } }