Browse Source

Initial commit.

simondevyoutube 5 năm trước cách đây
mục cha
commit
c49cb2b0de
9 tập tin đã thay đổi với 765 bổ sung0 xóa
  1. BIN
      assets/bird-colour.png
  2. BIN
      assets/bird.png
  3. BIN
      assets/pipe.png
  4. BIN
      assets/sky.png
  5. 74 0
      ffnet.js
  6. 18 0
      index.html
  7. 525 0
      main.js
  8. 24 0
      math.js
  9. 124 0
      population.js

BIN
assets/bird-colour.png


BIN
assets/bird.png


BIN
assets/pipe.png


BIN
assets/sky.png


+ 74 - 0
ffnet.js

@@ -0,0 +1,74 @@
+export const ffnet = (function() {
+
+  function dot(a, b) {
+    let r = 0;
+    for (let i = 0; i < a.length; i++) {
+      r += a[i] * b[i];
+    }
+    return r;
+  }
+
+  function add(a, b) {
+    return a.map((v1, i) => v1 + b[i]);
+  }
+
+
+  return {
+    sigmoid: function (z) {
+      return z.map(v => 1.0 / (1.0 + Math.exp(-v)));
+    },
+
+    relu: function (z) {
+      return z.map(v => Math.max(v, 0));
+    },
+
+    FFNeuralNetwork: class {
+      constructor(shapes) {
+        function _InitRandomArray(sz) {
+          return [...Array(sz)].map(_ => Math.random() * 2 - 1);
+        }
+
+        this._shapes = shapes;
+        this._biases = shapes.slice(1).map(x => _InitRandomArray(x.size));
+        this._weights = [];
+        for (let i = 1; i < shapes.length; i++) {
+          this._weights.push(
+              [...Array(shapes[i].size)].map(_=>_InitRandomArray(shapes[i-1].size)));
+        }
+      }
+
+      predict(inputs) {
+        let X = inputs;
+        for (let i = 0; i < this._weights.length; i++) {
+          const layer_weights = this._weights[i];
+          const layer_bias = this._biases[i];
+          // z = wx + b
+          const z = add(layer_weights.map(w => dot(X, w)), layer_bias);
+          // a = σ(z)
+          const a = this._shapes[i+1].activation(z);
+          // The output from the layer becomes the input to the next.
+          X = a;
+        }
+        return X;
+      }
+
+      toArray() {
+        return [...this._biases.flat()].concat(
+            [...this._weights.flat().flat()]);
+      }
+
+      fromArray(values) {
+        const arr = [...values];
+        let i = 0;
+        for (let b of this._biases) {
+          b.splice(0, b.length, ...arr.splice(0, b.length));
+        }
+        for (let w of this._weights) {
+          for (let w1 of w) {
+            w1.splice(0, w1.length, ...arr.splice(0, w1.length));
+          }
+        }
+      }
+    }
+  };
+})();

+ 18 - 0
index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+      <script src="https://cdn.jsdelivr.net/npm/phaser/dist/phaser.js"></script>
+      <script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]/dist/tf.min.js"></script>
+  </head>
+  <body>
+    <style>
+        body {
+          overflow: hidden;
+          padding: 0px;
+          margin: 0px;
+        }
+    </style>
+
+    <script src="main.js" type="module"></script>
+  </body>
+</html>

+ 525 - 0
main.js

@@ -0,0 +1,525 @@
+import {ffnet} from "./ffnet.js";
+import {population} from "./population.js"
+
+const _GRAVITY = 900;
+const _TERMINAL_VELOCITY = 400;
+const _MAX_UPWARDS_VELOCITY = -300;
+const _UPWARDS_ACCELERATION = -450;
+const _PIPE_SPACING_Y = 100;
+const _PIPE_SPACING_X = 200;
+const _TREADMILL_SPEED = -125;
+
+const _CONFIG_WIDTH = 960;
+const _CONFIG_HEIGHT = 540;
+const _GROUND_Y = _CONFIG_HEIGHT;
+const _BIRD_POS_X = 50;
+
+
+class PipePairObject {
+  constructor(scene, x) {
+    const height = _CONFIG_HEIGHT * (0.25 + 0.5 * Math.random());
+    this._sprite1 = scene.add.sprite(x, height + _PIPE_SPACING_Y * 0.5, 'pipe');
+    this._sprite1.displayOriginX = 0;
+    this._sprite1.displayOriginY = 0;
+
+    this._sprite2 = scene.add.sprite(x, height - _PIPE_SPACING_Y * 0.5, 'pipe');
+    this._sprite2.displayOriginX = 0;
+    this._sprite2.displayOriginY = 0;
+    this._sprite2.displayHeight = -1 * this._sprite2.height;
+  }
+
+  Destroy() {
+    this._sprite1.destroy();
+    this._sprite2.destroy();
+  }
+
+  Update(timeElapsed) {
+    this._sprite1.x += timeElapsed * _TREADMILL_SPEED;
+    this._sprite2.x += timeElapsed * _TREADMILL_SPEED;
+  }
+
+  Intersects(aabb) {
+    const b1 = this._sprite1.getBounds();
+    const b2 = this._sprite2.getBounds();
+    b2.y -= this._sprite2.height;
+    return (
+        Phaser.Geom.Intersects.RectangleToRectangle(b1, aabb) ||
+        Phaser.Geom.Intersects.RectangleToRectangle(b2, aabb));
+  }
+
+  Reset(x) {
+    const height = _CONFIG_HEIGHT * (0.25 + 0.5 * Math.random());
+    this._sprite1.x = x;
+    this._sprite1.y = height + _PIPE_SPACING_Y * 0.5;
+    this._sprite2.x = x;
+    this._sprite2.y = height - _PIPE_SPACING_Y * 0.5;
+  }
+
+  get X() {
+    return this._sprite1.x;
+  }
+
+  get Width() {
+    return this._sprite1.width;
+  }
+}
+
+class FlappyBirdObject {
+  constructor(scene) {
+    this._scene = scene;
+    this._sprite = scene.add.sprite(_BIRD_POS_X, 100, 'bird');
+    this._spriteTint = scene.add.sprite(_BIRD_POS_X, 100, 'bird-colour');
+    this._velocity = 0;
+    this._dead = false;
+  }
+
+  Destroy() {
+    this._sprite.destroy();
+  }
+
+  Update(params) {
+    if (this._dead) {
+      return;
+    }
+
+    this._ApplyGravity(params.timeElapsed)
+    this._velocity = Math.min(Math.max(
+        this._velocity, _MAX_UPWARDS_VELOCITY), _TERMINAL_VELOCITY);
+    this._sprite.y += this._velocity * params.timeElapsed;
+    this._spriteTint.y += this._velocity * params.timeElapsed;
+
+    const v = new Phaser.Math.Vector2(
+        -1 * _TREADMILL_SPEED * params.timeElapsed, 0);
+    v.add(new Phaser.Math.Vector2(0, this._velocity));
+    v.normalize();
+
+    const rad = Math.atan2(v.y, v.x);
+    const deg = (180.0 / Math.PI) * rad;
+
+    this._sprite.angle = deg * 0.75;
+    this._spriteTint.angle = deg * 0.75;
+  }
+
+  get Dead() {
+    return this._dead;
+  }
+
+  set Dead(d) {
+    this._dead = d;
+
+    this._scene.tweens.add({
+        targets: this._sprite,
+        props: {
+            alpha: { value: 0.0, duration: 500, ease: 'Sine.easeInOut' },
+        },
+    });
+    this._scene.tweens.add({
+        targets: this._spriteTint,
+        props: {
+            alpha: { value: 0.0, duration: 500, ease: 'Sine.easeInOut' },
+        },
+    });
+  }
+
+  set Alpha(a) {
+    this._sprite.alpha = a;
+    this._spriteTint.alpha = a;
+  }
+
+  get Bounds() {
+    return this._sprite.getBounds();
+  }
+
+  _ApplyGravity(timeElapsed) {
+    this._velocity += _GRAVITY * timeElapsed;
+  }
+}
+
+class FlappyBird_Manual extends FlappyBirdObject {
+  constructor(scene) {
+    super(scene);
+
+    this._frameInputs = [];
+  }
+
+  Update(params) {
+    this._HandleInput(params);
+
+    super.Update(params);
+  }
+
+  _HandleInput(params) {
+    if (!params.keys.up) {
+      return;
+    }
+
+    this._velocity += _UPWARDS_ACCELERATION;
+  }
+}
+
+class FlappyBird_NeuralNet extends FlappyBirdObject {
+  constructor(scene, populationEntity, params) {
+    super(scene);
+
+    this._model = new ffnet.FFNeuralNetwork(params.shapes);
+    this._model.fromArray(populationEntity.genotype);
+    this._populationEntity = populationEntity;
+    this._spriteTint.setTint(params.tint);
+  }
+
+  Update(params) {
+    function _PipeParams(bird, pipe) {
+      const distToPipe = (
+          (pipe.X + pipe.Width) - bird.Bounds.left) / _CONFIG_WIDTH;
+      const distToPipeB = (
+          (pipe._sprite1.y - bird.Bounds.bottom) / _CONFIG_HEIGHT) * 0.5 + 0.5;
+      const distToPipeT = (
+          (pipe._sprite2.y - bird.Bounds.top) / _CONFIG_HEIGHT) * 0.5 + 0.5;
+      return [distToPipe, distToPipeB, distToPipeT];
+    }
+
+    function _Params(bird, pipes) {
+      const inputs = pipes.map(p => _PipeParams(bird, p)).flat();
+
+      inputs.push((bird._velocity / _GRAVITY) * 0.5 + 0.5);
+
+      return inputs;
+    }
+
+    const inputs = _Params(this, params.nearestPipes);
+    const decision = this._model.predict(inputs);
+
+    if (decision > 0.5) {
+      this._velocity += _UPWARDS_ACCELERATION;
+    }
+
+    super.Update(params);
+
+    if (!this.Dead) {
+      this._populationEntity.fitness += params.timeElapsed;
+    }
+  }
+}
+
+class FlappyBirdGame {
+  constructor() {
+    this._game = this._CreateGame();
+    this._previousFrame = null;
+    this._gameOver = true;
+
+    this._statsText1 = null;
+    this._statsText2 = null;
+    this._gameOverText = null;
+    this._pipes = [];
+    this._birds = [];
+
+    this._InitPopulations();
+  }
+
+  _InitPopulations() {
+    const NN_DEF1 = [
+        {size: 7},
+        {size: 5, activation: ffnet.relu},
+        {size: 1, activation: ffnet.sigmoid}
+    ];
+
+    const NN_DEF2 = [
+        {size: 7},
+        {size: 12, activation: ffnet.relu},
+        {size: 1, activation: ffnet.sigmoid}
+    ];
+
+    const NN_DEF3 = [
+        {size: 7},
+        {size: 8, activation: ffnet.relu},
+        {size: 8, activation: ffnet.relu},
+        {size: 1, activation: ffnet.sigmoid}
+    ];
+
+    this._populations = [
+      this._CreatePopulation(64, NN_DEF1, 0xFF0000),
+      this._CreatePopulation(64, NN_DEF2, 0x0000FF),
+      this._CreatePopulation(64, NN_DEF3, 0x00FF00),
+    ];
+  }
+
+  _CreatePopulation(sz, shapes, colour) {
+    const t = new ffnet.FFNeuralNetwork(shapes);
+
+    const params = {
+      population_size: sz,
+      genotype: {
+        size: t.toArray().length,
+      },
+      mutation: {
+        magnitude: 0.5,
+        odds: 0.1,
+        decay: 0,
+      },
+      breed: {
+        selectionCutoff: 0.2,
+        immortalityCutoff: 0.05,
+        childrenPercentage: 0.5,
+      },
+      shapes: shapes,
+      tint: colour,
+    };
+
+    return new population.Population(params);
+  }
+
+  _Destroy() {
+    for (let b of this._birds) {
+      b.Destroy();
+    }
+    for (let p of this._pipes) {
+      p.Destroy();
+    }
+    this._statsText1.destroy();
+    this._statsText2.destroy();
+    if (this._gameOverText !== null) {
+      this._gameOverText.destroy();
+    }
+    this._birds = [];
+    this._pipes = [];
+    this._previousFrame = null;
+  }
+
+  _Init() {
+    for (let i = 0; i < 5; i+=1) {
+      this._pipes.push(
+          new PipePairObject(this._scene, 500 + i * _PIPE_SPACING_X));
+    }
+
+    this._gameOver = false;
+    this._stats = {
+      alive: 0,
+      score: 0,
+    };
+
+    const style = {
+      font: "40px Roboto",
+      fill: "#FFFFFF",
+      align: "right",
+      fixedWidth: 210,
+      shadow: {
+        offsetX: 2,
+        offsetY: 2,
+        color: "#000",
+        blur: 2,
+        fill: true
+      }
+    };
+    this._statsText1 = this._scene.add.text(0, 0, '', style);
+
+    style.align = 'left';
+    this._statsText2 = this._scene.add.text(
+        this._statsText1.width + 10, 0, '', style);
+
+    this._birds = [];
+    for (let curPop of this._populations) {
+      curPop.Step();
+
+      this._birds.push(...curPop._population.map(
+          p => new FlappyBird_NeuralNet(
+              this._scene, p, curPop._params)));
+    }
+  }
+
+  _CreateGame() {
+    const self = this;
+    const config = {
+        type: Phaser.AUTO,
+        scene: {
+            preload: function() { self._OnPreload(this); },
+            create: function() { self._OnCreate(this); },
+            update: function() { self._OnUpdate(this); },
+        },
+        scale: {
+          mode: Phaser.Scale.FIT,
+          autoCenter: Phaser.Scale.CENTER_BOTH,
+          width: _CONFIG_WIDTH,
+          height: _CONFIG_HEIGHT
+        }
+    };
+
+    return new Phaser.Game(config);
+  }
+
+  _OnPreload(scene) {
+    this._scene = scene;
+    this._scene.load.image('sky', 'assets/sky.png');
+    this._scene.load.image('bird', 'assets/bird.png');
+    this._scene.load.image('bird-colour', 'assets/bird-colour.png');
+    this._scene.load.image('pipe', 'assets/pipe.png');
+  }
+
+  _OnCreate(scene) {
+    const s = this._scene.add.image(0, 0, 'sky');
+    s.displayOriginX = 0;
+    s.displayOriginY = 0;
+    s.displayWidth = _CONFIG_WIDTH;
+    s.displayHeight = _CONFIG_HEIGHT;
+
+    this._keys = {
+      up: this._scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP),
+      f: this._scene.input.keyboard.addKey('F'),
+      r: this._scene.input.keyboard.addKey('R'),
+    }
+
+    this._keys.f.on('down', function () {
+      if (this._scene.scale.isFullscreen) {
+        this._scene.scale.stopFullscreen();
+      } else {
+        this._scene.scale.startFullscreen();
+      }
+    }, this);
+
+    this._keys.r.on('down', function () {
+      this._Destroy();
+      this._Init();
+    }, this);
+
+    this._Init();
+  }
+
+  _OnUpdate(scene) {
+    if (this._gameOver) {
+      this._DrawStats();
+      return;
+    }
+
+    const currentFrame = scene.time.now;
+    if (this._previousFrame == null) {
+      this._previousFrame = currentFrame;
+    }
+
+    const timeElapsedInS = Math.min(
+        (currentFrame - this._previousFrame) / 1000.0, 1.0 / 30.0);
+
+    this._UpdateBirds(timeElapsedInS);
+    this._UpdatePipes(timeElapsedInS);
+    this._CheckGameOver();
+    this._DrawStats();
+
+    this._previousFrame = currentFrame;
+  }
+
+  _CheckGameOver() {
+    const results = this._birds.map(b => this._IsBirdOutOfBounds(b));
+
+    this._stats.alive = results.reduce((t, r) => (r ? t: t + 1), 0);
+
+    if (results.every(b => b)) {
+      this._GameOver();
+    }
+  }
+
+  _IsBirdOutOfBounds(bird) {
+    const birdAABB = bird.Bounds;
+    birdAABB.top += 10;
+    birdAABB.bottom -= 10;
+    birdAABB.left += 10;
+    birdAABB.right -= 10;
+
+    if (bird.Dead) {
+      return true;
+    }
+
+    if (birdAABB.bottom >= _GROUND_Y || birdAABB.top <= 0) {
+      bird.Dead = true;
+      return true;
+    }
+
+    for (const p of this._pipes) {
+      if (p.Intersects(birdAABB)) {
+        bird.Dead = true;
+        return true;
+      }
+    }
+    return false;
+  }
+
+  _GetNearestPipes() {
+    let index = 0;
+    if (this._pipes[0].X + this._pipes[0].Width <= _BIRD_POS_X) {
+      index = 1;
+    }
+    return this._pipes.slice(index, 2);
+  }
+
+  _UpdateBirds(timeElapsed) {
+    const params = {
+        timeElapsed: timeElapsed,
+        keys: {up: Phaser.Input.Keyboard.JustDown(this._keys.up)},
+        nearestPipes: this._GetNearestPipes(),
+    };
+
+    for (let b of this._birds) {
+      b.Update(params);
+    }
+  }
+
+  _UpdatePipes(timeElapsed) {
+    const oldPipeX = this._pipes[0].X + this._pipes[0].Width;
+
+    for (const p of this._pipes) {
+      p.Update(timeElapsed);
+    }
+
+    const newPipeX = this._pipes[0].X + this._pipes[0].Width;
+
+    if (oldPipeX > _BIRD_POS_X && newPipeX <= _BIRD_POS_X) {
+      this._stats.score += 1;
+    }
+
+    if ((this._pipes[0].X + this._pipes[0].Width) <= 0) {
+      const p = this._pipes.shift();
+      p.Reset(this._pipes[this._pipes.length - 1].X + _PIPE_SPACING_X);
+      this._pipes.push(p);
+    }
+  }
+
+  _GameOver() {
+    const text = "GAME OVER";
+    const style = {
+      font: "100px Roboto",
+      fill: "#FFFFFF",
+      align: "center",
+      fixedWidth: _CONFIG_WIDTH,
+      shadow: {
+        offsetX: 2,
+        offsetY: 2,
+        color: "#000",
+        blur: 2,
+        fill: true
+      }
+    };
+
+    this._gameOverText = this._scene.add.text(
+        0, _CONFIG_HEIGHT * 0.25, text, style);
+    this._gameOver = true;
+
+    setTimeout(() => {
+      this._Destroy();
+      this._Init();
+    }, 2000);
+  }
+
+  _DrawStats() {
+    function _Line(t, s) {
+      return t + ': ' + s + '\n';
+    }
+
+    const text1 = 'Generation:\n' + 'Score:\n' + 'Alive:\n';
+    this._statsText1.text = text1;
+
+    const text2 = (
+        this._populations[0]._generations + '\n' +
+        this._stats.score + '\n' +
+        this._stats.alive + '\n');
+    this._statsText2.text = text2;
+  }
+}
+
+const _GAME = new FlappyBirdGame();

+ 24 - 0
math.js

@@ -0,0 +1,24 @@
+export const math = (function() {
+  return {
+    rand_range: function (a, b) {
+      return Math.random() * (b - a) + a;
+    },
+
+    rand_normalish: function () {
+      const r = Math.random() + Math.random() + Math.random() + Math.random();
+      return (r / 4.0) * 2.0 - 1;
+    },
+
+    lerp: function (x, a, b) {
+      return x * (b - a) + a;
+    },
+
+    clamp: function (x, a, b) {
+      return Math.min(Math.max(x, a), b);
+    },
+
+    sat: function (x) {
+      return Math.min(Math.max(x, 0.0), 1.0);
+    },
+  };
+})();

+ 124 - 0
population.js

@@ -0,0 +1,124 @@
+import {math} from "./math.js";
+
+export const population = (function() {
+
+  return {
+    Population: class {
+      constructor(params) {
+        this._params = params;
+        this._population = [...Array(this._params.population_size)].map(
+            _ => ({fitness: 1, genotype: this._CreateRandomGenotype()}));
+        this._lastGeneration = null;
+        this._generations = 0;
+      }
+
+      _CreateRandomGenotype() {
+        return [...Array(this._params.genotype.size)].map(
+            _ => Math.random() * 2 - 1);
+      }
+
+      Fittest() {
+        return this._lastGeneration.parents[0];
+      }
+
+      Step(tgtImgData) {
+        const parents = this._population.sort(
+            (a, b) => (b.fitness - a.fitness));
+
+        this._lastGeneration = {parents: parents};
+        this._generations += 1;
+
+        this._population = this._BreedNewPopulation(parents);
+      }
+
+      _BreedNewPopulation(parents) {
+        function _RouletteSelection(sortedParents, totalFitness) {
+          const roll = Math.random() * totalFitness;
+          let sum = 0;
+          for (let p of sortedParents) {
+            sum += p.fitness;
+            if (roll < sum) {
+              return p;
+            }
+          }
+          return sortedParents[sortedParents.length - 1];
+        }
+
+        function _RandomParent(sortedParents, otherParent, totalFitness) {
+          const p = _RouletteSelection(sortedParents, totalFitness);
+          return p;
+        }
+
+        function _CopyGenotype(g) {
+          return ({
+              fitness: g.fitness,
+              genotype: [...g.genotype],
+          });
+        }
+
+        const newPopulation = [];
+        const totalFitness = parents.reduce((t, p) => t + p.fitness, 0);
+        const numChildren = Math.ceil(
+            parents.length * this._params.breed.childrenPercentage);
+
+        const top = [...parents.slice(0, Math.ceil(
+            parents.length * this._params.breed.selectionCutoff))];
+        for (let j = 0; j < numChildren; j++) {
+          const i = j % top.length;
+          const p1 = top[i];
+          const p2 = _RandomParent(parents, p1, totalFitness);
+
+          // const g = [];
+          // for (let r = 0; r < p1.genotype.length; r++ ) {
+          //   const roll = Math.random();
+          //   g.push(roll < 0.5 ? p1.genotype[r] : p2.genotype[r]);
+          // }
+          // newPopulation.push(_CopyGenotype({fitness: 1, genotype: g}));
+
+          const index = Math.round(Math.random() * p1.genotype.length);
+
+          const g = p1.genotype.slice(0, index).concat(
+              p2.genotype.slice(index));
+
+          newPopulation.push(_CopyGenotype({fitness: 1, genotype: g}));
+        }
+
+        // Let's say keep top X% go through, but with mutations
+        const topX = [...parents.slice(0, Math.ceil(
+            parents.length * this._params.breed.immortalityCutoff))];
+
+        newPopulation.push(...topX.map(x => _CopyGenotype(x)));
+
+        // Mutations!
+        for (let p of newPopulation) {
+          const genotypeLength = p.genotype.length;
+          const mutationOdds = this._params.mutation.odds;
+          const mutationMagnitude = this._params.mutation.magnitude;
+          function _Mutate(x) {
+            const roll = Math.random();
+
+            if (roll < mutationOdds) {
+              const magnitude = mutationMagnitude * math.rand_normalish();
+              return x + magnitude;
+            }
+            return x;
+          }
+
+          p.genotype = p.genotype.map(g => _Mutate(g));
+        }
+
+        // Immortality granted to the winners from the last life.
+        // May the odds be forever in your favour.
+        newPopulation.push(...topX.map(x => _CopyGenotype(x)));
+
+        // Create a bunch of random crap to fill out the rest.
+        while (newPopulation.length < parents.length) {
+          newPopulation.push(
+              {fitness: 1, genotype: this._CreateRandomGenotype()});
+        }
+
+        return newPopulation;
+      }
+    },
+  };
+})();