cacatoo
Version:
Building, exploring, and sharing spatially structured models
908 lines (775 loc) • 33.8 kB
HTML
<html>
<script src="../scripts/cacatoo.js"></script> <!-- Include cacatoo library (compiled with rollup) -->
<script src="../scripts/all.js"></script> <!-- Load other packages -->
<link rel="shortcut icon" type="image/jpg" href="../../patterns/cacatoo.png"/>
<link rel="stylesheet" href="../../style/cacatoo.css"> <!-- Set style sheet -->
<style>
body {
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
background-color: rgb(0, 0, 0);
margin: 0px;
color: white;
font-size: 13px;
overflow-x: hidden;
}
/* The button itself */
button {
background-color: #8438a0;
border-radius: 10px 10px 10px 10px;
color: white;
border: 0px solid rgb(0, 0, 0);
padding: 7px;
font-size: 16px;
margin: 10px;
width: 128px;
}
a{
color:rgb(132, 0, 255);
}
.gameheader{
font-size: 20px;
color:white;
width:100%;
padding:3px;
text-align: center;
}
.gamecanvas{
padding: 0px;
margin: 0px;
text-align:center;
}
.canvas {
border: '1px solid red';
display: inline-block;
}
input[type='checkbox'] {
width:30px;
height:30px;
}
input[type='checkbox']:checked {
}
.skilltree1{
height: 700;
padding: 20px;
margin-left:70px;
width:16%;
color: white;
font-size:16px;
float: left;
border-radius: 15px;
background-color: #800080AA;
}
@media (max-width: 1000px) {
.skilltree1 {
height: 700;
padding: 20px;
margin-left:70px;
margin-top:70px;
width:30%;
color: white;
font-size:16px;
float: left;
border-radius: 15px;
background-color: #800080AA;
}
}
.skilltree2{
height: 700;
padding: 20px;
margin-right:70px;
width:16%;
color: black;
font-size:16px;
float: right;
border-radius: 15px;
background-color: #ffbb00d3;
}
@media (max-width: 1000px) {
.skilltree2 {
height: 700 ;
padding: 20px;
margin-right:70px;
margin-top:70px;
width:30%;
color: white;
font-size:16px;
float: right;
border-radius: 15px;
background-color: #ffbb00d3;
}
}
.grid-holder {
margin: 0px;
display: inline-block;
}
.number-input {
display: inline-block;
}
.number-input button {
display: inline-block;
background-color: #bb00ff;
color: white;
border: none;
width:100px;
padding: 10px 0px;
margin:10px;
height:70px;
font-size: 24px;
cursor: pointer;
}
.number-input button:focus {
outline: none;
}
.number-input input {
width: 60px;
text-align: center;
font-size: 24px;
margin: 0 10px;
padding: 5px;
}
/* ===== UITLEG MODAL ===== */
.helpbtn{
position: fixed;
cursor: pointer;
top: 10px;
left: 10px;
z-index: 9999;
width: auto;
padding: 9px 12px;
font-size: 14px;
background-color: #8438a0;
}
.modal-backdrop{
position: fixed;
inset: 0;
background: rgba(0,0,0,.68); /* donkerder overlay */
backdrop-filter: blur(10px); /* minder blur (of 0px) */
z-index: 9998;
display: none;
}
.modal{
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: min(720px, calc(100vw - 28px));
max-height: calc(100vh - 28px);
overflow: auto;
z-index: 9999;
display: none;
border-radius: 18px;
border: 1px solid rgba(255,255,255,.12);
background: linear-gradient(180deg, var(--card), var(--card2));
}
.modal-head{
display:flex;
justify-content: space-between;
align-items: center;
padding: 12px 14px;
border-bottom: 1px solid rgba(255,255,255,.1);
}
.modal-close{
width: auto;
padding: 6px 10px;
margin: 0;
font-size: 14px;
background: rgba(255,255,255,.08);
}
.modal-body{
padding: 14px;
color: var(--text);
font-size: 14px;
line-height: 1.45;
}
.modal-body ul{ margin: 8px 0 0 18px; }
.modal-body code{
background: rgba(255,255,255,.06);
padding: 2px 6px;
border-radius: 8px;
}
</style>
<script>
/*-----------------------Start user-defined code ---------------------*/
let sim;
var pressedKeys = {};
window.onkeyup = function(e) { pressedKeys[e.keyCode] = false; }
window.onkeydown = function(e) { pressedKeys[e.keyCode] = true; }
var start_totalfood = 50000
var food_purple = 0
var food_gold = 0
var maxpoints = 32
var ant_2_points = maxpoints
var ant_1_points = maxpoints
// controllable things
var ant_1_speed = 1
var ant_2_speed = 1
var ant_1_number = 100
var ant_2_number = 100
var ant_1_pherprod = 1
var ant_2_pherprod = 1
var ant_1_pherprod_food = 1
var ant_2_pherprod_food = 1
var ant_1_strength = 1
var ant_2_strength = 1
var ant_1_sense = 1
var ant_2_sense = 1
var max_foraging_time = 600
var boundary = true
var ff = false
/**
* function cacatoo() contains all the user-defined parts of a cacatoo-model.
Configuration, update rules, what is displayed or plotted, etc. It's all here.
*/
function cacatoo() {
let simconfig = {
title: "Epic ant battle (of history)", // The name of your cacatoo-simulation
description: "", // And a description if you wish
maxtime: 1000000, // How many time steps the model continues to run
// (note, the onscreen FPS may drop below 60 fps when using fast mode, although many more timesteps may be handled per second)
ncol: 100, // Number of columns (width of your grid)
nrow: 100, // Number of rows (height of your grid)
scale: 7, // Scale of the grid (nxn pixels per grid point)
sleep: 0,
seed: new Date().getTime(),
wrap: [false,false],
wrapreflect: 0.6,
bgcolour:'#00000011',
graph_update: 25,
graph_interval: 5
}
// FLOCKCONFIG EXAMPLE
let flockconfig = {
// Flock parameters
num_boids: 0, // Starting number of boids (flocking individuals)
shape: 'ant', // Shape of the boids drawn (options: bird, arrow, line, rect, dot, ant)
max_speed: 1, // Maximum velocity of boids
max_force: 0.5, // Maximum steering force applied to boids (separation/cohesion/alignment rules)
friction: 0.0,
// Mouse parameters
mouse_radius: 10, // Radius of boids captured by the mouse overlay
draw_mouse_radius: false, // Show a circle where the mouse is
click: 'repel',
}
sim = new Simulation(simconfig) // Initialise the Cacatoo simulation
sim.makeGridmodel("pheromones") // Make a grid for the pheromones
sim.makeFlockmodel("flock", flockconfig) // Add a flockmodel, which contains invidiuals (boids) in continuous space
// sim.flock.placeObstacle({x:sim.ncol/3,y:sim.nrow/3,r:12,fill:'#222222',force:1})
// sim.flock.placeObstacle({x:sim.ncol-sim.ncol/3,y:sim.nrow-sim.nrow/3,r:8,fill:'#222222',force:1})
sim.createFlockDisplay("flock",{label:""}) // Alias for old 'createDisplay' function, makes distinction with new flocks clearer
let ant_1_div = document.createElement("div")
ant_1_div.className = "skilltree1"
ant_1_div.id = "skilltree1"
ant_1_div.innerHTML += `<font size='4'><b>Purple ants <p style='margin:10px;font-size:30px' id='ant_1_points_remaining'>`
+ant_1_points+`/`+maxpoints+
`</p> attribute points remaining</b></font><br><br>`
document.getElementById("canvas_holder").appendChild(ant_1_div)
let pb2 = new AntButton({name:"Num ants", var: "ant_1_number", min: 10, max: 200, increment:10,
container:ant_1_div, points: 9,color: '#e0a709',digits:1,antpoints: 'ant_1_points', antpointsdiv: 'ant_1_points_remaining'})
let pb1 = new AntButton({name:"Speed", var: "ant_1_speed", min: 0.1, max: 2, increment:0.1,
container:ant_1_div, color: '#e0a709',points: 3,digits:2, antpoints: 'ant_1_points', antpointsdiv: 'ant_1_points_remaining'})
let pb3 = new AntButton({name:"Signal home", var: "ant_1_pherprod", min: 0, max: 2, increment:0.1,
container:ant_1_div, points: 8,color: '#e0a709',digits:2,antpoints: 'ant_1_points', antpointsdiv: 'ant_1_points_remaining'})
let pb4 = new AntButton({name:"Signal food", var: "ant_1_pherprod_food", min: 0, max: 2, increment:0.1,
container:ant_1_div, points: 8,color: '#e0a709',digits:2,antpoints: 'ant_1_points', antpointsdiv: 'ant_1_points_remaining'})
let pb6 = new AntButton({name:"Sensitivity", var: "ant_1_sense", min: 0.0, max: 1.0, increment:0.05,
container:ant_1_div, points:2, color: '#e0a709',digits:3,antpoints: 'ant_1_points', antpointsdiv: 'ant_1_points_remaining'})
let pb5 = new AntButton({name:"Strength", var: "ant_1_strength", min: 1, max: 3, increment:0.25,
container:ant_1_div, points: 2,color: '#e0a709',digits:3,antpoints: 'ant_1_points', antpointsdiv: 'ant_1_points_remaining'})
let ant_2_div = document.createElement("div")
ant_2_div.className = "skilltree2"
ant_2_div.id = "skilltree2"
ant_2_div.innerHTML += `<font size='4'><b>Golden ants <p style='margin:10px;font-size:30px' id='ant_2_points_remaining'>`+ant_2_points+
`</p> attribute points remaining</b></font><br><br>`
document.getElementById("canvas_holder").appendChild(ant_2_div)
let gb2 = new AntButton({name:"Num ants", var: "ant_2_number", min: 10, max: 200, increment:10,
container:ant_2_div, points: 6, digits:1, antpoints: 'ant_2_points', antpointsdiv: 'ant_2_points_remaining'})
let gb1 = new AntButton({name:"Speed", var: "ant_2_speed", min: 0.1, max: 2, increment:0.1,
container:ant_2_div, points: 2, digits:2, antpoints: 'ant_2_points', antpointsdiv: 'ant_2_points_remaining'})
let gb3 = new AntButton({name:"Signal home", var: "ant_2_pherprod", min: 0, max: 2, increment:0.1,
container:ant_2_div, points: 8,digits:2,antpoints: 'ant_2_points', antpointsdiv: 'ant_2_points_remaining'})
let gb4 = new AntButton({name:"Signal food", var: "ant_2_pherprod_food", min: 0, max: 2, increment:0.1,
container:ant_2_div, points: 8,digits:2,antpoints: 'ant_2_points', antpointsdiv: 'ant_2_points_remaining'})
let gb6 = new AntButton({name:"Sensitivity", var: "ant_2_sense", min: 0, max: 1.0, increment:0.05,
container:ant_2_div, points: 8,digits:3,antpoints: 'ant_2_points', antpointsdiv: 'ant_2_points_remaining'})
let gb5 = new AntButton({name:"Strength", var: "ant_2_strength", min: 1, max: 3, increment:0.25,
container:ant_2_div, points: 0,digits:3,antpoints: 'ant_2_points', antpointsdiv: 'ant_2_points_remaining'})
sim.spawnAnts = function(){
if(sim.flock.purple_ants < ant_1_number){
sim.flock.purple_ants++
let dir = sim.rng.random()*Math.PI
sim.flock.boids.push(boid = {position: {x:3+1*sim.rng.random(),y:3+1*sim.rng.random()},
velocity: {x:Math.sin(dir),y:Math.cos(dir)},
acceleration: {x:0,y:0},
max_speed:ant_1_speed,
type: "purple",
strength: ant_1_strength,
sense: ant_1_sense,
pherprod: ant_1_pherprod,
pherprod_food: ant_1_pherprod_food,
fill: "#bc27c4",
foraging: true,
foraging_time: 0,
size: 3 })
}
if(sim.flock.golden_ants < ant_2_number){
sim.flock.golden_ants++
let dir = sim.rng.random()*Math.PI
sim.flock.boids.push({position: {x:sim.ncol-3-1*sim.rng.random(),y:sim.nrow-3-1*sim.rng.random()},
velocity: {x:-sim.rng.random(),y:-sim.rng.random()},
acceleration: {x:0,y:0},
type: "gold",
fill: "#e0a709",
max_speed:ant_2_speed,
strength: ant_2_strength,
sense: ant_2_sense,
foraging: true,
foraging_time: 0,
pherprod: ant_2_pherprod,
pherprod_food: ant_2_pherprod_food,
size: 3 })
}
}
sim.reset = function(){
sim.flock.resetPlots()
sim.flock.purple_ants = 0
sim.flock.golden_ants = 0
totalfood = start_totalfood
food_gold = 0
food_purple = 0
for(let x=0;x<sim.ncol;x++) for(let y=0;y<sim.nrow;y++){
sim.pheromones.grid[x][y].homing_velocity = {x:0.0,y:0.0}
sim.pheromones.grid[x][y].food_velocity = {x:0.0,y:0.0}
}
sim.flock.boids = []
}
sim.reset()
/*
2. DEFINING THE RULES. Below, the user defines the nextState function. This function will be applied for each grid point when we will update the grid later.
*/
sim.pheromones.nextState = function(x, y) {
this.grid[x][y].homing_velocity = sim.flock.scaleVector(this.grid[x][y].homing_velocity, 0.98) // decay
this.grid[x][y].food_velocity = sim.flock.scaleVector(this.grid[x][y].food_velocity, 0.98) // decay
for(let i=1; i<=4; i++) { // diffusion
let neigh = this.getNeighbour(this,x,y,i)
if(neigh==undefined) continue
let diffusion_const = 0.01
neigh.homing_velocity.x += diffusion_const*this.grid[x][y].homing_velocity.x
this.grid[x][y].homing_velocity.x -= diffusion_const*this.grid[x][y].homing_velocity.x
neigh.homing_velocity.y += diffusion_const*this.grid[x][y].homing_velocity.y
this.grid[x][y].homing_velocity.y -= diffusion_const*this.grid[x][y].homing_velocity.y
neigh.food_velocity.x += diffusion_const*this.grid[x][y].food_velocity.x
this.grid[x][y].food_velocity.x -= diffusion_const*this.grid[x][y].food_velocity.x
neigh.food_velocity.y += diffusion_const*this.grid[x][y].food_velocity.y
this.grid[x][y].food_velocity.y -= diffusion_const*this.grid[x][y].food_velocity.y
}
//this.grid[x][y].homing_pheromone = sim.flock.lengthVector(this.grid[x][y].homing_velocity)
}
sim.pheromones.update = function() {
//if(sim.time%10==0)this.diffuseStateVector("homing_velocity",0.01)
//if(sim.time%10==0)this.diffuseStateVector("food_velocity",0.01)
this.asynchronous()
}
sim.flock.update = function(){
if(sim.time%5==0)sim.spawnAnts()
this.plotArray(["Purple","Gold"],[food_purple,food_gold],["#800080","gold"],"Sugar collected",
{
width:800, labelsDivStyles: { color: "black" },
axes: {
x: {
axisLabelColor: "white",
axisLabelFontColor: "white"
},
y: {
axisLabelColor: "white",
axisLabelFontColor: "white"
}
}
})
for(let boid of this.boids) {
if(boid.foraging_time>max_foraging_time){
if(boid.type=="purple") {
boid.position = {x:3+10*sim.rng.random(),y:3+10*sim.rng.random()}
boid.velocity = {x:sim.rng.random(),y:sim.rng.random()}
}
else {
boid.position = {x:sim.ncol-3-10*sim.rng.random(),y:sim.nrow-3-10*sim.rng.random()}
boid.velocity = {x:-sim.rng.random(),y:-sim.rng.random()}
}
boid.acceleration = {x:0,y:0}
boid.foraging = true
boid.foraging_time = 0
continue
}
if(boid.foraging){
let dx = boid.position.x - sim.ncol/2
let dy = boid.position.y - sim.nrow/2
let dist = Math.sqrt(dx*dx+dy*dy)
let size_food = Math.max(Math.sqrt(totalfood/Math.PI)*0.9,5)
//size_food = 10
size_food = 30/this.scale
if(dist < size_food && totalfood > 0){
totalfood-= boid.strength
boid.foraging = false
boid.foraging_time = 0
boid.velocity.x *= -1
boid.food ="white"
boid.velocity.y *= -1
if(totalfood <=0) {
boid.strength+=totalfood
totalfood = 0
if(food_purple>food_gold) document.getElementById('gameheader').innerHTML =`<h2><font color="#800080">Purple wins!!<br><font color="white" size=2>(Hit 'Reset' to start again)</font></h2>`
else if (food_purple<food_gold) document.getElementById('gameheader').innerHTML =`<h2><font color="#e0a709">Gold wins!!<br><font color="white" size=2>(Hit 'Reset' to start again)</font></h2>`
else document.getElementById('gameheader').innerHTML =`<h2><font color="white">Draw!<br><font color="white" size=2>(Hit 'Reset' to start again)</font></h2>`
}
}
}
else if(boid.position.x<10 && boid.position.y<10){
let dx = boid.position.x
let dy = boid.position.y
let dist = Math.sqrt(dx*dx+dy*dy)
if(dist < 6){
boid.foraging=true
boid.foraging_time = 0
food_purple += boid.strength
boid.food = undefined
boid.velocity.x *= -1
boid.velocity.y *= -1
}
}
else if(boid.position.x>sim.ncol-10 && boid.position.y>sim.nrow-10){
let dx = boid.position.x - sim.ncol
let dy = boid.position.y - sim.nrow
let dist = Math.sqrt(dx*dx+dy*dy)
if(dist<6){
boid.foraging=true
food_gold += boid.strength
boid.food = undefined
boid.velocity.x *= -1
boid.velocity.y *= -1
}
}
let x = Math.floor(boid.position.x)
let y = Math.floor(boid.position.y)
if(x < 0 || x >= sim.ncol || y < 0 || y >= sim.nrow) continue
let pherx = sim.pheromones.grid[x][y].homing_velocity.x
let phery = sim.pheromones.grid[x][y].homing_velocity.y
let pheromone_angle = Math.atan2(phery,pherx)
// Randomly rotate a bit
// let anglerot = sim.rng.randomGaus(0,2)
// boid.velocity = this.rotateVector(boid.velocity,anglerot)
if(boundary){
if(boid.type=="purple" && boid.position.y > -1-(sim.nrow/sim.ncol)*boid.position.x + sim.nrow) {
boid.acceleration.x += -0.5*sim.rng.random()*boid.max_speed // walk back
boid.acceleration.y += -0.5*sim.rng.random()*boid.max_speed
continue
}
else if(boid.type=="gold" && boid.position.y < 1-(sim.nrow/sim.ncol)*boid.position.x + sim.nrow) {
boid.acceleration.x += 0.5*sim.rng.random()*boid.max_speed
boid.acceleration.y += 0.5*sim.rng.random()*boid.max_speed
continue
}
}
let angle = sim.rng.randomGaus(0,6)
boid.velocity = this.rotateVector(boid.velocity,angle)
if(!boid.foraging){
let pheromone_amount = this.lengthVector(sim.pheromones.grid[x][y].homing_velocity)
boid.acceleration.x += boid.acceleration.x + Math.cos(pheromone_angle) * pheromone_amount * boid.sense/100 * -1
boid.acceleration.y += boid.acceleration.y + Math.sin(pheromone_angle) * pheromone_amount * boid.sense/100 * -1
}
else {
let pheromone_amount = this.lengthVector(sim.pheromones.grid[x][y].food_velocity)
boid.acceleration.x += boid.acceleration.x + Math.cos(pheromone_angle) * pheromone_amount * boid.sense/100
boid.acceleration.y += boid.acceleration.y + Math.sin(pheromone_angle) * pheromone_amount * boid.sense/100
}
if(x > 0 && x < sim.ncol && y > 0 && y < sim.nrow){
if(boid.foraging){
sim.pheromones.grid[x][y].homing_velocity.x += boid.velocity.x*100*boid.pherprod
sim.pheromones.grid[x][y].homing_velocity.y += boid.velocity.y*100*boid.pherprod
}
else {
sim.pheromones.grid[x][y].food_velocity.x += boid.velocity.x*100*boid.pherprod_food
sim.pheromones.grid[x][y].food_velocity.y += boid.velocity.y*100*boid.pherprod_food
}
}
boid.foraging_time++
}
sim.normaliseVector = function (x,y,length){
let magnitude = Math.sqrt(x*x + y*y);
if (magnitude > length) {
// Calculate the scaling factor
const scalingFactor = length / magnitude;
// Scale the vector components
const scaledX = x * scalingFactor;
const scaledY = y * scalingFactor;
// Return the scaled vector as an object
return { x: scaledX, y: scaledY };
}
return { x:x, y:y }
}
sim.canvases[0].underlay = function(){
if(pressedKeys[70] || ff) return
this.ctx.strokeStyle = "black"
this.ctx.lineWidth = 1
let scale_vector = 1
this.ctx.lineWidth = 1
// for(let x=0;x<sim.ncol;x++){
// for(let y=0;y<sim.nrow;y++){
// red = Math.min(sim.flock.lengthVector(sim.pheromones.grid[x][y].homing_velocity)/30,0.5)
// this.ctx.fillStyle = `rgba(0,255,255,${red})`;
// this.ctx.fillRect(x*sim.scale,y*sim.scale,sim.scale,sim.scale)
// blue = Math.min(sim.flock.lengthVector(sim.pheromones.grid[x][y].food_velocity)/30,0.5)
// this.ctx.fillStyle = `rgba(255,0,0,${blue})`;
// this.ctx.fillRect(x*sim.scale,y*sim.scale,sim.scale,sim.scale)
// }
// }
for(let x=0;x<sim.ncol;x++){
for(let y=0;y<sim.nrow;y++){
let velox = sim.pheromones.grid[x][y].homing_velocity.x
let veloy = sim.pheromones.grid[x][y].homing_velocity.y
let length = Math.sqrt(velox*velox + veloy*veloy)
let scaled = sim.normaliseVector(velox,veloy,10)
// this.ctx.beginPath()
// this.ctx.strokeStyle= `rgba(200,0,0,${length/100})`;
// this.ctx.lineWidth = 2
// this.ctx.moveTo(this.scale*0.5+x*this.scale,this.scale*0.5+y*this.scale)
// this.ctx.lineTo(this.scale*0.5+x*this.scale+scaled.x*scale_vector,this.scale*0.5+y*this.scale+scaled.y*scale_vector)
// this.ctx.stroke()
// this.ctx.closePath()
this.ctx.fillStyle= `rgba(200,0,0,${length/100})`;
this.ctx.fillRect(x*sim.scale,y*sim.scale,sim.scale/2,sim.scale/2)
velox = sim.pheromones.grid[x][y].food_velocity.x
veloy = sim.pheromones.grid[x][y].food_velocity.y
length = Math.sqrt(velox*velox + veloy*veloy)
scaled = sim.normaliseVector(velox,veloy,10)
// this.ctx.beginPath()
// this.ctx.strokeStyle= `rgba(0,100,250,${length/100})`;
// this.ctx.lineWidth = 2
// this.ctx.moveTo(this.scale*0.5+x*this.scale,this.scale*0.5+y*this.scale)
// this.ctx.lineTo(this.scale*0.5+x*this.scale+scaled.x*scale_vector,this.scale*0.5+y*this.scale+scaled.y*scale_vector)
// this.ctx.stroke()
// this.ctx.closePath()
this.ctx.fillStyle= `rgba(0,100,250,${length/100})`;
this.ctx.fillRect(x*sim.scale,y*sim.scale,sim.scale/2,sim.scale/2)
}
}
}
sim.canvases[0].overlay = function(){
if(boundary){
this.ctx.strokeStyle = "#444444"
this.ctx.lineWidth = 5
this.ctx.beginPath()
this.ctx.moveTo(0,sim.nrow*this.scale)
this.ctx.lineTo(sim.ncol*this.scale,0)
this.ctx.stroke()
this.ctx.closePath()
}
// Food source
let size = Math.sqrt(totalfood*this.scale)
if(size < 10) size =10
size = 40
let x = this.scale*sim.ncol/2
let y = this.scale*sim.nrow/2
this.ctx.fillStyle="#FFFFFFFF"
this.ctx.strokeStyle="black"
this.ctx.beginPath()
this.ctx.arc(x,y,size,0,Math.PI*2)
this.ctx.fill()
this.ctx.closePath()
this.ctx.fillStyle="black"
this.ctx.textAlign = "center";
this.ctx.textBaseline = 'middle';
this.ctx.font = "16px sans";
this.ctx.fillText(`Sugar:`,x,y-10)
this.ctx.fillText(`${totalfood}`,x,y+10)
// Purple nest
this.ctx.fillStyle="#800080aa"
this.ctx.beginPath()
this.ctx.arc(20,20,30,0,Math.PI*2)
this.ctx.fill()
this.ctx.closePath()
this.ctx.fillStyle="white"
this.ctx.textAlign = "center";
this.ctx.textBaseline = 'middle';
this.ctx.font = "14px sans";
this.ctx.fillText(Math.round(food_purple),20,20)
// Golden nest
this.ctx.fillStyle="#e0a709aa"
this.ctx.beginPath()
this.ctx.arc(sim.ncol*this.scale-20,sim.nrow*this.scale-20,30,0,Math.PI*2)
this.ctx.fill()
this.ctx.closePath()
this.ctx.fillStyle="white"
this.ctx.textAlign = "center";
this.ctx.textBaseline = 'middle';
this.ctx.font = "14px sans";
this.ctx.fillText(Math.round(food_gold),sim.ncol*this.scale-20,sim.nrow*this.scale-20)
}
}
sim.start()
// sim.addButton("pause/continue", function () { sim.toggle_play() })
// sim.addButton("step", function () { sim.step(); sim.display() })
sim.addButton("Respec purple", function () { pb6.reset(); pb1.reset();pb2.reset();pb3.reset();pb4.reset();pb5.reset(); })
document.getElementById('Respec purple').style = 'width: 200; margin-left: 7.5em; background:#800080AA; float: left;'
sim.addButton("Reset", function () {
if(sim.pause) {
sim.pause = false;
sim.reset();
this.innerHTML = "Reset"
return
}
else {
sim.reset();
sim.pause = true;
this.innerHTML = "Start!"
}})
document.getElementById('Reset').style = ''
sim.addButton("Respec gold", function () { gb6.reset(); gb1.reset();gb2.reset();gb3.reset();gb4.reset();gb5.reset(); })
document.getElementById('Respec gold').style = 'width:200; margin-right:7.5em; background:#ffbb00d3;float: right; width:200px'
sim.addButton("Start/pause",function () { sim.toggle_play() })
sim.addToggle("boundary", "Boundary")
sim.addToggle("ff", "Fast forward")
sim.step()
sim.pause = false
}
class AntButton {
// Genome constructor
constructor(cfg) {
this.cfg = cfg
this.name = cfg.name
this.var = cfg.var
this.min = cfg.min
this.max = cfg.max
this.increment = cfg.increment
this.points = cfg.points
this.container = cfg.container
this.antpoints = cfg.antpoints
this.antpointsdiv = cfg.antpointsdiv
this.color = cfg.color
this.createElement()
window[this.antpoints] -= this.points
document.getElementById(this.antpointsdiv).innerHTML = window[this.antpoints] + '/' + maxpoints
}
createElement(){
let div = document.createElement('div')
div.classList.add("number-input")
this.downbutton = document.createElement("button")
this.downbutton.addEventListener('click', (event) => {
if(event.shiftKey) for(let i=0;i<5;i++) this.decreaseValue(event)
else this.decreaseValue(event)
});
this.downbutton.style = "width:50;" + 'background-color:' + this.color + ";"
this.downbutton.innerText = "-"
this.upbutton = document.createElement("button")
this.upbutton.addEventListener('click', (event) => {
if(event.shiftKey) for(let i=0;i<5;i++) this.increaseValue(event)
else this.increaseValue(event)
});
this.upbutton.style = "width:50;" + 'background-color:' + this.color + ";"
this.upbutton.innerText = "+"
this.varvaluediv = document.createElement("div")
this.varvaluediv.style = "display:inline-block; position:relative; top:12px; height:50px; font-size:14px; width:90;"
window[this.var] = this.min + this.points*this.increment
this.varvaluediv.innerHTML += this.points + '<br>' + this.name + '<br>(' + window[this.var].toFixed(this.cfg.digits-1) + ')'
div.appendChild(this.downbutton)
div.appendChild(this.varvaluediv)
div.appendChild(this.upbutton)
this.container.appendChild(div)
}
increaseValue() {
if(window[this.antpoints] > 0 && window[this.var] < this.max){
this.points++ // points returned
window[this.var] = this.min + this.points*this.increment
this.varvaluediv.innerHTML = this.points + '<br>' + this.name + '<br>(' + window[this.var].toFixed(this.cfg.digits-1) + ')'
window[this.antpoints]--
document.getElementById(this.antpointsdiv).innerHTML = window[this.antpoints] + '/' + maxpoints
}
//sim.reset();
}
decreaseValue() {
if(window[this.antpoints] < maxpoints && window[this.var] > this.min){
this.points-- // points invested down
window[this.var] = this.min + this.points*this.increment
this.varvaluediv.innerHTML = this.points + '<br>' + this.name + '<br>(' + window[this.var].toFixed(this.cfg.digits-1) + ')'
window[this.antpoints]++
document.getElementById(this.antpointsdiv).innerHTML = window[this.antpoints] + '/' + maxpoints
}
}
reset(){
window[this.antpoints]+= this.points
this.points = 0
window[this.var] = this.min + this.points*this.increment
this.varvaluediv.innerHTML = this.points + '<br>' + this.name + '<br>(' + window[this.var].toFixed(this.cfg.digits-1) + ')'
document.getElementById(this.antpointsdiv).innerHTML = window[this.antpoints] + '/' + maxpoints
}
}
// ===== UITLEG MODAL LOGIC =====
function openHelp(){
const m = document.getElementById('helpmodal');
const b = document.getElementById('helpbackdrop');
if(!m || !b) return;
m.style.display = 'block';
b.style.display = 'block';
}
function closeHelp(){
const m = document.getElementById('helpmodal');
const b = document.getElementById('helpbackdrop');
if(!m || !b) return;
m.style.display = 'none';
b.style.display = 'none';
}
window.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('helpbtn');
const close = document.getElementById('helpclose');
const back = document.getElementById('helpbackdrop');
if(btn) btn.addEventListener('click', openHelp);
if(close) close.addEventListener('click', closeHelp);
if(back) back.addEventListener('click', closeHelp);
window.addEventListener('keydown', (e) => {
if(e.key === 'Escape') closeHelp();
});
});
</script>
<body onload="cacatoo()">
<button class="helpbtn" id="helpbtn">Uitleg en vragen</button>
<div class="modal-backdrop" id="helpbackdrop"></div>
<div class="modal" id="helpmodal" role="dialog" aria-modal="true" aria-labelledby="helpTitle">
<div class="modal-head">
<b id="helpTitle">Uitleg en vragen</b>
<button class="modal-close" id="helpclose">X</button>
</div>
<div class="modal-body">
<p>
Modellen en simulaties kunnen in de biologie worden gebruikt om te begrijpen hoe biologische systemen werken en om voorspellingen te maken.
In deze interactieve simulatie kun je zelf een model van mieren die suiker zoeken aanpassen en kijken wat er gebeurt.
</p>
<p>
Dit is een simpele simulatie van mieren die suiker zoeken. De <b>mieren</b> lopen blind rond op zoek het suiker (in het midden),
en laten daarbij rode en blauwe <b>pheromoon-signalen</b> achter. Omdat de mieren rode signalen achterlaten als ze van het nest weglopen,
kunnen mieren deze signalen volgen om terug te lopen naar het nest. En omdat ze blauwe signalen achterlaten als ze voedsel hebben gevonden, kunnen
andere mieren deze volgen om bij het voedsel te komen. De signalen verspreiden/doven langzaam.
</p>
<p><b>Wat kun je doen?</b></p>
<ul>
<li>Verdeel de <i> attribute points</i> voor de <font color='magenta'><b>paarse</b></font> en <font color='gold'><b>gouden</b></font> mieren (links/rechts), en kijk wie er wint.</li>
<li>Test strategieën: meer mieren vs. sneller vs. beter “ruiken” vs. meer suiker kunnen dragen.</li>
<li>Gebruik <code>Reset</code> / <code>Respec</code> om opnieuw te beginnen.</li>
</ul>
<p><b>Probeer deze vragen te beantwoorden:</b></p>
<ul>
<li>Wie winnen er met de standaardinstellingen op korte termijn? En op lange termijn?</li>
<li>Kun je een strategie vinden die de (standaard ingestelde) gouden mieren verslaan?</li>
<li>De <i>boundary</i> (grijze lijn) zorgt ervoor dat de mieren niet naar elkaars helft kunnen. Wat zie je als je deze muur weghaalt? </li>
</ul>
<p>
Heb je vragen? Stuur ze gerust naar <a href="mailto:b.vandijk@uu.nl">Bram van Dijk</a>
</p>
</div>
</div>
<div class="gameheader" id="gameheader">
<h2>Foraging for sugar: power in numbers?<br><font size=2>A simple simulation of ants foraging for food. Try your own strategy and see what works!</font> </h2>
</div>
<div class="gamecanvas" id="canvas_holder"></div>
<center><div class="" id="form_holder" style="clear: both; width:100%"> </div>
<div class="" id="graph_holder" style="display:inline-block; text-align:center"> </div></center>
</body>
</html>