main.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import {ffnet} from "./ffnet.js";
  2. import {population} from "./population.js"
  3. const _GRAVITY = 900;
  4. const _TERMINAL_VELOCITY = 400;
  5. const _MAX_UPWARDS_VELOCITY = -300;
  6. const _UPWARDS_ACCELERATION = -450;
  7. const _PIPE_SPACING_Y = 100;
  8. const _PIPE_SPACING_X = 200;
  9. const _TREADMILL_SPEED = -125;
  10. const _CONFIG_WIDTH = 960;
  11. const _CONFIG_HEIGHT = 540;
  12. const _GROUND_Y = _CONFIG_HEIGHT;
  13. const _BIRD_POS_X = 50;
  14. class PipePairObject {
  15. constructor(scene, x) {
  16. const height = _CONFIG_HEIGHT * (0.25 + 0.5 * Math.random());
  17. this._sprite1 = scene.add.sprite(x, height + _PIPE_SPACING_Y * 0.5, 'pipe');
  18. this._sprite1.displayOriginX = 0;
  19. this._sprite1.displayOriginY = 0;
  20. this._sprite2 = scene.add.sprite(x, height - _PIPE_SPACING_Y * 0.5, 'pipe');
  21. this._sprite2.displayOriginX = 0;
  22. this._sprite2.displayOriginY = 0;
  23. this._sprite2.displayHeight = -1 * this._sprite2.height;
  24. }
  25. Destroy() {
  26. this._sprite1.destroy();
  27. this._sprite2.destroy();
  28. }
  29. Update(timeElapsed) {
  30. this._sprite1.x += timeElapsed * _TREADMILL_SPEED;
  31. this._sprite2.x += timeElapsed * _TREADMILL_SPEED;
  32. }
  33. Intersects(aabb) {
  34. const b1 = this._sprite1.getBounds();
  35. const b2 = this._sprite2.getBounds();
  36. b2.y -= this._sprite2.height;
  37. return (
  38. Phaser.Geom.Intersects.RectangleToRectangle(b1, aabb) ||
  39. Phaser.Geom.Intersects.RectangleToRectangle(b2, aabb));
  40. }
  41. Reset(x) {
  42. const height = _CONFIG_HEIGHT * (0.25 + 0.5 * Math.random());
  43. this._sprite1.x = x;
  44. this._sprite1.y = height + _PIPE_SPACING_Y * 0.5;
  45. this._sprite2.x = x;
  46. this._sprite2.y = height - _PIPE_SPACING_Y * 0.5;
  47. }
  48. get X() {
  49. return this._sprite1.x;
  50. }
  51. get Width() {
  52. return this._sprite1.width;
  53. }
  54. }
  55. class FlappyBirdObject {
  56. constructor(scene) {
  57. this._scene = scene;
  58. this._sprite = scene.add.sprite(_BIRD_POS_X, 100, 'bird');
  59. this._spriteTint = scene.add.sprite(_BIRD_POS_X, 100, 'bird-colour');
  60. this._velocity = 0;
  61. this._dead = false;
  62. }
  63. Destroy() {
  64. this._sprite.destroy();
  65. }
  66. Update(params) {
  67. if (this._dead) {
  68. return;
  69. }
  70. this._ApplyGravity(params.timeElapsed)
  71. this._velocity = Math.min(Math.max(
  72. this._velocity, _MAX_UPWARDS_VELOCITY), _TERMINAL_VELOCITY);
  73. this._sprite.y += this._velocity * params.timeElapsed;
  74. this._spriteTint.y += this._velocity * params.timeElapsed;
  75. const v = new Phaser.Math.Vector2(
  76. -1 * _TREADMILL_SPEED * params.timeElapsed, 0);
  77. v.add(new Phaser.Math.Vector2(0, this._velocity));
  78. v.normalize();
  79. const rad = Math.atan2(v.y, v.x);
  80. const deg = (180.0 / Math.PI) * rad;
  81. this._sprite.angle = deg * 0.75;
  82. this._spriteTint.angle = deg * 0.75;
  83. }
  84. get Dead() {
  85. return this._dead;
  86. }
  87. set Dead(d) {
  88. this._dead = d;
  89. this._scene.tweens.add({
  90. targets: this._sprite,
  91. props: {
  92. alpha: { value: 0.0, duration: 500, ease: 'Sine.easeInOut' },
  93. },
  94. });
  95. this._scene.tweens.add({
  96. targets: this._spriteTint,
  97. props: {
  98. alpha: { value: 0.0, duration: 500, ease: 'Sine.easeInOut' },
  99. },
  100. });
  101. }
  102. set Alpha(a) {
  103. this._sprite.alpha = a;
  104. this._spriteTint.alpha = a;
  105. }
  106. get Bounds() {
  107. return this._sprite.getBounds();
  108. }
  109. _ApplyGravity(timeElapsed) {
  110. this._velocity += _GRAVITY * timeElapsed;
  111. }
  112. }
  113. class FlappyBird_Manual extends FlappyBirdObject {
  114. constructor(scene) {
  115. super(scene);
  116. this._frameInputs = [];
  117. }
  118. Update(params) {
  119. this._HandleInput(params);
  120. super.Update(params);
  121. }
  122. _HandleInput(params) {
  123. if (!params.keys.up) {
  124. return;
  125. }
  126. this._velocity += _UPWARDS_ACCELERATION;
  127. }
  128. }
  129. class FlappyBird_NeuralNet extends FlappyBirdObject {
  130. constructor(scene, populationEntity, params) {
  131. super(scene);
  132. this._model = new ffnet.FFNeuralNetwork(params.shapes);
  133. this._model.fromArray(populationEntity.genotype);
  134. this._populationEntity = populationEntity;
  135. this._spriteTint.setTint(params.tint);
  136. }
  137. Update(params) {
  138. function _PipeParams(bird, pipe) {
  139. const distToPipe = (
  140. (pipe.X + pipe.Width) - bird.Bounds.left) / _CONFIG_WIDTH;
  141. const distToPipeB = (
  142. (pipe._sprite1.y - bird.Bounds.bottom) / _CONFIG_HEIGHT) * 0.5 + 0.5;
  143. const distToPipeT = (
  144. (pipe._sprite2.y - bird.Bounds.top) / _CONFIG_HEIGHT) * 0.5 + 0.5;
  145. return [distToPipe, distToPipeB, distToPipeT];
  146. }
  147. function _Params(bird, pipes) {
  148. const inputs = pipes.map(p => _PipeParams(bird, p)).flat();
  149. inputs.push((bird._velocity / _GRAVITY) * 0.5 + 0.5);
  150. return inputs;
  151. }
  152. const inputs = _Params(this, params.nearestPipes);
  153. const decision = this._model.predict(inputs);
  154. if (decision > 0.5) {
  155. this._velocity += _UPWARDS_ACCELERATION;
  156. }
  157. super.Update(params);
  158. if (!this.Dead) {
  159. this._populationEntity.fitness += params.timeElapsed;
  160. }
  161. }
  162. }
  163. class FlappyBirdGame {
  164. constructor() {
  165. this._game = this._CreateGame();
  166. this._previousFrame = null;
  167. this._gameOver = true;
  168. this._statsText1 = null;
  169. this._statsText2 = null;
  170. this._gameOverText = null;
  171. this._pipes = [];
  172. this._birds = [];
  173. this._InitPopulations();
  174. }
  175. _InitPopulations() {
  176. const NN_DEF1 = [
  177. {size: 7},
  178. {size: 5, activation: ffnet.relu},
  179. {size: 1, activation: ffnet.sigmoid}
  180. ];
  181. const NN_DEF2 = [
  182. {size: 7},
  183. {size: 12, activation: ffnet.relu},
  184. {size: 1, activation: ffnet.sigmoid}
  185. ];
  186. const NN_DEF3 = [
  187. {size: 7},
  188. {size: 8, activation: ffnet.relu},
  189. {size: 8, activation: ffnet.relu},
  190. {size: 1, activation: ffnet.sigmoid}
  191. ];
  192. this._populations = [
  193. this._CreatePopulation(64, NN_DEF1, 0xFF0000),
  194. this._CreatePopulation(64, NN_DEF2, 0x0000FF),
  195. this._CreatePopulation(64, NN_DEF3, 0x00FF00),
  196. ];
  197. }
  198. _CreatePopulation(sz, shapes, colour) {
  199. const t = new ffnet.FFNeuralNetwork(shapes);
  200. const params = {
  201. population_size: sz,
  202. genotype: {
  203. size: t.toArray().length,
  204. },
  205. mutation: {
  206. magnitude: 0.5,
  207. odds: 0.1,
  208. decay: 0,
  209. },
  210. breed: {
  211. selectionCutoff: 0.2,
  212. immortalityCutoff: 0.05,
  213. childrenPercentage: 0.5,
  214. },
  215. shapes: shapes,
  216. tint: colour,
  217. };
  218. return new population.Population(params);
  219. }
  220. _Destroy() {
  221. for (let b of this._birds) {
  222. b.Destroy();
  223. }
  224. for (let p of this._pipes) {
  225. p.Destroy();
  226. }
  227. this._statsText1.destroy();
  228. this._statsText2.destroy();
  229. if (this._gameOverText !== null) {
  230. this._gameOverText.destroy();
  231. }
  232. this._birds = [];
  233. this._pipes = [];
  234. this._previousFrame = null;
  235. }
  236. _Init() {
  237. for (let i = 0; i < 5; i+=1) {
  238. this._pipes.push(
  239. new PipePairObject(this._scene, 500 + i * _PIPE_SPACING_X));
  240. }
  241. this._gameOver = false;
  242. this._stats = {
  243. alive: 0,
  244. score: 0,
  245. };
  246. const style = {
  247. font: "40px Roboto",
  248. fill: "#FFFFFF",
  249. align: "right",
  250. fixedWidth: 210,
  251. shadow: {
  252. offsetX: 2,
  253. offsetY: 2,
  254. color: "#000",
  255. blur: 2,
  256. fill: true
  257. }
  258. };
  259. this._statsText1 = this._scene.add.text(0, 0, '', style);
  260. style.align = 'left';
  261. this._statsText2 = this._scene.add.text(
  262. this._statsText1.width + 10, 0, '', style);
  263. this._birds = [];
  264. for (let curPop of this._populations) {
  265. curPop.Step();
  266. this._birds.push(...curPop._population.map(
  267. p => new FlappyBird_NeuralNet(
  268. this._scene, p, curPop._params)));
  269. }
  270. }
  271. _CreateGame() {
  272. const self = this;
  273. const config = {
  274. type: Phaser.AUTO,
  275. scene: {
  276. preload: function() { self._OnPreload(this); },
  277. create: function() { self._OnCreate(this); },
  278. update: function() { self._OnUpdate(this); },
  279. },
  280. scale: {
  281. mode: Phaser.Scale.FIT,
  282. autoCenter: Phaser.Scale.CENTER_BOTH,
  283. width: _CONFIG_WIDTH,
  284. height: _CONFIG_HEIGHT
  285. }
  286. };
  287. return new Phaser.Game(config);
  288. }
  289. _OnPreload(scene) {
  290. this._scene = scene;
  291. this._scene.load.image('sky', 'assets/sky.png');
  292. this._scene.load.image('bird', 'assets/bird.png');
  293. this._scene.load.image('bird-colour', 'assets/bird-colour.png');
  294. this._scene.load.image('pipe', 'assets/pipe.png');
  295. }
  296. _OnCreate(scene) {
  297. const s = this._scene.add.image(0, 0, 'sky');
  298. s.displayOriginX = 0;
  299. s.displayOriginY = 0;
  300. s.displayWidth = _CONFIG_WIDTH;
  301. s.displayHeight = _CONFIG_HEIGHT;
  302. this._keys = {
  303. up: this._scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP),
  304. f: this._scene.input.keyboard.addKey('F'),
  305. r: this._scene.input.keyboard.addKey('R'),
  306. }
  307. this._keys.f.on('down', function () {
  308. if (this._scene.scale.isFullscreen) {
  309. this._scene.scale.stopFullscreen();
  310. } else {
  311. this._scene.scale.startFullscreen();
  312. }
  313. }, this);
  314. this._keys.r.on('down', function () {
  315. this._Destroy();
  316. this._Init();
  317. }, this);
  318. this._Init();
  319. }
  320. _OnUpdate(scene) {
  321. if (this._gameOver) {
  322. this._DrawStats();
  323. return;
  324. }
  325. const currentFrame = scene.time.now;
  326. if (this._previousFrame == null) {
  327. this._previousFrame = currentFrame;
  328. }
  329. const timeElapsedInS = Math.min(
  330. (currentFrame - this._previousFrame) / 1000.0, 1.0 / 30.0);
  331. this._UpdateBirds(timeElapsedInS);
  332. this._UpdatePipes(timeElapsedInS);
  333. this._CheckGameOver();
  334. this._DrawStats();
  335. this._previousFrame = currentFrame;
  336. }
  337. _CheckGameOver() {
  338. const results = this._birds.map(b => this._IsBirdOutOfBounds(b));
  339. this._stats.alive = results.reduce((t, r) => (r ? t: t + 1), 0);
  340. if (results.every(b => b)) {
  341. this._GameOver();
  342. }
  343. }
  344. _IsBirdOutOfBounds(bird) {
  345. const birdAABB = bird.Bounds;
  346. birdAABB.top += 10;
  347. birdAABB.bottom -= 10;
  348. birdAABB.left += 10;
  349. birdAABB.right -= 10;
  350. if (bird.Dead) {
  351. return true;
  352. }
  353. if (birdAABB.bottom >= _GROUND_Y || birdAABB.top <= 0) {
  354. bird.Dead = true;
  355. return true;
  356. }
  357. for (const p of this._pipes) {
  358. if (p.Intersects(birdAABB)) {
  359. bird.Dead = true;
  360. return true;
  361. }
  362. }
  363. return false;
  364. }
  365. _GetNearestPipes() {
  366. let index = 0;
  367. if (this._pipes[0].X + this._pipes[0].Width <= _BIRD_POS_X) {
  368. index = 1;
  369. }
  370. return this._pipes.slice(index, 2);
  371. }
  372. _UpdateBirds(timeElapsed) {
  373. const params = {
  374. timeElapsed: timeElapsed,
  375. keys: {up: Phaser.Input.Keyboard.JustDown(this._keys.up)},
  376. nearestPipes: this._GetNearestPipes(),
  377. };
  378. for (let b of this._birds) {
  379. b.Update(params);
  380. }
  381. }
  382. _UpdatePipes(timeElapsed) {
  383. const oldPipeX = this._pipes[0].X + this._pipes[0].Width;
  384. for (const p of this._pipes) {
  385. p.Update(timeElapsed);
  386. }
  387. const newPipeX = this._pipes[0].X + this._pipes[0].Width;
  388. if (oldPipeX > _BIRD_POS_X && newPipeX <= _BIRD_POS_X) {
  389. this._stats.score += 1;
  390. }
  391. if ((this._pipes[0].X + this._pipes[0].Width) <= 0) {
  392. const p = this._pipes.shift();
  393. p.Reset(this._pipes[this._pipes.length - 1].X + _PIPE_SPACING_X);
  394. this._pipes.push(p);
  395. }
  396. }
  397. _GameOver() {
  398. const text = "GAME OVER";
  399. const style = {
  400. font: "100px Roboto",
  401. fill: "#FFFFFF",
  402. align: "center",
  403. fixedWidth: _CONFIG_WIDTH,
  404. shadow: {
  405. offsetX: 2,
  406. offsetY: 2,
  407. color: "#000",
  408. blur: 2,
  409. fill: true
  410. }
  411. };
  412. this._gameOverText = this._scene.add.text(
  413. 0, _CONFIG_HEIGHT * 0.25, text, style);
  414. this._gameOver = true;
  415. setTimeout(() => {
  416. this._Destroy();
  417. this._Init();
  418. }, 2000);
  419. }
  420. _DrawStats() {
  421. function _Line(t, s) {
  422. return t + ': ' + s + '\n';
  423. }
  424. const text1 = 'Generation:\n' + 'Score:\n' + 'Alive:\n';
  425. this._statsText1.text = text1;
  426. const text2 = (
  427. this._populations[0]._generations + '\n' +
  428. this._stats.score + '\n' +
  429. this._stats.alive + '\n');
  430. this._statsText2.text = text2;
  431. }
  432. }
  433. const _GAME = new FlappyBirdGame();