// LL 2021
// Based on https://sagejenson.com/physarum

const particles = 500; // min=1 max=5000 step=1
const grid_size = 300; // min=20 max=1000 step=1
const iterations = 2000; // min=0 max=100000 step=1
const follow_trails = 1; // min=0 max=1 step=1 (No,Yes)

const formation = 0; // min=0 max=3 step=1 (Random,Grid,Circle,Clover)

const draw_on_turn = 0; // min=0 max=1 step=1 (No,Yes)

const diffuse = 1; // min=0 max=1 step=1 (No,Yes)

const decay = 0.99; // min=0.0 max=1 step=0.01

const seed=0; // min=0 max=100 step=1

const opacity = -0.4; // min=-1 max=1 step=0.001
Canvas.setpenopacity(Math.sign(opacity) * Math.abs(opacity**4));

const grid = Array.from({length: grid_size * grid_size});
const grid2 = Array.from({length: grid_size * grid_size});
const particle_list = Array.from({length: particles});

const EPS = 0.001;

const canvas_size = 200;

const travel_distance = grid_size / canvas_size / 20;
const sensor_distance = grid_size / canvas_size / 0.125;
const travel_angle = Math.PI / 12;
const sensor_angle = Math.PI / 4;

const turtle = new Turtle();
var rng;

function walk(i, t) {
    if (i==0) {
        rng = new RNG(seed);
        init_grid();
        init_particles();
    }
    
    particle_list.forEach(p => p.draw());
    update_grid();
    particle_list.forEach(p => p.move());
    particle_list.forEach(p => p.deposit());
    
    const iterations_t = iterations * t;
    return i+1 < iterations_t;
}

function init_particles() {
    const r = (formation == 2) ? 0.4 : 0.8;
    for (var i=0; i<particles; i++) {
        var x = random() * canvas_size - canvas_size / 2;
        var y = random() * canvas_size - canvas_size / 2;
        var a = random() * Math.PI * 2;

        if (formation == 1) {
            x = Math.round(x / 40) * 40;
            y = Math.round(y / 40) * 40;
        } else if (formation >= 2) {
            const len = Math.hypot(x, y);
            if (len > 0.01) {
                x = x / len * canvas_size * (r + random() * 0.1);
                y = y / len * canvas_size * (r + random() * 0.1);
            }
        }

        particle_list[i] = new Particle(x, y, a);
    }
}

function init_grid() {
    for (var j=0; j<grid_size; j++) {
        for (var i=0; i<grid_size; i++) {
            var value = 0;
            // if ((i % 50) == 0) value = 100;
            // if ((j % 50) == 0) value = 100;
            grid[j*grid_size+i] = value;
        }
    }
}

function update_grid() {
    if (!follow_trails) return;
    
    // Diffuse
    for (var j=0; j<grid_size; j++) {
        for (var i=0; i<grid_size; i++) {
            var value = 0, count = 0;
            for (var dy=-diffuse; dy<=diffuse; dy+=1) {
                for (var dx=-diffuse; dx<=diffuse; dx+=1) {
                    const factor = ((Math.abs(dx) + Math.abs(dy)) < EPS) ? 10 : 1;
                    const px = (i + dx + grid_size) % grid_size;
                    const py = (j + dy + grid_size) % grid_size;
                    value += grid[px + py * grid_size] * factor;
                    count += factor;
                }
                grid2[i + j * grid_size] = value / count;
            }
        }
    }
    
    // Decay
    for (var gi=0; gi<grid.length; gi++) {
        var value = grid2[gi] * decay;
        if (value < 0.01) value = 0;
        grid[gi] = value;
    }
}

class Particle {
    constructor(x, y, a) {
        this.x = x;
        this.y = y;
        this.dir = a;
        this.need_to_draw = false;
    }
    
    move() {
        if (follow_trails) {
            const values = [0, 0, 0];
            const factors = [-1, 0, 1];
            factors.forEach((f, id) => {
                const x = this.x + sensor_distance * Math.cos(this.dir + f * sensor_angle);
                const y = this.y + sensor_distance * Math.sin(this.dir + f * sensor_angle);
                const dx = ((x / canvas_size + 0.5) * grid_size) | 0;
                const dy = ((y / canvas_size + 0.5) * grid_size) | 0;
                const i = dx + dy * grid_size;
                if (i >= 0 && i < grid_size*grid_size) {
                    values[id] = grid[i];
                }
            });
            
            this.need_to_draw = false;
            if (values[1] > values[0]+EPS && values[1] > values[2]+EPS) {
                // No change
            } else if (Math.abs(values[0] - values[1]) < EPS) {
                // Random turn
                this.dir += travel_angle * (random() * 2 - 1);
            }
            else {
                // Turn towards stronger scent
                this.dir += Math.sign(values[2] - values[0]) * travel_angle;
                this.need_to_draw = true;
            }
        } else {
            this.dir += travel_angle * (random() * 2 - 1);
        }

        this.x += travel_distance * Math.cos(this.dir);
        this.y += travel_distance * Math.sin(this.dir);
        
        if (Math.abs(this.x) > canvas_size / 2) {
            this.x = (this.x + canvas_size * 1.5) % canvas_size - canvas_size / 2;
        }
        if (Math.abs(this.y) > canvas_size / 2) {
            this.y = (this.y + canvas_size * 1.5) % canvas_size - canvas_size / 2;
        }
    }
    
    deposit() {
        const dx = ((this.x / canvas_size + 0.5) * grid_size) | 0;
        const dy = ((this.y / canvas_size + 0.5) * grid_size) | 0;
        const i = dx + dy * grid_size;
        if (i >= 0 && i < grid_size*grid_size) {
            grid[i] += 1;
        }
    }
    
    draw() {
        if (draw_on_turn && !this.need_to_draw) return;
        
        const r = canvas_size / 1000;
        const step = 8;
        turtle.jump(this.x + r, this.y);
        for (var a=1/step; a<1+0.5/step; a+=1/step) {
            turtle.goto(
                this.x + r * Math.cos(a * Math.PI * 2),
                this.y + r * Math.sin(a * Math.PI * 2)
            );
        }
    }
}

//function random() { return Math.random(); }
function random() { return rng.nextFloat(); }

// Minified Random Number Generator from https://turtletoy.net/turtle/ab7a7e539e
function RNG(t){return new class{constructor(t){this.m=2147483648,this.a=1103515245,this.c=12345,this.state=t||Math.floor(Math.random()*(this.m-1))}nextFloat(){return this.state=(this.a*this.state+this.c)%this.m,this.state/(this.m-1)}}(t)}