578 lines
19 KiB
JavaScript
578 lines
19 KiB
JavaScript
var canvas, ctx;
|
|
|
|
// A 2D vector utility
|
|
var Vec = function(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
Vec.prototype = {
|
|
|
|
// utilities
|
|
dist_from: function(v) { return Math.sqrt(Math.pow(this.x-v.x,2) + Math.pow(this.y-v.y,2)); },
|
|
length: function() { return Math.sqrt(Math.pow(this.x,2) + Math.pow(this.y,2)); },
|
|
|
|
// new vector returning operations
|
|
add: function(v) { return new Vec(this.x + v.x, this.y + v.y); },
|
|
sub: function(v) { return new Vec(this.x - v.x, this.y - v.y); },
|
|
rotate: function(a) { // CLOCKWISE
|
|
return new Vec(this.x * Math.cos(a) + this.y * Math.sin(a),
|
|
-this.x * Math.sin(a) + this.y * Math.cos(a));
|
|
},
|
|
|
|
// in place operations
|
|
scale: function(s) { this.x *= s; this.y *= s; },
|
|
normalize: function() { var d = this.length(); this.scale(1.0/d); }
|
|
}
|
|
|
|
// line intersection helper function: does line segment (p1,p2) intersect segment (p3,p4) ?
|
|
var line_intersect = function(p1,p2,p3,p4) {
|
|
var denom = (p4.y-p3.y)*(p2.x-p1.x)-(p4.x-p3.x)*(p2.y-p1.y);
|
|
if(denom===0.0) { return false; } // parallel lines
|
|
var ua = ((p4.x-p3.x)*(p1.y-p3.y)-(p4.y-p3.y)*(p1.x-p3.x))/denom;
|
|
var ub = ((p2.x-p1.x)*(p1.y-p3.y)-(p2.y-p1.y)*(p1.x-p3.x))/denom;
|
|
if(ua>0.0&&ua<1.0&&ub>0.0&&ub<1.0) {
|
|
var up = new Vec(p1.x+ua*(p2.x-p1.x), p1.y+ua*(p2.y-p1.y));
|
|
return {ua:ua, ub:ub, up:up}; // up is intersection point
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var line_point_intersect = function(p1,p2,p0,rad) {
|
|
var v = new Vec(p2.y-p1.y,-(p2.x-p1.x)); // perpendicular vector
|
|
var d = Math.abs((p2.x-p1.x)*(p1.y-p0.y)-(p1.x-p0.x)*(p2.y-p1.y));
|
|
d = d / v.length();
|
|
if(d > rad) { return false; }
|
|
|
|
v.normalize();
|
|
v.scale(d);
|
|
var up = p0.add(v);
|
|
if(Math.abs(p2.x-p1.x)>Math.abs(p2.y-p1.y)) {
|
|
var ua = (up.x - p1.x) / (p2.x - p1.x);
|
|
} else {
|
|
var ua = (up.y - p1.y) / (p2.y - p1.y);
|
|
}
|
|
if(ua>0.0&&ua<1.0) {
|
|
return {ua:ua, up:up};
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Wall is made up of two points
|
|
var Wall = function(p1, p2) {
|
|
this.p1 = p1;
|
|
this.p2 = p2;
|
|
}
|
|
|
|
// World object contains many agents and walls and food and stuff
|
|
var util_add_box = function(lst, x, y, w, h) {
|
|
lst.push(new Wall(new Vec(x,y), new Vec(x+w,y)));
|
|
lst.push(new Wall(new Vec(x+w,y), new Vec(x+w,y+h)));
|
|
lst.push(new Wall(new Vec(x+w,y+h), new Vec(x,y+h)));
|
|
lst.push(new Wall(new Vec(x,y+h), new Vec(x,y)));
|
|
}
|
|
|
|
// item is circle thing on the floor that agent can interact with (see or eat, etc)
|
|
var Item = function(x, y, type) {
|
|
this.p = new Vec(x, y); // position
|
|
this.type = type;
|
|
this.rad = 10; // default radius
|
|
this.age = 0;
|
|
this.cleanup_ = false;
|
|
}
|
|
|
|
var World = function() {
|
|
this.agents = [];
|
|
this.W = canvas.width;
|
|
this.H = canvas.height;
|
|
|
|
this.clock = 0;
|
|
|
|
// set up walls in the world
|
|
this.walls = [];
|
|
var pad = 10;
|
|
util_add_box(this.walls, pad, pad, this.W-pad*2, this.H-pad*2);
|
|
util_add_box(this.walls, 100, 100, 200, 300); // inner walls
|
|
this.walls.pop();
|
|
util_add_box(this.walls, 400, 100, 200, 300);
|
|
this.walls.pop();
|
|
|
|
// set up food and poison
|
|
this.items = []
|
|
for(var k=0;k<30;k++) {
|
|
var x = convnetjs.randf(20, this.W-20);
|
|
var y = convnetjs.randf(20, this.H-20);
|
|
var t = convnetjs.randi(1, 3); // food or poison (1 and 2)
|
|
var it = new Item(x, y, t);
|
|
this.items.push(it);
|
|
}
|
|
}
|
|
|
|
World.prototype = {
|
|
// helper function to get closest colliding walls/items
|
|
stuff_collide_: function(p1, p2, check_walls, check_items) {
|
|
var minres = false;
|
|
|
|
// collide with walls
|
|
if(check_walls) {
|
|
for(var i=0,n=this.walls.length;i<n;i++) {
|
|
var wall = this.walls[i];
|
|
var res = line_intersect(p1, p2, wall.p1, wall.p2);
|
|
if(res) {
|
|
res.type = 0; // 0 is wall
|
|
if(!minres) { minres=res; }
|
|
else {
|
|
// check if its closer
|
|
if(res.ua < minres.ua) {
|
|
// if yes replace it
|
|
minres = res;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// collide with items
|
|
if(check_items) {
|
|
for(var i=0,n=this.items.length;i<n;i++) {
|
|
var it = this.items[i];
|
|
var res = line_point_intersect(p1, p2, it.p, it.rad);
|
|
if(res) {
|
|
res.type = it.type; // store type of item
|
|
if(!minres) { minres=res; }
|
|
else { if(res.ua < minres.ua) { minres = res; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return minres;
|
|
},
|
|
tick: function() {
|
|
// tick the environment
|
|
this.clock++;
|
|
|
|
// fix input to all agents based on environment
|
|
// process eyes
|
|
this.collpoints = [];
|
|
for(var i=0,n=this.agents.length;i<n;i++) {
|
|
var a = this.agents[i];
|
|
for(var ei=0,ne=a.eyes.length;ei<ne;ei++) {
|
|
var e = a.eyes[ei];
|
|
// we have a line from p to p->eyep
|
|
var eyep = new Vec(a.p.x + e.max_range * Math.sin(a.angle + e.angle),
|
|
a.p.y + e.max_range * Math.cos(a.angle + e.angle));
|
|
var res = this.stuff_collide_(a.p, eyep, true, true);
|
|
if(res) {
|
|
// eye collided with wall
|
|
e.sensed_proximity = res.up.dist_from(a.p);
|
|
e.sensed_type = res.type;
|
|
} else {
|
|
e.sensed_proximity = e.max_range;
|
|
e.sensed_type = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// let the agents behave in the world based on their input
|
|
for(var i=0,n=this.agents.length;i<n;i++) {
|
|
this.agents[i].forward();
|
|
}
|
|
|
|
// apply outputs of agents on evironment
|
|
for(var i=0,n=this.agents.length;i<n;i++) {
|
|
var a = this.agents[i];
|
|
a.op = a.p; // back up old position
|
|
a.oangle = a.angle; // and angle
|
|
|
|
// steer the agent according to outputs of wheel velocities
|
|
var v = new Vec(0, a.rad / 2.0);
|
|
v = v.rotate(a.angle + Math.PI/2);
|
|
var w1p = a.p.add(v); // positions of wheel 1 and 2
|
|
var w2p = a.p.sub(v);
|
|
var vv = a.p.sub(w2p);
|
|
vv = vv.rotate(-a.rot1);
|
|
var vv2 = a.p.sub(w1p);
|
|
vv2 = vv2.rotate(a.rot2);
|
|
var np = w2p.add(vv);
|
|
np.scale(0.5);
|
|
var np2 = w1p.add(vv2);
|
|
np2.scale(0.5);
|
|
a.p = np.add(np2);
|
|
|
|
a.angle -= a.rot1;
|
|
if(a.angle<0)a.angle+=2*Math.PI;
|
|
a.angle += a.rot2;
|
|
if(a.angle>2*Math.PI)a.angle-=2*Math.PI;
|
|
|
|
// agent is trying to move from p to op. Check walls
|
|
var res = this.stuff_collide_(a.op, a.p, true, false);
|
|
if(res) {
|
|
// wall collision! reset position
|
|
a.p = a.op;
|
|
}
|
|
|
|
// handle boundary conditions
|
|
if(a.p.x<0)a.p.x=0;
|
|
if(a.p.x>this.W)a.p.x=this.W;
|
|
if(a.p.y<0)a.p.y=0;
|
|
if(a.p.y>this.H)a.p.y=this.H;
|
|
}
|
|
|
|
// tick all items
|
|
var update_items = false;
|
|
for(var i=0,n=this.items.length;i<n;i++) {
|
|
var it = this.items[i];
|
|
it.age += 1;
|
|
|
|
// see if some agent gets lunch
|
|
for(var j=0,m=this.agents.length;j<m;j++) {
|
|
var a = this.agents[j];
|
|
var d = a.p.dist_from(it.p);
|
|
if(d < it.rad + a.rad) {
|
|
|
|
// wait lets just make sure that this isn't through a wall
|
|
var rescheck = this.stuff_collide_(a.p, it.p, true, false);
|
|
if(!rescheck) {
|
|
// ding! nom nom nom
|
|
if(it.type === 1) a.digestion_signal += 5.0; // mmm delicious apple
|
|
if(it.type === 2) a.digestion_signal += -6.0; // ewww poison
|
|
it.cleanup_ = true;
|
|
update_items = true;
|
|
break; // break out of loop, item was consumed
|
|
}
|
|
}
|
|
}
|
|
|
|
if(it.age > 5000 && this.clock % 100 === 0 && convnetjs.randf(0,1)<0.1) {
|
|
it.cleanup_ = true; // replace this one, has been around too long
|
|
update_items = true;
|
|
}
|
|
}
|
|
if(update_items) {
|
|
var nt = [];
|
|
for(var i=0,n=this.items.length;i<n;i++) {
|
|
var it = this.items[i];
|
|
if(!it.cleanup_) nt.push(it);
|
|
}
|
|
this.items = nt; // swap
|
|
}
|
|
if(this.items.length < 30 && this.clock % 10 === 0 && convnetjs.randf(0,1)<0.25) {
|
|
var newitx = convnetjs.randf(20, this.W-20);
|
|
var newity = convnetjs.randf(20, this.H-20);
|
|
var newitt = convnetjs.randi(1, 3); // food or poison (1 and 2)
|
|
var newit = new Item(newitx, newity, newitt);
|
|
this.items.push(newit);
|
|
}
|
|
|
|
// agents are given the opportunity to learn based on feedback of their action on environment
|
|
for(var i=0,n=this.agents.length;i<n;i++) {
|
|
this.agents[i].backward();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Eye sensor has a maximum range and senses walls
|
|
var Eye = function(angle) {
|
|
this.angle = angle; // angle relative to agent its on
|
|
this.max_range = 85;
|
|
this.sensed_proximity = 85; // what the eye is seeing. will be set in world.tick()
|
|
this.sensed_type = -1; // what does the eye see?
|
|
}
|
|
|
|
// A single agent
|
|
var Agent = function() {
|
|
|
|
// positional information
|
|
this.p = new Vec(50, 50);
|
|
this.op = this.p; // old position
|
|
this.angle = 0; // direction facing
|
|
|
|
this.actions = [];
|
|
this.actions.push([1,1]);
|
|
this.actions.push([0.8,1]);
|
|
this.actions.push([1,0.8]);
|
|
this.actions.push([0.5,0]);
|
|
this.actions.push([0,0.5]);
|
|
|
|
// properties
|
|
this.rad = 10;
|
|
this.eyes = [];
|
|
for(var k=0;k<9;k++) { this.eyes.push(new Eye((k-3)*0.25)); }
|
|
|
|
// braaain
|
|
//this.brain = new deepqlearn.Brain(this.eyes.length * 3, this.actions.length);
|
|
var spec = document.getElementById('qspec').value;
|
|
eval(spec);
|
|
this.brain = brain;
|
|
|
|
this.reward_bonus = 0.0;
|
|
this.digestion_signal = 0.0;
|
|
|
|
// outputs on world
|
|
this.rot1 = 0.0; // rotation speed of 1st wheel
|
|
this.rot2 = 0.0; // rotation speed of 2nd wheel
|
|
|
|
this.prevactionix = -1;
|
|
}
|
|
Agent.prototype = {
|
|
forward: function() {
|
|
// in forward pass the agent simply behaves in the environment
|
|
// create input to brain
|
|
var num_eyes = this.eyes.length;
|
|
var input_array = new Array(num_eyes * 3);
|
|
for(var i=0;i<num_eyes;i++) {
|
|
var e = this.eyes[i];
|
|
input_array[i*3] = 1.0;
|
|
input_array[i*3+1] = 1.0;
|
|
input_array[i*3+2] = 1.0;
|
|
if(e.sensed_type !== -1) {
|
|
// sensed_type is 0 for wall, 1 for food and 2 for poison.
|
|
// lets do a 1-of-k encoding into the input array
|
|
input_array[i*3 + e.sensed_type] = e.sensed_proximity/e.max_range; // normalize to [0,1]
|
|
}
|
|
}
|
|
|
|
// get action from brain
|
|
var actionix = this.brain.forward(input_array);
|
|
var action = this.actions[actionix];
|
|
this.actionix = actionix; //back this up
|
|
|
|
// demultiplex into behavior variables
|
|
this.rot1 = action[0]*1;
|
|
this.rot2 = action[1]*1;
|
|
|
|
//this.rot1 = 0;
|
|
//this.rot2 = 0;
|
|
},
|
|
backward: function() {
|
|
// in backward pass agent learns.
|
|
// compute reward
|
|
var proximity_reward = 0.0;
|
|
var num_eyes = this.eyes.length;
|
|
for(var i=0;i<num_eyes;i++) {
|
|
var e = this.eyes[i];
|
|
// agents dont like to see walls, especially up close
|
|
proximity_reward += e.sensed_type === 0 ? e.sensed_proximity/e.max_range : 1.0;
|
|
}
|
|
proximity_reward = proximity_reward/num_eyes;
|
|
proximity_reward = Math.min(1.0, proximity_reward * 2);
|
|
|
|
// agents like to go straight forward
|
|
var forward_reward = 0.0;
|
|
if(this.actionix === 0 && proximity_reward > 0.75) forward_reward = 0.1 * proximity_reward;
|
|
|
|
// agents like to eat good things
|
|
var digestion_reward = this.digestion_signal;
|
|
this.digestion_signal = 0.0;
|
|
|
|
var reward = proximity_reward + forward_reward + digestion_reward;
|
|
|
|
// pass to brain for learning
|
|
this.brain.backward(reward);
|
|
}
|
|
}
|
|
|
|
function draw_net() {
|
|
if(simspeed <=1) {
|
|
// we will always draw at these speeds
|
|
} else {
|
|
if(w.clock % 50 !== 0) return; // do this sparingly
|
|
}
|
|
|
|
var canvas = document.getElementById("net_canvas");
|
|
var ctx = canvas.getContext("2d");
|
|
var W = canvas.width;
|
|
var H = canvas.height;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
var L = w.agents[0].brain.value_net.layers;
|
|
var dx = (W - 50)/L.length;
|
|
var x = 10;
|
|
var y = 40;
|
|
ctx.font="12px Verdana";
|
|
ctx.fillStyle = "rgb(0,0,0)";
|
|
ctx.fillText("Value Function Approximating Neural Network:", 10, 14);
|
|
for(var k=0;k<L.length;k++) {
|
|
if(typeof(L[k].out_act)==='undefined') continue; // maybe not yet ready
|
|
var kw = L[k].out_act.w;
|
|
var n = kw.length;
|
|
var dy = (H-50)/n;
|
|
ctx.fillStyle = "rgb(0,0,0)";
|
|
ctx.fillText(L[k].layer_type + "(" + n + ")", x, 35);
|
|
for(var q=0;q<n;q++) {
|
|
var v = Math.floor(kw[q]*100);
|
|
if(v >= 0) ctx.fillStyle = "rgb(0,0," + v + ")";
|
|
if(v < 0) ctx.fillStyle = "rgb(" + (-v) + ",0,0)";
|
|
ctx.fillRect(x,y,10,10);
|
|
y += 12;
|
|
if(y>H-25) { y = 40; x += 12};
|
|
}
|
|
x += 50;
|
|
y = 40;
|
|
}
|
|
}
|
|
|
|
var reward_graph = new cnnvis.Graph();
|
|
function draw_stats() {
|
|
var canvas = document.getElementById("vis_canvas");
|
|
var ctx = canvas.getContext("2d");
|
|
var W = canvas.width;
|
|
var H = canvas.height;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
var a = w.agents[0];
|
|
var b = a.brain;
|
|
var netin = b.last_input_array;
|
|
ctx.strokeStyle = "rgb(0,0,0)";
|
|
//ctx.font="12px Verdana";
|
|
//ctx.fillText("Current state:",10,10);
|
|
ctx.lineWidth = 10;
|
|
ctx.beginPath();
|
|
for(var k=0,n=netin.length;k<n;k++) {
|
|
ctx.moveTo(10+k*12, 120);
|
|
ctx.lineTo(10+k*12, 120 - netin[k] * 100);
|
|
}
|
|
ctx.stroke();
|
|
|
|
if(w.clock % 200 === 0) {
|
|
reward_graph.add(w.clock/200, b.average_reward_window.get_average());
|
|
var gcanvas = document.getElementById("graph_canvas");
|
|
reward_graph.drawSelf(gcanvas);
|
|
}
|
|
}
|
|
|
|
// Draw everything
|
|
function draw() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.lineWidth = 1;
|
|
var agents = w.agents;
|
|
|
|
// draw walls in environment
|
|
ctx.strokeStyle = "rgb(0,0,0)";
|
|
ctx.beginPath();
|
|
for(var i=0,n=w.walls.length;i<n;i++) {
|
|
var q = w.walls[i];
|
|
ctx.moveTo(q.p1.x, q.p1.y);
|
|
ctx.lineTo(q.p2.x, q.p2.y);
|
|
}
|
|
ctx.stroke();
|
|
|
|
// draw agents
|
|
// color agent based on reward it is experiencing at the moment
|
|
var r = Math.floor(agents[0].brain.latest_reward * 200);
|
|
if(r>255)r=255;if(r<0)r=0;
|
|
ctx.fillStyle = "rgb(" + r + ", 150, 150)";
|
|
ctx.strokeStyle = "rgb(0,0,0)";
|
|
for(var i=0,n=agents.length;i<n;i++) {
|
|
var a = agents[i];
|
|
|
|
// draw agents body
|
|
ctx.beginPath();
|
|
ctx.arc(a.op.x, a.op.y, a.rad, 0, Math.PI*2, true);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// draw agents sight
|
|
for(var ei=0,ne=a.eyes.length;ei<ne;ei++) {
|
|
var e = a.eyes[ei];
|
|
var sr = e.sensed_proximity;
|
|
if(e.sensed_type === -1 || e.sensed_type === 0) {
|
|
ctx.strokeStyle = "rgb(0,0,0)"; // wall or nothing
|
|
}
|
|
if(e.sensed_type === 1) { ctx.strokeStyle = "rgb(255,150,150)"; } // apples
|
|
if(e.sensed_type === 2) { ctx.strokeStyle = "rgb(150,255,150)"; } // poison
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.op.x, a.op.y);
|
|
ctx.lineTo(a.op.x + sr * Math.sin(a.oangle + e.angle),
|
|
a.op.y + sr * Math.cos(a.oangle + e.angle));
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// draw items
|
|
ctx.strokeStyle = "rgb(0,0,0)";
|
|
for(var i=0,n=w.items.length;i<n;i++) {
|
|
var it = w.items[i];
|
|
if(it.type === 1) ctx.fillStyle = "rgb(255, 150, 150)";
|
|
if(it.type === 2) ctx.fillStyle = "rgb(150, 255, 150)";
|
|
ctx.beginPath();
|
|
ctx.arc(it.p.x, it.p.y, it.rad, 0, Math.PI*2, true);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
|
|
w.agents[0].brain.visSelf(document.getElementById('brain_info_div'));
|
|
}
|
|
|
|
// Tick the world
|
|
function tick() {
|
|
w.tick();
|
|
if(!skipdraw || w.clock % 50 === 0) {
|
|
draw();
|
|
draw_stats();
|
|
draw_net();
|
|
}
|
|
}
|
|
|
|
var simspeed = 2;
|
|
function goveryfast() {
|
|
window.clearInterval(current_interval_id);
|
|
current_interval_id = setInterval(tick, 0);
|
|
skipdraw = true;
|
|
simspeed = 3;
|
|
}
|
|
function gofast() {
|
|
window.clearInterval(current_interval_id);
|
|
current_interval_id = setInterval(tick, 0);
|
|
skipdraw = false;
|
|
simspeed = 2;
|
|
}
|
|
function gonormal() {
|
|
window.clearInterval(current_interval_id);
|
|
current_interval_id = setInterval(tick, 30);
|
|
skipdraw = false;
|
|
simspeed = 1;
|
|
}
|
|
function goslow() {
|
|
window.clearInterval(current_interval_id);
|
|
current_interval_id = setInterval(tick, 200);
|
|
skipdraw = false;
|
|
simspeed = 0;
|
|
}
|
|
|
|
function savenet() {
|
|
var j = w.agents[0].brain.value_net.toJSON();
|
|
var t = JSON.stringify(j);
|
|
document.getElementById('tt').value = t;
|
|
}
|
|
|
|
function loadnet() {
|
|
var t = document.getElementById('tt').value;
|
|
var j = JSON.parse(t);
|
|
w.agents[0].brain.value_net.fromJSON(j);
|
|
stoplearn(); // also stop learning
|
|
gonormal();
|
|
}
|
|
|
|
function startlearn() {
|
|
w.agents[0].brain.learning = true;
|
|
}
|
|
function stoplearn() {
|
|
w.agents[0].brain.learning = false;
|
|
}
|
|
|
|
function reload() {
|
|
w.agents = [new Agent()]; // this should simply work. I think... ;\
|
|
reward_graph = new cnnvis.Graph(); // reinit
|
|
}
|
|
|
|
var w; // global world object
|
|
var current_interval_id;
|
|
var skipdraw = false;
|
|
function start() {
|
|
canvas = document.getElementById("canvas");
|
|
ctx = canvas.getContext("2d");
|
|
|
|
w = new World();
|
|
w.agents = [new Agent()];
|
|
|
|
gofast();
|
|
} |