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
26 changes: 26 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
<h1>Language Evolution Simulation</h1>
<p>with <a href="https://www.wikiwand.com/en/Agent-based_model">Agent Based Models</a>
- <a href="http://github.com/fatiherikli/language-evolution-simulation">Source code on github</a></p>
<div class="controls">
<button id="pause-btn">Pause</button>
<button id="reset-btn">Reset</button>
<label>Speed: <input type="range" id="speed-slider" min="50" max="1000" value="300" step="50"><span id="speed-value">300</span>ms</label>
</div>
<svg id="canvas" xmlns="http://www.w3.org/2000/svg" width="510" height="420"></svg>
<div class="info">
<div id="stats"></div>
Expand Down Expand Up @@ -58,6 +63,27 @@ <h4>Most used word</h4>
);

simulation.run();

// Wire up controls
var pauseBtn = document.getElementById('pause-btn');
var resetBtn = document.getElementById('reset-btn');
var speedSlider = document.getElementById('speed-slider');
var speedValue = document.getElementById('speed-value');

pauseBtn.addEventListener('click', function () {
var paused = simulation.togglePause();
pauseBtn.textContent = paused ? 'Resume' : 'Pause';
});

resetBtn.addEventListener('click', function () {
simulation.reset();
});

speedSlider.addEventListener('input', function () {
var value = parseInt(speedSlider.value, 10);
speedValue.textContent = value;
simulation.setSpeed(value);
});
</script>

<script>
Expand Down
12 changes: 7 additions & 5 deletions js/Agent.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
var __agentIdCounter__ = 0;

function Agent(island, model, eventLog, counter) {
this.island = island;
this.position = null;
this.model = model;
this.id = Math.random();
this.id = 'agent_' + (++__agentIdCounter__) + '_' + Date.now().toString(36);
this.state = Agent.LIVE;
this.inbox = null;
this.eventLog = eventLog;
Expand Down Expand Up @@ -69,17 +71,17 @@ Agent.prototype.step = function () {
return (
neighborhoodCells.filter(function (neighborhood) {
return (
neighborhood[0] == cell[0] &&
neighborhood[1] == cell[1]
neighborhood[0] === cell[0] &&
neighborhood[1] === cell[1]
);
}).length > 0
);
}).filter(function (cell) {
return (
neighborhoodAgents.filter(function (agent) {
return (
agent.position[0] == cell[0] &&
agent.position[1] == cell[1]
agent.position[0] === cell[0] &&
agent.position[1] === cell[1]
);
}).length === 0
);
Expand Down
14 changes: 14 additions & 0 deletions js/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,19 @@ Model.prototype.step = function () {
this.agents.forEach(function (agent) {
agent.step();
});
};

Model.prototype.reset = function () {
this.iteration = -1;
this.islands = [];
this.agents = [];
this.eventLog = new EventLog();
this.counter = new Counter();

// Reset agent ID counter
__agentIdCounter__ = 0;

window.__COUNTER__ = this.counter;

this.setup();
};
62 changes: 55 additions & 7 deletions js/Simulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ function Simulation(canvas, model) {
this.canvas = canvas;
this.model = model;
this.gridPixelSize = 10;
this.paused = false;
this.frameInterval = 300;

model.eventLog.onUpdate(
this.onEventLogUpdate.bind(this)
Expand All @@ -10,6 +12,41 @@ function Simulation(canvas, model) {
window.__MODEL__ = model;
}

Simulation.prototype.pause = function () {
this.paused = true;
};

Simulation.prototype.resume = function () {
this.paused = false;
};

Simulation.prototype.togglePause = function () {
this.paused = !this.paused;
return this.paused;
};

Simulation.prototype.setSpeed = function (intervalMs) {
this.frameInterval = intervalMs;
};

Simulation.prototype.reset = function () {
// Clear the canvas except for islands and bridges
var circles = this.canvas.querySelectorAll('circle');
for (var i = 0; i < circles.length; i++) {
circles[i].remove();
}

// Clear event log
var eventLog = document.getElementById('event-log');
while (eventLog.firstChild) {
eventLog.removeChild(eventLog.firstChild);
}

// Reset the model
this.model.reset();
this.renderStats();
};

Simulation.prototype.tick = function () {
this.model.step();
this.drawAgents();
Expand All @@ -21,14 +58,18 @@ Simulation.prototype.run = function () {
this.drawBridges();
this.renderStats();

var request = function () {
requestAnimationFrame(function () {
this.tick();
setTimeout(request, 300);
}.bind(this));
}.bind(this);
var self = this;
var lastFrameTime = 0;

request();
var animate = function (currentTime) {
if (!self.paused && currentTime - lastFrameTime >= self.frameInterval) {
self.tick();
lastFrameTime = currentTime;
}
requestAnimationFrame(animate);
};

requestAnimationFrame(animate);
};

Simulation.prototype.drawAgents = function () {
Expand Down Expand Up @@ -156,6 +197,8 @@ Simulation.prototype.colorizeWord = function (instance) {
return span;
};

Simulation.MAX_EVENT_LOG_ENTRIES = 100;

Simulation.prototype.onEventLogUpdate = function (event, instance) {
var eventLogElement = document.getElementById('event-log');
var logEntry = document.createElement('li');
Expand All @@ -176,6 +219,11 @@ Simulation.prototype.onEventLogUpdate = function (event, instance) {
}
eventLogElement.insertBefore(logEntry, eventLogElement.firstChild);

// Limit event log to prevent memory leak
while (eventLogElement.children.length > Simulation.MAX_EVENT_LOG_ENTRIES) {
eventLogElement.removeChild(eventLogElement.lastChild);
}

this.renderStats();
};

Expand Down
6 changes: 3 additions & 3 deletions js/Word.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ Word.random = function (length, island) {

Word.prototype.compoundWith = function (wordInstance, island) {
var left = this.word.slice(
Math.floor(this.word.length / 2)
0, Math.floor(this.word.length / 2)
);

var right = this.word.slice(
Math.floor(this.word.length / 2)
var right = wordInstance.word.slice(
Math.floor(wordInstance.word.length / 2)
);

var infix = (
Expand Down
2 changes: 1 addition & 1 deletion js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function attributeGetter(attr) {

function isEqual(value) {
return function (item) {
return item == value;
return item === value;
};
}

Expand Down
40 changes: 39 additions & 1 deletion style.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,48 @@ body {
}

#canvas {
margin-top: 20px;
margin-top: 10px;
background: #F8F8F8;
}

.controls {
margin-top: 15px;
padding: 10px 0;
display: flex;
align-items: center;
gap: 15px;
}

.controls button {
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
border: 1px solid #ccc;
background: #fff;
border-radius: 4px;
}

.controls button:hover {
background: #f0f0f0;
}

.controls label {
display: flex;
align-items: center;
gap: 8px;
color: gray;
font-size: 14px;
}

.controls input[type="range"] {
width: 120px;
}

#speed-value {
min-width: 35px;
display: inline-block;
}

.canvas-container {
overflow: hidden;
}
Expand Down