text: /*
   * Kontra.js v4.0.1 (Custom Build on 2018-09-11) | MIT
   * Build: https://straker.github.io/kontra/download?files=gameLoop+keyboard+sprite+assets
   */
  kontra = {
  
    /**
     * Initialize the canvas.
     * @memberof kontra
     *
     * @param {string|HTMLCanvasElement} canvas - Main canvas ID or Element for the game.
     */
    init(canvas) {
  
      // check if canvas is a string first, an element next, or default to getting
      // first canvas on page
      var canvasEl = this.canvas = document.getElementById(canvas) ||
                                   canvas ||
                                   document.querySelector('canvas');
  
      // @if DEBUG
      if (!canvasEl) {
        throw Error('You must provide a canvas element for the game');
      }
      // @endif
  
      this.context = canvasEl.getContext('2d');
    },
  
    /**
     * Noop function.
     * @see https://stackoverflow.com/questions/21634886/what-is-the-javascript-convention-for-no-operation#comment61796464_33458430
     * @memberof kontra
     * @private
     *
     * The new operator is required when using sinon.stub to replace with the noop.
     */
    _noop: new Function,
  
    /**
     * Dispatch event to any part of the code that needs to know when
     * a new frame has started. Will be filled out in pointer events.
     * @memberOf kontra
     * @private
     */
    _tick: new Function
  };
  
  (function() {
  
    /**
     * Game loop that updates and renders the game every frame.
     * @memberof kontra
     *
     * @param {object}   properties - Properties of the game loop.
     * @param {number}   [properties.fps=60] - Desired frame rate.
     * @param {boolean}  [properties.clearCanvas=true] - Clear the canvas every frame.
     * @param {function} properties.update - Function called to update the game.
     * @param {function} properties.render - Function called to render the game.
     */
    kontra.gameLoop = function(properties) {
      properties = properties || {};
  
      // check for required functions
      // @if DEBUG
      if ( !(properties.update && properties.render) ) {
        throw Error('You must provide update() and render() functions');
      }
      // @endif
  
      // animation variables
      let fps = properties.fps || 60;
      let accumulator = 0;
      let delta = 1E3 / fps;  // delta between performance.now timings (in ms)
      let step = 1 / fps;
  
      let clear = (properties.clearCanvas === false ?
                  kontra._noop :
                  function clear() {
                    kontra.context.clearRect(0,0,kontra.canvas.width,kontra.canvas.height);
                  });
      let last, rAF, now, dt;
  
      /**
       * Called every frame of the game loop.
       */
      function frame() {
        rAF = requestAnimationFrame(frame);
  
        now = performance.now();
        dt = now - last;
        last = now;
  
        // prevent updating the game with a very large dt if the game were to lose focus
        // and then regain focus later
        if (dt > 1E3) {
          return;
        }
  
        kontra._tick();
        accumulator += dt;
  
        while (accumulator >= delta) {
          gameLoop.update(step);
  
          accumulator -= delta;
        }
  
        clear();
        gameLoop.render();
      }
  
      // game loop object
      let gameLoop = {
        update: properties.update,
        render: properties.render,
        isStopped: true,
  
        /**
         * Start the game loop.
         * @memberof kontra.gameLoop
         */
        start() {
          last = performance.now();
          this.isStopped = false;
          requestAnimationFrame(frame);
        },
  
        /**
         * Stop the game loop.
         */
        stop() {
          this.isStopped = true;
          cancelAnimationFrame(rAF);
        },
  
        // expose properties for testing
        // @if DEBUG
        _frame: frame,
        set _last(value) {
          last = value;
        }
        // @endif
      };
  
      return gameLoop;
    };
  })();
  
  (function() {
    let callbacks = {};
    let pressedKeys = {};
  
    let keyMap = {
      // named keys
      13: 'enter',
      27: 'esc',
      32: 'space',
      37: 'left',
      38: 'up',
      39: 'right',
      40: 'down'
    };
  
    // alpha keys
    // @see https://stackoverflow.com/a/43095772/2124254
    for (let i = 0; i < 26; i++) {
      keyMap[65+i] = (10 + i).toString(36);
    }
    // numeric keys
    for (i = 0; i < 10; i++) {
      keyMap[48+i] = ''+i;
    }
  
    addEventListener('keydown', keydownEventHandler);
    addEventListener('keyup', keyupEventHandler);
    addEventListener('blur', blurEventHandler);
  
    /**
     * Execute a function that corresponds to a keyboard key.
     * @private
     *
     * @param {Event} e
     */
    function keydownEventHandler(e) {
      let key = keyMap[e.which];
      pressedKeys[key] = true;
  
      if (callbacks[key]) {
        callbacks[key](e);
      }
    }
  
    /**
     * Set the released key to not being pressed.
     * @private
     *
     * @param {Event} e
     */
    function keyupEventHandler(e) {
      pressedKeys[ keyMap[e.which] ] = false;
    }
  
    /**
     * Reset pressed keys.
     * @private
     *
     * @param {Event} e
     */
    function blurEventHandler(e) {
      pressedKeys = {};
    }
  
    /**
     * Object for using the keyboard.
     */
    kontra.keys = {
      /**
       * Register a function to be called on a key press.
       * @memberof kontra.keys
       *
       * @param {string|string[]} keys - key or keys to bind.
       */
      bind(keys, callback) {
        // smaller than doing `Array.isArray(keys) ? keys : [keys]`
        [].concat(keys).map(function(key) {
          callbacks[key] = callback;
        })
      },
  
      /**
       * Remove the callback function for a key.
       * @memberof kontra.keys
       *
       * @param {string|string[]} keys - key or keys to unbind.
       */
      unbind(keys, undefined) {
        [].concat(keys).map(function(key) {
          callbacks[key] = undefined;
        })
      },
  
      /**
       * Returns whether a key is pressed.
       * @memberof kontra.keys
       *
       * @param {string} key - Key to check for press.
       *
       * @returns {boolean}
       */
      pressed(key) {
        return !!pressedKeys[key];
      }
    };
  })();
  
  (function() {
  
    class Vector {
      /**
       * Initialize the vectors x and y position.
       * @memberof kontra.vector
       * @private
       *
       * @param {number} [x=0] - X coordinate.
       * @param {number} [y=0] - Y coordinate.
       *
       * @returns {vector}
       */
      constructor(x, y) {
        this._x = x || 0;
        this._y = y || 0;
      }
  
      /**
       * Add a vector to this vector.
       * @memberof kontra.vector
       *
       * @param {vector} vector - Vector to add.
       * @param {number} dt=1 - Time since last update.
       */
      add(vector, dt) {
        this.x += (vector.x || 0) * (dt || 1);
        this.y += (vector.y || 0) * (dt || 1);
      }
  
      /**
       * Clamp the vector between two points that form a rectangle.
       * @memberof kontra.vector
       *
       * @param {number} xMin - Min x value.
       * @param {number} yMin - Min y value.
       * @param {number} xMax - Max x value.
       * @param {number} yMax - Max y value.
       */
      clamp(xMin, yMin, xMax, yMax) {
        this._c = true;
        this._a = xMin;
        this._b = yMin;
        this._d = xMax;
        this._e = yMax;
      }
  
      /**
       * Vector x
       * @memberof kontra.vector
       *
       * @property {number} x
       */
      get x() {
        return this._x;
      }
  
      /**
       * Vector y
       * @memberof kontra.vector
       *
       * @property {number} y
       */
      get y() {
        return this._y;
      }
  
      set x(value) {
        this._x = (this._c ? Math.min( Math.max(this._a, value), this._d ) : value);
      }
  
      set y(value) {
        this._y = (this._c ? Math.min( Math.max(this._b, value), this._e ) : value);
      }
    }
  
    /**
     * A vector for 2D space.
     * @memberof kontra
     *
     * @param {number} [x=0] - X coordinate.
     * @param {number} [y=0] - Y coordinate.
     */
    kontra.vector = (x, y) => {
      return new Vector(x, y);
    };
    kontra.vector.prototype = Vector.prototype;
  
  
  
  
  
    class Sprite {
      /**
       * Initialize properties on the sprite.
       * @memberof kontra.sprite
       *
       * @param {object} properties - Properties of the sprite.
       * @param {number} properties.x - X coordinate of the sprite.
       * @param {number} properties.y - Y coordinate of the sprite.
       * @param {number} [properties.dx] - Change in X position.
       * @param {number} [properties.dy] - Change in Y position.
       * @param {number} [properties.ddx] - Change in X velocity.
       * @param {number} [properties.ddy] - Change in Y velocity.
       *
       * @param {number} [properties.ttl=0] - How may frames the sprite should be alive.
       * @param {Context} [properties.context=kontra.context] - Provide a context for the sprite to draw on.
       *
       * @param {Image|Canvas} [properties.image] - Image for the sprite.
       *
       * @param {object} [properties.animations] - Animations for the sprite instead of an image.
       *
       * @param {string} [properties.color] - If no image or animation is provided, use color to draw a rectangle for the sprite.
       * @param {number} [properties.width] - Width of the sprite for drawing a rectangle.
       * @param {number} [properties.height] - Height of the sprite for drawing a rectangle.
       *
       * @param {function} [properties.update] - Function to use to update the sprite.
       * @param {function} [properties.render] - Function to use to render the sprite.
       *
       * If you need the sprite to live forever, or just need it to stay on screen until you
       * decide when to kill it, you can set <code>ttl</code> to <code>Infinity</code>.
       * Just be sure to set <code>ttl</code> to 0 when you want the sprite to die.
       */
      // @see https://github.com/jed/140bytes/wiki/Byte-saving-techniques#use-placeholder-arguments-instead-of-var
      init(properties, prop, temp, firstAnimation) {
        properties = properties || {};
  
        this.position = kontra.vector(properties.x, properties.y);
        this.velocity = kontra.vector(properties.dx, properties.dy);
        this.acceleration = kontra.vector(properties.ddx, properties.ddy);
  
        // defaults
        this.width = this.height = 0;
        this.context = kontra.context;
  
        // loop through properties before overrides
        for (prop in properties) {
          this[prop] = properties[prop];
        }
  
        // image sprite
        if (temp = properties.image) {
          this.image = temp;
          this.width = temp.width;
          this.height = temp.height;
        }
        // animation sprite
        else if (temp = properties.animations) {
  
          // clone each animation so no sprite shares an animation
          for (prop in temp) {
            this.animations[prop] = temp[prop].clone();
  
            // default the current animation to the first one in the list
            firstAnimation = firstAnimation || temp[prop];
          }
  
          this._ca = firstAnimation;
          this.width = firstAnimation.width;
          this.height = firstAnimation.height;
        }
  
        return this;
      }
  
      // define getter and setter shortcut functions to make it easier to work with the
      // position, velocity, and acceleration vectors.
  
      /**
       * Sprite position.x
       * @memberof kontra.sprite
       *
       * @property {number} x
       */
      get x() {
        return this.position.x;
      }
  
      /**
       * Sprite position.y
       * @memberof kontra.sprite
       *
       * @property {number} y
       */
      get y() {
        return this.position.y;
      }
  
      /**
       * Sprite velocity.x
       * @memberof kontra.sprite
       *
       * @property {number} dx
       */
      get dx() {
        return this.velocity.x;
      }
  
      /**
       * Sprite velocity.y
       * @memberof kontra.sprite
       *
       * @property {number} dy
       */
      get dy() {
        return this.velocity.y;
      }
  
      /**
       * Sprite acceleration.x
       * @memberof kontra.sprite
       *
       * @property {number} ddx
       */
      get ddx() {
        return this.acceleration.x;
      }
  
      /**
       * Sprite acceleration.y
       * @memberof kontra.sprite
       *
       * @property {number} ddy
       */
      get ddy() {
        return this.acceleration.y;
      }
  
      set x(value) {
        this.position.x = value;
      }
      set y(value) {
        this.position.y = value;
      }
      set dx(value) {
        this.velocity.x = value;
      }
      set dy(value) {
        this.velocity.y = value;
      }
      set ddx(value) {
        this.acceleration.x = value;
      }
      set ddy(value) {
        this.acceleration.y = value;
      }
  
      /**
       * Determine if the sprite is alive.
       * @memberof kontra.sprite
       *
       * @returns {boolean}
       */
      isAlive() {
        return this.ttl > 0;
      }
  
      /**
       * Simple bounding box collision test.
       * @memberof kontra.sprite
       *
       * @param {object} object - Object to check collision against.
       *
       * @returns {boolean} True if the objects collide, false otherwise.
       */
      collidesWith(object) {
        return this.x < object.x + object.width &&
               this.x + this.width > object.x &&
               this.y < object.y + object.height &&
               this.y + this.height > object.y;
      }
  
      /**
       * Update the sprites velocity and position.
       * @memberof kontra.sprite
       * @abstract
       *
       * @param {number} dt - Time since last update.
       *
       * This function can be overridden on a per sprite basis if more functionality
       * is needed in the update step. Just call <code>this.advance()</code> when you need
       * the sprite to update its position.
       *
       * @example
       * sprite = kontra.sprite({
       *   update: function update(dt) {
       *     // do some logic
       *
       *     this.advance(dt);
       *   }
       * });
       */
      update(dt) {
        this.advance(dt);
      }
  
      /**
       * Render the sprite.
       * @memberof kontra.sprite.
       * @abstract
       *
       * This function can be overridden on a per sprite basis if more functionality
       * is needed in the render step. Just call <code>this.draw()</code> when you need the
       * sprite to draw its image.
       *
       * @example
       * sprite = kontra.sprite({
       *   render: function render() {
       *     // do some logic
       *
       *     this.draw();
       *   }
       * });
       */
      render() {
        this.draw();
      }
  
      /**
       * Play an animation.
       * @memberof kontra.sprite
       *
       * @param {string} name - Name of the animation to play.
       */
      playAnimation(name) {
        this._ca = this.animations[name];
  
        if (!this._ca.loop) {
          this._ca.reset();
        }
      }
  
      /**
       * Advance the sprites position, velocity, and current animation (if it
       * has one).
       * @memberof kontra.sprite
       *
       * @param {number} dt - Time since last update.
       */
      advance(dt) {
        this.velocity.add(this.acceleration, dt);
        this.position.add(this.velocity, dt);
  
        this.ttl--;
  
        if (this._ca) {
          this._ca.update(dt);
        }
      }
  
      /**
       * Draw the sprite to the canvas.
       * @memberof kontra.sprite
       */
      draw() {
        if (this.image) {
          this.context.drawImage(this.image, this.x, this.y);
        }
        else if (this._ca) {
          this._ca.render(this);
        }
        else {
          this.context.fillStyle = this.color;
          this.context.fillRect(this.x, this.y, this.width, this.height);
        }
      }
    };
  
    /**
     * A sprite with a position, velocity, and acceleration.
     * @memberof kontra
     * @requires kontra.vector
     *
     * @param {object} properties - Properties of the sprite.
     * @param {number} properties.x - X coordinate of the sprite.
     * @param {number} properties.y - Y coordinate of the sprite.
     * @param {number} [properties.dx] - Change in X position.
     * @param {number} [properties.dy] - Change in Y position.
     * @param {number} [properties.ddx] - Change in X velocity.
     * @param {number} [properties.ddy] - Change in Y velocity.
     *
     * @param {number} [properties.ttl=0] - How may frames the sprite should be alive.
     * @param {Context} [properties.context=kontra.context] - Provide a context for the sprite to draw on.
     *
     * @param {Image|Canvas} [properties.image] - Image for the sprite.
     *
     * @param {object} [properties.animations] - Animations for the sprite instead of an image.
     *
     * @param {string} [properties.color] - If no image or animation is provided, use color to draw a rectangle for the sprite.
     * @param {number} [properties.width] - Width of the sprite for drawing a rectangle.
     * @param {number} [properties.height] - Height of the sprite for drawing a rectangle.
     *
     * @param {function} [properties.update] - Function to use to update the sprite.
     * @param {function} [properties.render] - Function to use to render the sprite.
     */
    kontra.sprite = (properties) => {
      return (new Sprite()).init(properties);
    };
    kontra.sprite.prototype = Sprite.prototype;
  })();
  
  (function() {
    let imageRegex = /(jpeg|jpg|gif|png)$/;
    let audioRegex = /(wav|mp3|ogg|aac)$/;
    let noRegex = /^no$/;
    let leadingSlash = /^\//;
    let trailingSlash = /\/$/;
    let assets;
  
    // audio playability
    // @see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/audio.js
    let audio = new Audio();
    let canUse = {
      wav: '',
      mp3: audio.canPlayType('audio/mpeg;').replace(noRegex,''),
      ogg: audio.canPlayType('audio/ogg; codecs="vorbis"').replace(noRegex,''),
      aac: audio.canPlayType('audio/aac;').replace(noRegex,'')
    };
  
    /**
     * Join a base path and asset path.
     *
     * @param {string} base - The asset base path.
     * @param {string} url - The URL to the asset.
     *
     * @returns {string}
     */
    function joinPath(base, url) {
      return [base.replace(trailingSlash, ''), base ? url.replace(leadingSlash, '') : url]
        .filter(s => s)
        .join('/')
    }
  
    /**
     * Get the extension of an asset.
     *
     * @param {string} url - The URL to the asset.
     *
     * @returns {string}
     */
    function getExtension(url) {
      return url.split('.').pop();
    }
  
    /**
     * Get the name of an asset.
     *
     * @param {string} url - The URL to the asset.
     *
     * @returns {string}
     */
    function getName(url) {
      let name = url.replace('.' + getExtension(url), '');
  
      // remove leading slash if there is no folder in the path
      // @see https://stackoverflow.com/a/50592629/2124254
      return name.split('/').length == 2 ? name.replace(leadingSlash, '') : name;
    }
  
    /**
     * Load an Image file. Uses imagePath to resolve URL.
     * @memberOf kontra.assets
     * @private
     *
     * @param {string} url - The URL to the Image file.
     *
     * @returns {Promise} A deferred promise. Promise resolves with the Image.
     *
     * @example
     * kontra.loadImage('car.png');
     * kontra.loadImage('autobots/truck.png');
     */
    function loadImage(originalUrl, url) {
      return new Promise(function(resolve, reject) {
        let image = new Image();
        url = joinPath(assets.imagePath, originalUrl);
  
        image.onload = function loadImageOnLoad() {
          assets.images[ getName(originalUrl) ] = assets.images[url] = this;
          resolve(this);
        };
  
        image.onerror = function loadImageOnError() {
          reject(/* @if DEBUG */ 'Unable to load image ' + /* @endif */ url);
        };
  
        image.src = url;
      });
    }
  
    /**
     * Load an Audio file. Supports loading multiple audio formats which will be resolved by
     * the browser in the order listed. Uses audioPath to resolve URL.
     * @memberOf kontra.assets
     * @private
     *
     * @param {string|string[]} url - The URL to the Audio file.
     *
     * @returns {Promise} A deferred promise. Promise resolves with the Audio.
     *
     * @example
     * kontra.loadAudio('sound_effects/laser.mp3');
     * kontra.loadAudio(['explosion.mp3', 'explosion.m4a', 'explosion.ogg']);
     */
    function loadAudio(originalUrl, url, undefined) {
      return new Promise(function(resolve, reject) {
  
        // determine which audio format the browser can play
        originalUrl = [].concat(originalUrl).reduce(function(a, source) {
          return canUse[ getExtension(source) ] ? source : a
        }, undefined);
  
        if (!originalUrl) {
          reject(/* @if DEBUG */ 'cannot play any of the audio formats provided' + /* @endif */ originalUrl);
        }
        else {
          let audio = new Audio();
          url = joinPath(assets.audioPath, originalUrl);
  
          audio.addEventListener('canplay', function loadAudioOnLoad() {
            assets.audio[ getName(originalUrl) ] = assets.audio[url] = this;
            resolve(this);
          });
  
          audio.onerror = function loadAudioOnError() {
            reject(/* @if DEBUG */ 'Unable to load audio ' + /* @endif */ url);
          };
  
          audio.src = url;
          audio.load();
        }
      });
    }
  
    /**
     * Load a data file (be it text or JSON). Uses dataPath to resolve URL.
     * @memberOf kontra.assets
     * @private
     *
     * @param {string} url - The URL to the data file.
     *
     * @returns {Promise} A deferred promise. Resolves with the data or parsed JSON.
     *
     * @example
     * kontra.loadData('bio.json');
     * kontra.loadData('dialog.txt');
     */
    function loadData(originalUrl, url) {
      url = joinPath(assets.dataPath, originalUrl);
  
      return fetch(url).then(function(response) {
        if (!response.ok) throw response;
        return response.clone().json().catch(function() { return response.text() })
      }).then(function(data) {
        assets.data[ getName(originalUrl) ] = assets.data[url] = data;
        return data;
      });
    }
  
    /**
     * Object for loading assets.
     */
    assets = kontra.assets = {
      // all assets are stored by name as well as by URL
      images: {},
      audio: {},
      data: {},
  
      // base asset path for determining asset URLs
      imagePath: '',
      audioPath: '',
      dataPath: '',
  
      /**
       * Load an Image, Audio, or data file.
       * @memberOf kontra.assets
       *
       * @param {string|string[]} - Comma separated list of assets to load.
       *
       * @returns {Promise}
       *
       * @example
       * kontra.loadAsset('car.png');
       * kontra.loadAsset(['explosion.mp3', 'explosion.ogg']);
       * kontra.loadAsset('bio.json');
       * kontra.loadAsset('car.png', ['explosion.mp3', 'explosion.ogg'], 'bio.json');
       */
      load() {
        let promises = [];
        let url, extension, asset, i, promise;
  
        for (i = 0; (asset = arguments[i]); i++) {
          url = [].concat(asset)[0];
  
          extension = getExtension(url);
          if (extension.match(imageRegex)) {
            promise = loadImage(asset);
          }
          else if (extension.match(audioRegex)) {
            promise = loadAudio(asset);
          }
          else {
            promise = loadData(asset);
          }
  
          promises.push(promise);
        }
  
        return Promise.all(promises);
      },
  
      // expose properties for testing
      /* @if DEBUG */
      _canUse: canUse
      /* @endif */
    };
  })();