video-game-coding-club/geometry-smash

View on GitHub
main.js

Summary

Maintainability
F
4 days
Test Coverage
let canvas = document.getElementById("game-layer");
let ctx = canvas.getContext("2d");

/* The game time. */
let time = 0;

/* The last time. */
let time_last = 0;

/* The time difference between the time in the previous call and this
 * one. */
let time_difference = 0;

/* The floor speed. */
let floorSpeed = 1;

/* The obstacle speed. */
let obs_speed = 1;

/* The background speed. */
let backgroundSpeed = 0.5 * obs_speed;

/* The obstacle offset (when moving in time). */
let obs_offset = 0;

/* The position of the floor. */
let floorHeight = 0;

/* Debug mode. */
let debugMode = false;

/* The current mouse position. */
let mousePosition = {
  x: 0,
  y: 0
};

/* The toxic sign image. */
let toxicImage = new Image();
toxicImage.src = "assets/toxic.jpg";

/* The electric sign image. */
let electricImage = new Image();
electricImage.src = "assets/electric.jpg";

/* The snowflake image. */
let snowflakeImage = new Image();
snowflakeImage.src = "assets/snowflake.jpg";

/* The dragon image. */
let dragonImage = new Image();
dragonImage.src = "assets/Dragon.jpg";

/* The bone image. */
let boneImage = new Image();
boneImage.src = "assets/bone_dragon.jpg";

/* The Griffin image. */
let GriffinImage = new Image();
GriffinImage.src = "assets/griffin.jpg";

/* The sound effect for the laser obstacle. */
let lightningSound = new Audio("assets/flash.wav");
lightningSound.loop = true;
lightningSound.muted = true;

/* The state of the laser sound. */
let laserSound;

let obstacleSpike = {
  draw: function(x, y) {
    ctx.beginPath();
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x + 60, this.y);
    ctx.lineTo(this.x + 30, this.y - 60);
    ctx.closePath();
    ctx.fillStyle = "black";
    ctx.fill();
  },
  x: 0,
  y: 0,
  w: 60,
  h: -60
};

let obstacleSaw = {
  draw: function(x, y) {
    let numberSpikes = 20;
    let sawRadius = 80;
    let sawHeight = y - 60 * (3 + Math.sin(time / 70 / 2 * Math.PI));

    ctx.beginPath();
    ctx.ellipse(x, sawHeight, sawRadius, sawRadius, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fillStyle = "silver";
    ctx.fill();

    ctx.beginPath();
    ctx.ellipse(x, sawHeight, 15, 15, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fillStyle = "black";
    ctx.fill();

    ctx.beginPath();
    ctx.rect(x - 5, sawHeight, 10, sawHeight);
    ctx.closePath();
    ctx.fillStyle = "black";
    ctx.fill();

    for (let i = 0; i < numberSpikes; i++) {
      ctx.save();

      let alpha = -2 * Math.PI / numberSpikes * i - 0.15 * time / (2 * Math.PI) % (2 * Math.PI);

      ctx.translate(x + sawRadius * Math.sin(alpha), sawHeight + sawRadius * Math.cos(alpha));
      ctx.rotate(-alpha);

      ctx.beginPath();
      ctx.moveTo(-10, 0);
      ctx.lineTo(10, 0);
      ctx.lineTo(0, 20);
      ctx.closePath();
      ctx.fillStyle = "black";
      ctx.fill();

      ctx.restore();
    }
  },
  x: -80,
  y: 0,
  w: 160,
  h: -160
};

let obstacleThorns = {
  draw: function(x, y) {
    this.x = x;
    this.y = y;
    ctx.beginPath();
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x + 10, this.y - 10);
    ctx.lineTo(this.x + 20, this.y);
    ctx.closePath();
    ctx.strokeStyle = "black";
    ctx.stroke();
  },
  x: 0,
  y: 0,
  w: 20,
  h: -10,
  pos_x: 0,
  pos_y: 0
};

let drawSignOutline = function(x, y) {
  ctx.beginPath();
  ctx.rect(x, y - 20, 140, -120);
  ctx.closePath();

  ctx.fillStyle = "white";
  ctx.strokeStyle = "black";
  ctx.lineWidth = 4;
  ctx.stroke();
  ctx.fill();
};

let toxicSign = {
  draw: function(x, y) {
    drawSignOutline(x, y);

    ctx.beginPath();
    ctx.rect(x + 60, y, 20, -20);
    ctx.closePath();
    ctx.fillStyle = "black";
    ctx.fill();

    ctx.drawImage(toxicImage, x, y - 140, 140, 120);
  },
  x: 0,
  y: 0,
  w: 140,
  h: -140,
  ignore_me: true
};

let electricSign = {
  draw: function(x, y) {
    drawSignOutline(x, y);

    ctx.beginPath();
    ctx.rect(x + 60, y, 20, -20);
    ctx.closePath();

    ctx.fillStyle = "black";
    ctx.fill();

    ctx.drawImage(electricImage, x, y - 140, 140, 120);
  },
  x: 0,
  y: 0,
  w: 140,
  h: -140,
  ignore_me: true
};

let obstacleLaser = {
  draw: function(x, y) {
    ctx.fillStyle = "black";
    ctx.fillRect(x, y, 76, -40);

    ctx.beginPath();
    ctx.arc(x + 38, y - 40, 30, Math.PI, 2 * Math.PI);
    ctx.fillStyle = "gold";
    ctx.fill();

    if (time % this.laserInterval > this.laserInterval - this.laserOn) {
      this.h = -canvas.height;
      /* The current height of the laser beam. */
      let laserTop = 0;

      if (laserSound === undefined) {
        laserSound = lightningSound.play();
        if (laserSound !== undefined) {
          laserSound.then(_ => {
            console.log("playing sound");
          }).catch(error => {
            console.log("could not play sound");
            console.log(error);
          });
        }
      }
      if (time % this.laserInterval < this.laserInterval - this.laserOn + this.laserSpeed) {
        laserTop = y - 76 - (y - 76) / 10 * (time % 10);
      }

      ctx.beginPath();
      ctx.moveTo(x + 38, y - 76);
      ctx.lineTo(x + 38, laserTop);
      ctx.lineWidth = 9;
      ctx.strokeStyle = "darkred";
      ctx.stroke();

      ctx.beginPath();
      ctx.moveTo(x + 32, y - 75);
      ctx.lineTo(x + 32, laserTop);
      ctx.strokeStyle = "orangered";
      ctx.lineWidth = 4;
      ctx.stroke();

      ctx.beginPath();
      ctx.moveTo(x + 44, y - 75);
      ctx.lineTo(x + 44, laserTop);
      ctx.strokeStyle = "orangered";
      ctx.lineWidth = 4;
      ctx.stroke();
    } else {
      this.h = 0;
      if (laserSound !== undefined) {
        lightningSound.pause();
        laserSound = undefined;
      }
    }

    ctx.fillStyle = "black";
    ctx.fillRect(x + 16, 0, 44, 18);
  },
  laserInterval: 120,
  laserOn: 40,
  laserSpeed: 7,
  x: 0,
  y: 0,
  w: 76,
  h: 0
};

let obstacleTrapdoor = {
  draw: function(x, y) {
    ctx.fillStyle = "black";
    ctx.fillRect(x, y, 350, -80);
  },
  x: 0,
  y: 0,
  w: 350,
  h: -80
};

let obstaclePole = {
  draw: function(x, y) {
    let speed = 0.1;
    //let height = -Math.max(300, x - speed * time);
    let poleHeight = Math.min(250, 0.5 * canvas.width - x);
    this.h = -poleHeight;

    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x + 100, y);
    ctx.lineTo(x + 80, floorHeight - poleHeight);
    ctx.lineTo(x + 20, floorHeight - poleHeight);
    ctx.closePath();
    ctx.fillStyle = "green";
    ctx.fill();

    ctx.strokeStyle = "black";
    ctx.lineWidth = 4;
    ctx.stroke();
  },
  x: 0,
  y: 0,
  w: 100,
  h: -100
};

let explodingWallBricks = [];
let obstacleExplodingWall = {
  draw: function(x, y) {
    if (explodingWallBricks.length === 0) {
      ctx.strokeStyle = "black";
      for (let i = 0; 35 * i < floorHeight; i++) {
        ctx.beginPath();
        ctx.rect(x, 10 + 35 * i, 10, -30);
        ctx.stroke();
      }
    }
  },
  x: 0,
  y: 0,
  w: 10,
  h: -canvas.height
};

/* Each obstacle in the level is given by two things:
 *
 * 1. The obstacle type
 * 2. The distance to the previous obstacle
 */
let obstacles = [
  [obstacleTrapdoor, 600],
  [obstacleSpike, 500],
  [obstacleSaw, 400],
  [electricSign, 400],
  [obstacleLaser, 500],
  [obstacleThorns, 300],
  [obstaclePole, 500],
  [toxicSign, 300],
  [obstacleSpike, 500],
  [obstacleThorns, 300],
  [obstacleExplodingWall, 200],
  [electricSign, 400],
  [toxicSign, 200],
  [obstacleSpike, 300],
  [obstacleLaser, 200],
  [obstacleSpike, 400],
  [obstacleSpike, 550],
  [obstacleSpike, 320],
  [obstacleSpike, 300],
  [obstacleTrapdoor, 600],
  [obstacleSpike, 500],
  [obstacleThorns, 300],
  [obstacleExplodingWall, 200],
  [obstaclePole, 200],
  [electricSign, 400],
  [toxicSign, 200],
  [obstacleSpike, 300],
  [obstacleLaser, 200],
  [obstacleSaw, 400],
  [obstacleSpike, 400],
  [obstacleSpike, 550],
  [obstacleSpike, 320],
  [obstacleSpike, 300],
  [obstacleTrapdoor, 600],
  [obstacleSpike, 500],
  [obstacleThorns, 300],
  [obstacleExplodingWall, 200],
  [obstaclePole, 200],
  [electricSign, 400],
  [toxicSign, 200],
  [obstacleSpike, 300],
  [obstacleLaser, 200],
  [obstacleSaw, 400],
  [obstacleSpike, 400],
  [obstacleSpike, 550],
  [obstacleSpike, 320],
  [obstacleSpike, 300]
];

let background = function(color) {
  ctx.fillStyle = color;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = 'black';
  ctx.lineWidth = '5';
  ctx.strokeRect(0, 0, canvas.width, canvas.height);
};

let resetGame = function() {
  time = 0;
  time_last = 0;
  obs_offset = 0;
};

let drawStats = function() {
  ctx.fillStyle = "white";
  ctx.font = '20px monospace';
  ctx.fillText("time           = " + time, 10, 20);
  ctx.fillText("hero position  = [" + hero.x.toFixed(0) +
    ", " + hero.y.toFixed(0) + "]", 10, 40);
  ctx.fillText("hero velocity  = " + hero.velocity.toFixed(2), 10, 60);
  ctx.fillText("booster (CTRL) = " + (hero.is_boosting ? "on" : "off"), 10, 80);
  ctx.fillText("debug (d)      = " + (debugMode ? "on" : "off"), 10, 100);
  ctx.fillText("restart (r)", 10, 120);
  ctx.fillText("= make hero jump higher", 10, 140);
  ctx.fillText("- make hero jump lower", 10, 160);
  ctx.fillText("< make hero go slower", 10, 180);
  ctx.fillText("> make hero go faster", 10, 200);
  ctx.fillText("G make gravity bigger", 10, 220);
  ctx.fillText("g make gravity smaller", 10, 240);
  if (debugMode) {
    ctx.fillText("step debug (s) = " + (debugMode ? "enabled" : "disabled"), 10, 260);
    ctx.fillText("step back (S)  = " + (debugMode ? "enabled" : "disabled"), 10, 280);
  }
  ctx.fillText("obs speed  = " + obs_speed.toFixed(2), 380, 20);
  ctx.fillText("gravity  = " + hero.g.toFixed(2), 380, 40);
  ctx.fillText("jump  = " + hero.jump_velocity.toFixed(2), 380, 60);
};

let drawGameOverSign = function() {
  ctx.fillStyle = "red";
  ctx.beginPath();
  ctx.rect(0.5 * canvas.width - 150, 0.5 * canvas.height - 100, 300, 60);
  ctx.fill();
  ctx.fillStyle = "black";
  ctx.lineWidth = 4;
  ctx.stroke();

  ctx.font = '48px serif';
  ctx.fillText("GAME OVER", 0.5 * canvas.width - 140, 0.5 * canvas.height - 55);
};

let drawBackground = function() {
  let offset = -backgroundSpeed * time;

  ctx.drawImage(snowflakeImage, offset + 300, floorHeight - 200, 100, 100);
  ctx.drawImage(dragonImage, offset + 600, floorHeight - 200, 100, 100);
  ctx.drawImage(boneImage, offset + 900, floorHeight - 200, 100, 100);
  ctx.drawImage(GriffinImage, offset + 900, floorHeight - 200, 80, 80);
};

let drawFloor = function() {
  let strokeColors = ["black", "black"];
  let fillColors = ["darkblue", "yellow"];

  for (let i = 0; - floorSpeed * time % 400 + 400 * i < canvas.width; i++) {
    for (let j = 0; j < 2; j++) {
      ctx.beginPath();
      ctx.rect(-floorSpeed * time % 400 + 400 * i + 200 * j, floorHeight, 200, 50);
      ctx.closePath();

      ctx.strokeStyle = strokeColors[j];
      ctx.fillStyle = fillColors[j];
      ctx.fill();
      ctx.stroke();
    }
  }
};

let drawBoundingBox = function(obstacle) {
  ctx.strokeStyle = "orangered";
  ctx.lineWidth = 1;
  ctx.strokeRect(obstacle.x, obstacle.y, obstacle.w, obstacle.h);

  ctx.font = '14px monospace';
  ctx.fillStyle = "white";
  ctx.textAlign = "right";
  ctx.textBaseline = "top";
  ctx.fillText("(x, y)", obstacle.x, obstacle.y);
  ctx.textAlign = "left";
  ctx.textBaseline = "top";
  ctx.fillText("(x + w, y)", obstacle.x + obstacle.w, obstacle.y);
  ctx.textAlign = "left";
  ctx.textBaseline = "bottom";
  ctx.fillText("(x + w, y + h)", obstacle.x + obstacle.w, obstacle.y + obstacle.h);
  ctx.textAlign = "right";
  ctx.textBaseline = "bottom";
  ctx.fillText("(x, y + h)", obstacle.x, obstacle.y + obstacle.h);

  ctx.textAlign = "left";
  ctx.textBaseline = "alphabetic";
};

let drawObstacleBoundingBox = function(obstacle) {
  if (debugMode) {
    if (obstacle.hasOwnProperty("drawBoundingBox")) {
      obstacles.drawBoundingBox();
    } else {
      drawBoundingBox(obstacle);
    }
  }
};

let is_overlapping = function(object1, object2) {
  if (object2.ignore_me) {
    return false;
  }
  if (object1.x + object1.w > object2.x &&
    object1.y > object2.y + object2.h &&
    object1.x < object2.x + object2.w &&
    object1.y + object1.h < object2.y) {
    return true;
  }
  return false;
};

let drawObstacles = function() {
  let obs_listPosition = 0;

  /* Calculate the offset of the obstacles. This is the amount by
   * which we shift the obstacles to the left. */
  obs_offset += obs_speed * time_difference;

  for (let i = 0; i < obstacles.length; i++) {
    // x-position summed from list.
    obs_listPosition += obstacles[i][1];

    let obs_x = obs_listPosition - obs_offset;
    let obs_y = floorHeight;
    let obs_right = obs_x + obstacles[i][0].w;

    // Draw if coordinates are within the canvas.
    if (obs_right > 0 && obs_x < canvas.width) {
      obstacles[i][0].x = obs_x;
      obstacles[i][0].y = obs_y;

      obstacles[i][0].draw(obs_x, obs_y);
      drawObstacleBoundingBox(obstacles[i][0]);

      // Detect collision.
      if (is_overlapping(hero, obstacles[i][0])) {
        drawGameOverSign();
      }
    } else {
      if (obstacles[i][0] === obstacleLaser) {
        lightningSound.muted = true;
      }
    }
  }
};

let drawSoundButton = function() {
  ctx.fillStyle = "yellow";
  ctx.fillRect(canvas.width - 230, 10, 220, 100);

  ctx.fillStyle = "black";
  ctx.font = '48px serif';
  if (lightningSound.muted) {
    ctx.fillText("Sound On", canvas.width - 220, 80);
  } else {
    ctx.fillText("Sound Off", canvas.width - 220, 80);
  }
};

let mouseClickedSoundButton = function(event) {
  if (event.clientX > canvas.width - 230 && event.clientX < canvas.width - 10 &&
    event.clientY > 10 && event.clientY < 110) {
    if (lightningSound.muted) {
      console.log("turn sound on");
      lightningSound.muted = false;
    } else {
      console.log("turn sound off");
      lightningSound.muted = true;
    }
  }
};

let drawHeroBoundingBox = function(object) {
  ctx.strokeStyle = "lightblue";
  ctx.lineWidth = "1";
  ctx.strokeRect(object.x, object.y, object.w, object.h);

  ctx.font = '14px monospace';
  ctx.fillStyle = "white";
  ctx.textAlign = "right";
  ctx.textBaseline = "top";
  ctx.fillText("(x, y)", object.x, object.y);
  ctx.textAlign = "left";
  ctx.textBaseline = "top";
  ctx.fillText("(x + w, y)", object.x + object.w, object.y);
  ctx.textAlign = "left";
  ctx.textBaseline = "bottom";
  ctx.fillText("(x + w, y + h)", object.x + object.w, object.y + object.h);
  ctx.textAlign = "right";
  ctx.textBaseline = "bottom";
  ctx.fillText("(x, y + h)", object.x, object.y + object.h);

  ctx.textAlign = "left";
  ctx.textBaseline = "alphabetic";
};

let hero = {
  draw: function() {

    /* body and color */

    ctx.beginPath();
    ctx.ellipse(this.x + 50, this.y - 50, 50, 50, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fillStyle = "rgb(105, 73, 75)";
    ctx.fill();

    /* big smile */

    ctx.fillStyle = "rgb(245, 240, 240)";
    ctx.beginPath();
    ctx.arc(this.x + 50, this.y - 50, 35, 0, Math.PI);
    ctx.closePath();
    ctx.fill();

    /* eyes and pupils */

    ctx.beginPath();
    ctx.ellipse(this.x + 30, this.y - 70, 12, 12, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fill();

    ctx.beginPath();
    ctx.ellipse(this.x + 70, this.y - 70, 12, 12, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fill();

    ctx.fillStyle = "blue";
    ctx.beginPath();
    ctx.ellipse(this.x + 30, this.y - 66, 6, 6, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fill();

    ctx.beginPath();
    ctx.ellipse(this.x + 70, this.y - 66, 6, 6, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fill();

    /* arms and fists */

    ctx.strokeStyle = "black";
    ctx.beginPath();
    ctx.moveTo(this.x, this.y - 60);
    ctx.lineTo(this.x - 20, this.y - 90);
    ctx.closePath();
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(this.x + 100, this.y - 60);
    ctx.lineTo(this.x + 120, this.y - 90);
    ctx.closePath();
    ctx.stroke();

    ctx.fillStyle = "red";
    ctx.beginPath();
    ctx.ellipse(this.x - 20, this.y - 90, 10, 10, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fill();

    ctx.beginPath();
    ctx.ellipse(this.x + 120, this.y - 90, 10, 10, 0, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fill();
  },
  is_jumping: false,
  is_boosting: false,
  velocity: 0,
  jump_velocity: 15, // The jump velocity.
  g: -0.3, // "gravity" acceleration term
  x: 190 - 50,
  y: floorHeight,
  w: 100,
  h: -100
};

let drawHero = function() {
  if (debugMode) {
    hero.x = mousePosition.x;
    hero.y = mousePosition.y;
  } else {
    if (hero.is_boosting) {
      hero.velocity -= 0.5;
      hero.is_jumping = true;
    }

    hero.velocity -= hero.g;
    hero.y += hero.velocity;

    if (hero.y > floorHeight) {
      hero.y = floorHeight;
      hero.velocity = 0;
      hero.is_jumping = false;
    }
  }
  hero.draw();
  if (debugMode) {
    drawHeroBoundingBox(hero);
  }
};

let jumpHero = function() {
  if (!debugMode) {
    if (!hero.is_jumping) {
      console.log("jump hero");
      hero.velocity = -hero.jump_velocity;
      hero.is_jumping = true;
    }
  }
};

let mouseClickedMoveHero = function(event) {
  jumpHero();
};

let mouseMoved = function(event) {
  mousePosition.x = event.clientX;
  mousePosition.y = event.clientY;
};

let powerkeyPressedMoveHero = function(event) {
  if (event.code === "ControlLeft" || event.code == "ControlRight") {
    console.log("boosting hero");
    hero.is_boosting = true;
  }
};

let debugKeyPressed = function(event) {
  if (event.code === "KeyD" && event.key === "d") {
    if (debugMode) {
      /* Reset x component of hero position. */
      hero.x = 190 - 50;
      hero.velocity = 0;
    }
    debugMode = !debugMode;
  }
};

let equalKeyPressed = function(event) {
  if (event.code === "Equal") {
    hero.jump_velocity += 1;
  }
};

let lessKeyPressed = function(event) {
  if (event.code === "Comma") {
    obs_speed -= 0.02;
  }
};

let minusKeyPressed = function(event) {
  if (event.code === "Minus") {
    hero.jump_velocity -= 1;
  }
};

let greaterKeyPressed = function(event) {
  if (event.code === "Period") {
    obs_speed += 0.02;
  }
};

let restartKeyPressed = function(event) {
  if (event.code === "KeyR" && event.key === "r") {
    resetGame();
  }
};

let stepKeyPressed = function(event) {
  if (event.code === "KeyS" && event.key === "s") {
    if (debugMode) {
      time++;
    }
  }
};

let reverseStepKeyPressed = function(event) {
  if (event.code === "KeyS" && event.key === "S") {
    if (debugMode) {
      time--;
    }
  }
};

let powerkeyReleasedMoveHero = function(event) {
  if (event.code === "ControlLeft" || event.code == "ControlRight") {
    console.log("turning hero booster off");
    hero.is_boosting = false;
  }
};

let spaceKeyPressed = function(event) {
  if (event.code === "Space" && event.key === " ") {
    jumpHero();
  }
};

let upperCaseGKeyPressed = function(event) {
  if (event.code === "KeyG" && event.key === "G") {
    hero.g = hero.g - 0.1;
  }
};

let lowerCaseGKeyPressed = function(event) {
  if (event.code === "KeyG" && event.key === "g") {
    hero.g = hero.g + 0.1;
  }
};

let mouseClickedListeners = [
  mouseClickedSoundButton,
  jumpHero
];

let mouseMoveListeners = [
  mouseMoved
];

let keyPressListeners = [
  debugKeyPressed,
  equalKeyPressed,
  greaterKeyPressed,
  lessKeyPressed,
  lowerCaseGKeyPressed,
  minusKeyPressed,
  powerkeyPressedMoveHero,
  restartKeyPressed,
  reverseStepKeyPressed,
  spaceKeyPressed,
  stepKeyPressed,
  upperCaseGKeyPressed
];

let keyReleaseListeners = [
  powerkeyReleasedMoveHero
];

(function() {
  let mouseClick = function(event) {
    console.log("mouse clicked");
    for (let i = 0; i < mouseClickedListeners.length; i++) {
      mouseClickedListeners[i](event);
    }
  };

  let mouseMove = function(event) {
    console.log("mouse moved");
    for (let i = 0; i < mouseMoveListeners.length; i++) {
      mouseMoveListeners[i](event);
    }
  };

  let keyPress = function(event) {
    console.log("pressed key '" + event.key + "', code " + event.code);
    for (let i = 0; i < keyPressListeners.length; i++) {
      keyPressListeners[i](event);
    }
  };

  let keyRelease = function(event) {
    console.log("released key '" + event.key + "', code " + event.code);
    for (let i = 0; i < keyReleaseListeners.length; i++) {
      keyReleaseListeners[i](event);
    }
  };

  let initialize = function() {
    /* The mousedown event is fired when a pointing device button is
       pressed on an element [1].

       [1] https://developer.mozilla.org/en-US/docs/Web/Events/mousedown
    */
    canvas.addEventListener('mousedown', mouseClick);
    canvas.addEventListener('touchstart', mouseClick);
    canvas.addEventListener('mousemove', mouseMove);
    document.addEventListener('keydown', keyPress);
    document.addEventListener('keyup', keyRelease);
  };

  initialize();
})();

let draw = function() {
  window.requestAnimationFrame(draw);
  background("blue");

  /* Calculate the time difference between the time in this call and
   * the time during the last call. */
  time_difference = time - time_last;
  time_last = time;

  drawStats();
  drawSoundButton();
  drawBackground();
  drawFloor();
  drawObstacles();
  drawHero();

  /* Increment time. */
  if (!debugMode) {
    time++;
  }
};

(function() {
  let initialize = function() {
    window.addEventListener('resize', resizeCanvas);
    canvas.style.position = "absolute";
    canvas.style.left = "0px";
    canvas.style.top = "0px";
    resizeCanvas();
  };

  let resizeCanvas = function() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    floorHeight = canvas.height - 50;
  };

  resetGame();
  initialize();
})();

draw();