UNPKG

cacatoo

Version:

Building, exploring, and sharing spatially structured models

908 lines (775 loc) 33.8 kB
<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>