Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Controllers/ControlPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
59 changes: 47 additions & 12 deletions src/Environments/WorldEnvironment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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();
Expand All @@ -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() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down
64 changes: 29 additions & 35 deletions src/Grid/GridMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<cols; c++) {
var row = [];
for(var r=0; r<rows; r++) {
var cell = new Cell(CellStates.empty, c, r, c*cell_size, r*cell_size);
row.push(cell);
}
this.grid.push(row);
for(let i=0; i<cols*rows; i++) {
let c = Math.floor(i/rows);
let r = i%rows;
this.grid[i] = new Cell(CellStates.empty, c, r, c*cell_size, r*cell_size);
}
}

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;
}
for (const cell of this.grid) {
if (ignore_walls && cell.state === CellStates.wall) continue;
cell.setType(state);
cell.owner = null;
cell.cell_owner = null;
}
}

cellAt(col, row) {
if (!this.isValidLoc(col, row)) {
return null;
}
return this.grid[col][row];
if(col < 0 || row < 0 || col >= 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){
Expand All @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions src/Organism/Organism.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
31 changes: 21 additions & 10 deletions src/Organism/Perception/Brain.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
27 changes: 12 additions & 15 deletions src/Rendering/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -33,36 +34,32 @@ 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) {
cell.state.render(this.ctx, cell, this.cell_size);
}

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() {
Expand Down
10 changes: 5 additions & 5 deletions src/Utils/Perlin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
Expand Down