Flappy bird p5.js

i’m trying to make an ai version of flappy birds, but when i go to save the bird it cant find the saved score. it keeps saying undefined when i console.log.

sketch.js

var bird = [];
var saved = [];
var paused = false;
var space = 200;
var moveSpeed = 1;
var pipe = [];
var pipeX = 100;
var total = 500;
var highScore = 0;

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
  if (bird.length == 0){
    nextGen();
  }
  if (paused == false){
    for (var i = bird.length-1; i >= 0; i--){
      bird[i].move();
      bird[i].hit();
      bird[i].think();
      if (bird[i].alive == false){
        saved.push(bird.splice(i,1),[0]);
      }
    }
    for (var i = pipe.length-1; i >= 0; i--){
      pipe[i].move();
      if (pipe[i].destroy){
        for (var j = bird.length-1; j >= 0; j--){
          bird[j].score += 1;
        }
        pipe.splice(i,1);
      }
    }
  }
  for (var p of pipe){
    p.show();
  }
  for (var b of bird){
    b.show();
  }
  fill(0);
  textSize(20);
  text(highScore,5,20);
  if (bird.length > 0){
    text(bird[0].score,5,40);
  }
}

function keyPressed(){
  if (key == 'p'){
    if (paused == true){paused = false;}
    else if (paused == false){paused = true;}
  }
}

bird.js

class Bird{
  
  constructor(brain){
    this.x = 32;
    this.y = height/2;
    
    this.grav = 0.7;
    this.lift = -12;
    this.vel = 0;
    this.alive = true;
    this.score = 0;
    this.r = 8;
    if (brain){
      this.brain = brain.copy();
    } else {
      this.brain = new NeuralNetwork(4, 6, 2);
    }
  }
  
  mutate(){
    this.brain.mutate(0.1);
  }
  
  think(){
    
    let closest = null;
    let closestD = Infinity;
    for (let i = 0; i < pipe.length; i++){
      let d = pipe[i].x - this.x;
      if (d < closestD && d > 0){
        closest = pipe[i];
        closestD = d;
      }
    }
    
    let input = [];
    input[0] = this.y/height;
    input[1] = this.x/width;
    input[2] = closest.x/width;
    input[3] = (closest.top + (space/2))/height;
    let output = this.brain.predict(input);
    if (output[0] > output[1]){
      this.up();
    }
  }
  
  show(){
    if (this.alive == false){fill(255,0,0);}
    if(this.alive){fill(255,255,0);}
    ellipse(this.x,this.y,this.r*2,this.r*2);
  }
  
  hit(){
    this.alive = true;
    for (var i = pipe.length-1; i >= 0; i--){
      if (this.x > pipe[i].x && this.x < pipe[i].x + pipe[i].w){
        if (this.y < pipe[i].top || this.y > height - pipe[i].bottom){
          this.alive = false;
        }
      }
    }
  }
  
  up(){
    if (this.vel >= 0){
      this.vel += this.lift;
    }
  }
  
  move(){
    this.vel += this.grav;
    this.y += this.vel;
    if (this.y > height || this.y < 0){
      this.alive = false;
    }
  }
}

pipe.js

class Pipe{
  
  constructor(){
    this.x = 410;
    this.w = 50;
    this.top = random(25,height - 25 - space);
    this.bottom = height - this.top - space;
    this.destroy = false;
    this.create = false;
  }
  
  show(){
    fill(0,255,0);
    rect(this.x,0,this.w,this.top);
    rect(this.x,height - this.bottom,this.w,this.bottom);
  }
  
  move(){
    this.x -= moveSpeed;
    if (this.create == false && this.x < pipeX){
      pipe.push(new Pipe());
      this.create = true;
    }
    if (this.x < -this.w){
      this.destroy = true;
    }
  }
}

nn.js

class ActivationFunction {
  constructor(func, dfunc) {
    this.func = func;
    this.dfunc = dfunc;
  }
}

let sigmoid = new ActivationFunction(
  x => 1 / (1 + Math.exp(-x)),
  y => y * (1 - y)
);

let tanh = new ActivationFunction(
  x => Math.tanh(x),
  y => 1 - (y * y)
);


class NeuralNetwork {
  // TODO: document what a, b, c are
  constructor(a, b, c) {
    if (a instanceof NeuralNetwork) {
      this.input_nodes = a.input_nodes;
      this.hidden_nodes = a.hidden_nodes;
      this.output_nodes = a.output_nodes;

      this.weights_ih = a.weights_ih.copy();
      this.weights_ho = a.weights_ho.copy();

      this.bias_h = a.bias_h.copy();
      this.bias_o = a.bias_o.copy();
    } else {
      this.input_nodes = a;
      this.hidden_nodes = b;
      this.output_nodes = c;

      this.weights_ih = new Matrix(this.hidden_nodes, this.input_nodes);
      this.weights_ho = new Matrix(this.output_nodes, this.hidden_nodes);
      this.weights_ih.randomize();
      this.weights_ho.randomize();

      this.bias_h = new Matrix(this.hidden_nodes, 1);
      this.bias_o = new Matrix(this.output_nodes, 1);
      this.bias_h.randomize();
      this.bias_o.randomize();
    }

    // TODO: copy these as well
    this.setLearningRate();
    this.setActivationFunction();


  }

  predict(input_array) {

    // Generating the Hidden Outputs
    let inputs = Matrix.fromArray(input_array);
    let hidden = Matrix.multiply(this.weights_ih, inputs);
    hidden.add(this.bias_h);
    // activation function!
    hidden.map(this.activation_function.func);

    // Generating the output's output!
    let output = Matrix.multiply(this.weights_ho, hidden);
    output.add(this.bias_o);
    output.map(this.activation_function.func);

    // Sending back to the caller!
    return output.toArray();
  }

  setLearningRate(learning_rate = 0.1) {
    this.learning_rate = learning_rate;
  }

  setActivationFunction(func = sigmoid) {
    this.activation_function = func;
  }

  train(input_array, target_array) {
    // Generating the Hidden Outputs
    let inputs = Matrix.fromArray(input_array);
    let hidden = Matrix.multiply(this.weights_ih, inputs);
    hidden.add(this.bias_h);
    // activation function!
    hidden.map(this.activation_function.func);

    // Generating the output's output!
    let outputs = Matrix.multiply(this.weights_ho, hidden);
    outputs.add(this.bias_o);
    outputs.map(this.activation_function.func);

    // Convert array to matrix object
    let targets = Matrix.fromArray(target_array);

    // Calculate the error
    // ERROR = TARGETS - OUTPUTS
    let output_errors = Matrix.subtract(targets, outputs);

    // let gradient = outputs * (1 - outputs);
    // Calculate gradient
    let gradients = Matrix.map(outputs, this.activation_function.dfunc);
    gradients.multiply(output_errors);
    gradients.multiply(this.learning_rate);


    // Calculate deltas
    let hidden_T = Matrix.transpose(hidden);
    let weight_ho_deltas = Matrix.multiply(gradients, hidden_T);

    // Adjust the weights by deltas
    this.weights_ho.add(weight_ho_deltas);
    // Adjust the bias by its deltas (which is just the gradients)
    this.bias_o.add(gradients);

    // Calculate the hidden layer errors
    let who_t = Matrix.transpose(this.weights_ho);
    let hidden_errors = Matrix.multiply(who_t, output_errors);

    // Calculate hidden gradient
    let hidden_gradient = Matrix.map(hidden, this.activation_function.dfunc);
    hidden_gradient.multiply(hidden_errors);
    hidden_gradient.multiply(this.learning_rate);

    // Calcuate input->hidden deltas
    let inputs_T = Matrix.transpose(inputs);
    let weight_ih_deltas = Matrix.multiply(hidden_gradient, inputs_T);

    this.weights_ih.add(weight_ih_deltas);
    // Adjust the bias by its deltas (which is just the gradients)
    this.bias_h.add(hidden_gradient);

    // outputs.print();
    // targets.print();
    // error.print();
  }

  serialize() {
    return JSON.stringify(this);
  }

  static deserialize(data) {
    if (typeof data == 'string') {
      data = JSON.parse(data);
    }
    let nn = new NeuralNetwork(data.input_nodes, data.hidden_nodes, data.output_nodes);
    nn.weights_ih = Matrix.deserialize(data.weights_ih);
    nn.weights_ho = Matrix.deserialize(data.weights_ho);
    nn.bias_h = Matrix.deserialize(data.bias_h);
    nn.bias_o = Matrix.deserialize(data.bias_o);
    nn.learning_rate = data.learning_rate;
    return nn;
  }


  // Adding function for neuro-evolution
  copy() {
    return new NeuralNetwork(this);
  }

  mutate(rate) {
    function mutate(val) {
      if (Math.random() < rate) {
        // return 2 * Math.random() - 1;
        return val + randomGaussian(0, 0.1);
      } else {
        return val;
      }
    }
    this.weights_ih.map(mutate);
    this.weights_ho.map(mutate);
    this.bias_h.map(mutate);
    this.bias_o.map(mutate);
  }



}

matrix.js

class Matrix {
  constructor(rows, cols) {
    this.rows = rows;
    this.cols = cols;
    this.data = Array(this.rows).fill().map(() => Array(this.cols).fill(0));
  }

  copy() {
    let m = new Matrix(this.rows, this.cols);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.cols; j++) {
        m.data[i][j] = this.data[i][j];
      }
    }
    return m;
  }

  static fromArray(arr) {
    return new Matrix(arr.length, 1).map((e, i) => arr[i]);
  }

  static subtract(a, b) {
    if (a.rows !== b.rows || a.cols !== b.cols) {
      console.log('Columns and Rows of A must match Columns and Rows of B.');
      return;
    }

    // Return a new Matrix a-b
    return new Matrix(a.rows, a.cols)
      .map((_, i, j) => a.data[i][j] - b.data[i][j]);
  }

  toArray() {
    let arr = [];
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.cols; j++) {
        arr.push(this.data[i][j]);
      }
    }
    return arr;
  }

  randomize() {
    return this.map(e => Math.random() * 2 - 1);
  }

  add(n) {
    if (n instanceof Matrix) {
      if (this.rows !== n.rows || this.cols !== n.cols) {
        console.log('Columns and Rows of A must match Columns and Rows of B.');
        return;
      }
      return this.map((e, i, j) => e + n.data[i][j]);
    } else {
      return this.map(e => e + n);
    }
  }

  static transpose(matrix) {
    return new Matrix(matrix.cols, matrix.rows)
      .map((_, i, j) => matrix.data[j][i]);
  }

  static multiply(a, b) {
    // Matrix product
    if (a.cols !== b.rows) {
      console.log('Columns of A must match rows of B.')
      return;
    }

    return new Matrix(a.rows, b.cols)
      .map((e, i, j) => {
        // Dot product of values in col
        let sum = 0;
        for (let k = 0; k < a.cols; k++) {
          sum += a.data[i][k] * b.data[k][j];
        }
        return sum;
      });
  }

  multiply(n) {
    if (n instanceof Matrix) {
      if (this.rows !== n.rows || this.cols !== n.cols) {
        console.log('Columns and Rows of A must match Columns and Rows of B.');
        return;
      }

      // hadamard product
      return this.map((e, i, j) => e * n.data[i][j]);
    } else {
      // Scalar product
      return this.map(e => e * n);
    }
  }

  map(func) {
    // Apply a function to every element of matrix
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.cols; j++) {
        let val = this.data[i][j];
        this.data[i][j] = func(val, i, j);
      }
    }
    return this;
  }

  static map(matrix, func) {
    // Apply a function to every element of matrix
    return new Matrix(matrix.rows, matrix.cols)
      .map((e, i, j) => func(matrix.data[i][j], i, j));
  }

  print() {
    console.table(this.data);
    return this;
  }

  serialize() {
    return JSON.stringify(this);
  }

  static deserialize(data) {
    if (typeof data == 'string') {
      data = JSON.parse(data);
    }
    let matrix = new Matrix(data.rows, data.cols);
    matrix.data = data.data;
    return matrix;
  }
}

if (typeof module !== 'undefined') {
  module.exports = Matrix;
}

ga.js

function nextGen(){
  var bb = [5];// 5 best saved birds
  var bs = [5];// 5 best scores
  for (let i = 0; i < bb.length; i++){// reset all score and birds to 0
    bb[i] = 0;
    bs[i] = 0;
  }
  pipe = [];// clear all pipes
  findBest();// find the top best 5
  for (let i = 0; i < total; i++){
    bird[i] = PickOne();// pick a bird surprisely works fine
  }
  pipe.push(new Pipe());// add pipe
  saved = [];// clear saved
}

function PickOne(){
  let index = random(4);// pick random from best
  let bird = saved[index];// get saved bird
  let child = new Bird(bird.brain);// new bird with that bird brain
  child.mutate();// mutate the new bird
  return child;// return the bird
}

function findBest(){
  for (var i = 0; i < saved.length; i++){// loop through all saved bird
    var sc = saved[i].score;// get score from saved
    console.log(sc);// console log score
    if (sc > bs[0]){// check if score > 0
      bb[4] = bb[3];
      bb[3] = bb[2];
      bb[2] = bb[1];
      bb[1] = bb[0];
      bb[0] = i;
      bs[4] = bs[3];
      bs[3] = bs[2];
      bs[2] = bs[1];
      bs[1] = bs[0];
      bs[0] = sc;
      if (bs[0] > highScore){highScore = bs[0];}// set highScore
    }
    else if (sc > bs[1]){
      bb[4] = bb[3];
      bb[3] = bb[2];
      bb[2] = bb[1];
      bb[1] = i;
      bs[4] = bs[3];
      bs[3] = bs[2];
      bs[2] = bs[1];
      bs[1] = sc;
    }
    else if (sc > bs[2]){
      bb[4] = bb[3];
      bb[3] = bb[2];
      bb[2] = i;
      bs[4] = bs[3];
      bs[3] = bs[2];
      bs[2] = sc;
    }
    else if (sc > bs[3]){
      bb[4] = bb[3];
      bb[3] = i;
      bs[4] = bs[3];
      bs[3] = sc;
    }
    else if (sc > bs[4]){
      bb[4] = i;
      bs[4] = sc;
    }
  }
}

In the nn.js file, you have a definition for class NeuralNetwork. Within that definition, you define a static deserialize(data) method that includes this statement:

    let nn = new NeuralNetwork(data.input_nodes, data.hidden_nodes, data.output_nodes);

So, whenever you call that method from a NeuralNetwork instance, you are creating an additional NeuralNetwork instance. Is that what you are intending to do? If so, please explain the purpose of doing this.

1 Like

I pulled the nn and matrix from a tutorial.
here the video I copied the code from

Thanks for the link to the videos. There’s a lot of content there, and I was unable to find an explanation regarding the deserialize method. However, because the method is static, and therefore is independent of any particular instance of NeuralNetwork, there isn’t a problem of a proliferation of multiple new instances of NeuralNetwork being created when deserialization is performed.

Unfortunately, the above does not help with your original issue of an undefined saved score.

1 Like

Is the error occurring with this statement in the ga.js file?:

    console.log(sc);// console log score

If so, then the reason why sc is undefined might be here in the pickOne function:

  let index = random(4);// pick random from best
  let bird = saved[index];// get saved bird

The random function is returning a floating point number that is not suitable as an index. You will need to truncate it to an integer, like this

  let index = floor(random(5));// pick random from best
2 Likes

There are many other truncate options in JS btW. Check these 2 examples using JS operators:

  1. const index = ~~random(4);
  2. const index = random(4) | 0;
2 Likes

Be sure to take into account that if there are 5 saved birds, you need to choose among indexes 0, 1, 2, 3, and 4. So in that case, you need to pass 5 to random, because the argument is exclusive. Then truncate the result however you choose to do it.

See random().

2 Likes

And given saved[] is an array, it’s shorter & safer to simply use it as random()'s argument:
const bird = random(saved);

2 Likes

Some very old Flappy Bird minimalist code: :baby_chick:
Flappy Bird

There’s also this game sketch. Not Flappy Bird but some concepts are similar: :beaver:
Hoppy Beaver

1 Like

Thanks ill try all your advice out and report back

1 Like

it says cant find property of undefined brain

function nextGen(){
  var bb = [5];// 5 best saved birds
  var bs = [5];// 5 best scores
  for (let i = 0; i < bb.length; i++){// reset all score and birds to 0
    bb[i] = 0;
    bs[i] = 0;
  }
  pipe = [];// clear all pipes
  findBest();// find the top best 5
  for (let i = 0; i < total; i++){
    bird[i] = PickOne();// pick a bird surprisely works fine
  }
  pipe.push(new Pipe());// add pipe
  saved = [];// clear saved
}

function PickOne(){
  let index = floor(random(bb.length));// pick random from best
  let bird = saved[index];// get saved bird
  let child = new Bird(bird.brain);// new bird with that bird brain
  child.mutate();// mutate the new bird
  return child;// return the bird
}

function findBest(){
  for (var i = 0; i < saved.length; i++){// loop through all saved bird
    var sc = saved[i].score;// get score from saved
    console.log(sc);// console log score
    if (sc > bs[0]){// check if score > 0
      bb[4] = bb[3];
      bb[3] = bb[2];
      bb[2] = bb[1];
      bb[1] = bb[0];
      bb[0] = i;
      bs[4] = bs[3];
      bs[3] = bs[2];
      bs[2] = bs[1];
      bs[1] = bs[0];
      bs[0] = sc;
      if (bs[0] > highScore){highScore = bs[0];}// set highScore
    }
    else if (sc > bs[1]){
      bb[4] = bb[3];
      bb[3] = bb[2];
      bb[2] = bb[1];
      bb[1] = i;
      bs[4] = bs[3];
      bs[3] = bs[2];
      bs[2] = bs[1];
      bs[1] = sc;
    }
    else if (sc > bs[2]){
      bb[4] = bb[3];
      bb[3] = bb[2];
      bb[2] = i;
      bs[4] = bs[3];
      bs[3] = bs[2];
      bs[2] = sc;
    }
    else if (sc > bs[3]){
      bb[4] = bb[3];
      bb[3] = i;
      bs[4] = bs[3];
      bs[3] = sc;
    }
    else if (sc > bs[4]){
      bb[4] = i;
      bs[4] = sc;
    }
  }
}

These statements leave bb and bs with only one element each, namely the number 5.

You could do this:

  var bb = Array(5); // 5 best saved birds
  var bs = Array(5); // 5 best scores

so var bb = Array(5);
gives me 5 things?
on the video later on he used to get 5 items and he give them each numbers
bb[0], bb[1] ect.

After instantiating an array via its constructor Array(), immediately invoke its method fill():

const bb = Array(5).fill(), bs = Array(5).fill();

The reason to do so is b/c arrays created via Array() are sparse, w/ 100% empty slots:

Method fill() will populate the whole array, eliminating any empty slots there:

And depending on the browser’s engine, it might even internally upgrade a sparse array to its solid version, which is slightly more performant!

And btW, if we know an array will store number values only and it won’t change its length later, it’s preferred to make it a typed array instead:

const bb = new Uint16Array(5), bs = new Uint16Array(5);
1 Like

if int bb = is wrong. why did it work in bird under think?

I guess you’ve meant const, let or var instead of int, right?

Apart from that, creating an array via [] is the most common way to do it in JS.

However, keep in mind that just [] is initially an empty array w/ length = 0.

1 Like

yes i meant let or var. sry i do js and java and confuse them when in hurry. but still i’m confused why it worked in bird but not in ga

Array creation style like const arr = []; is more appropriate for push() populate.

While const NUM = 10, arr = Array(NUM).fill(); style is for length fixed arrays
and populate using this for (var i = 0; i < NUM; ++i) arr[i] = new ...; template.

1 Like