text: class Game {
      constructor() {
          this.g = new GameInfo(this);
          this.p = this.g.player;
  
          this.registerListeners();
      }
  
      draw() {
          this.g.context.clearRect(0, 0, this.g.canvas.width, this.g.canvas.height);
          this.drawMatrix(this.p.matrix, this.p.pos);
      }
  
      drawArena() {
          this.g.contextBg.fillStyle = '#000';
          this.g.contextBg.fillRect(0, 0, this.g.canvas.width, this.g.canvas.height);
          this.drawMatrix(this.g.arena.field, {x: 0, y: 0}, this.g.contextBg);
      }
  
      drawHolding() {
          if (this.p.holdingTile === null)
              return;
          this.g.contextHold.clearRect(0, 0, this.g.canvasHold.width, this.g.canvasHold.height);
          const offset = centerOffset(this.p.holdingTile);
          const x = 3 - (this.p.holdingTile[0].length / 2) + offset.x;
          const y = 3 - (this.p.holdingTile.length / 2) + offset.y;
          this.drawMatrix(this.p.holdingTile, {x: x, y: y}, this.g.contextHold);
      }
  
      drawMatrix(matrix, offset, useContext = this.g.context) {
          matrix.forEach((row, y) => {
              row.forEach((value, x) => {
                  if (value !== 0) {
                      this.drawTile(x, y, offset, colors[value], matrix, useContext);
                  }
              });
          });
      }
  
      drawTile(x, y, offset, color, matrix, ctx = this.g.context) {
          ctx.fillStyle = color;
          x += offset.x;
          y += offset.y;
          switch (this.g.theme) {
              case "default":
                  ctx.fillRect(x + tileGap / 2, y + tileGap / 2, 1 - tileGap, 1 - tileGap);
                  break;
              case "clean":
                  ctx.fillRect(x, y, 1, 1);
                  break;
              case "modern":
                  drawRoundRect(ctx, x + tileGap / 2, y + tileGap / 2, 1 - tileGap, 1 - tileGap, .15);
                  break;
              case "snakes":
                  let r1 = .15, // top right
                      r2 = .15, // bottom right
                      r3 = .15, // bottom left
                      r4 = .15; // top left
                  // Is there a tile to the left?
                  if (matrix[y][x - 1] > 0) {
                      r3 = 0;
                      r4 = 0;
                  }
                  // Is there a tile to the right?
                  if (matrix[y][x + 1] > 0) {
                      r1 = 0;
                      r2 = 0;
                  }
                  // Is there a tile to the top?
                  if (matrix[y - 1] !== undefined && matrix[y - 1][x] > 0) {
                      r1 = 0;
                      r4 = 0;
                  }
                  // Is there a tile to the bottom?
                  if (matrix[y + 1] !== undefined && matrix[y + 1][x] > 0) {
                      r2 = 0;
                      r3 = 0;
                  }
                  drawRoundRect(ctx, x, y, 1, 1, [r1, r2, r3, r4]);
                  break;
              case "retro":
                  drawReliefRect(ctx, x, y, 1, 1, .15, color);
                  break;
              default:
                  this.g.theme = "default";
                  this.drawTile(x, y, offset, color, matrix, ctx);
                  break;
          }
      }
  
      drawUpcoming() {
          this.g.contextUpcoming.clearRect(0, 0, this.g.canvasUpcoming.width, this.g.canvasUpcoming.height);
          let offsetY = 0;
          let offset;
          this.g.upcomingTiles.forEach((tile) => {
              offset = centerOffset(tile);
              const x = 3 - (tile[0].length / 2) + offset.x;
              const y = offsetY + 3 - (tile.length / 2) + offset.y;
              this.drawMatrix(tile, {x: x, y: y}, this.g.contextUpcoming);
              offsetY += 6;
          });
      }
  
      gameOver() {
          this.g.arena.field.forEach(row => row.fill(0));
          timePassed = 0;
          this.g.lastTimeUpdate = Date.now();
          this.updateTime();
          this.p.score = 0;
          this.g.dropInterval = 1000;
          this.updateScore();
      }
  
      merge(arena, player) {
          player.matrix.forEach((row, y) => {
              row.forEach((value, x) => {
                  if (value !== 0) {
                      arena.field[y + player.pos.y][x + player.pos.x] = value;
                  }
              });
          });
          this.drawArena();
      }
  
      redrawScreen() {
          this.draw();
          this.drawArena();
          this.drawHolding();
          this.drawUpcoming();
      }
  
      registerListeners() {
          // Keyboard controls
          document.addEventListener('keydown', event => {
              Object.keys(this.g.keys).map((objKey) => {
                  const keyBind = this.g.keys[objKey];
                  if (keyBind.keys.includes(event.keyCode)) {
                      keyBind.action();
                  }
              });
          });
      }
  
      saveHighscore() {
          if (getCookie("highscore").value < this.p.score) {
              document.cookie = "highscore=" + this.p.score + "; max-age=" + 60 * 60 * 24 * 365 * 1000 + "; path=/;";
          }
      }
  
      start() {
          this.drawArena();
          this.p.addTile();
          this.p.addTile();
          this.p.addTile();
          this.p.reset();
          this.g.startTime = Date.now();
          this.update();
          this.updateScore();
      }
  
      update(time = 0) {
          if (!this.g.isPaused) {
              const deltaTime = time - this.g.lastTime;
              this.g.lastTime = time;
  
              this.g.dropCounter += deltaTime;
              if (this.g.dropCounter > this.g.dropInterval) {
                  this.p.drop();
              }
  
              this.updateTime();
  
              this.draw();
              requestAnimationFrame(this.update.bind(this));
          }
      }
  
      updateScore() {
          if (this.g.lastScore !== this.p.score) {
              scoreUpdateAni();
              this.g.lastScore = this.p.score;
              this.saveHighscore();
          }
          document.getElementById('score').innerText = this.p.score.toString();
      }
  
      updateTime() {
          timePassed += Date.now() - this.g.lastTimeUpdate;
          timeElement.innerHTML = formatMillis(timePassed);
          this.g.lastTimeUpdate = Date.now();
      }
  }