123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- 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();
|