#canvas {
  background-color: black;
  color: white;
  width:512px;
  height:480px;
  position: absolute;
  margin: auto;
  padding: 0;
  top: 0; left: 0; bottom: 0; right: 0;
}
body {
  background-color: #111;
}
Array.prototype.shuffle = function(){
  o = this;
    for(var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
    return o;
};
function random(min, max, step){
  if(max === undefined){
    max = min;
    min = 0;
  }
  if(step === undefined)step = 1;
  return Math.round(Math.random()*(max-min))+min;
}
function coin(){
  return random(1) === 0;
}
function findFallSpeed(level){
  return 60-(level-1)*8
}
function findGoal(level){
  return 4*level;
}
function makeGarbage(map){
  map.shift();
  var garbage = [];
  for(var i = 0; i < mapWidth; i ++)garbage.push(8);
  garbage[random(garbage.length-1)] = 0;
  map.push(garbage);
}
function opponent(player){
  if(player.name === 'player1')return p2;
  if(player.name === 'player2')return p1;
}

function Player(id, playing, testing){
  if(playing === undefined)playing = false;
  if(testing === undefined)testing = false;
  this.score = 0;
  this.displayScore = 0;
  this.level = defaults.level;
  this.lines = 0;
  this.goal = findGoal(this.level);
  this.map = [];
  this.bag = [];
  this.bag.list = [];
  this.bag.initialized = false;
  this.held = undefined;
  this.canHold = true;
  this.name = 'player'+id;
  this.playing = playing;
  this.testing = testing;
  this.wins = 0;
  this.blink = true;
  this.blinkTimer = 0;
  this.blinkLimit = 30;
  this.garbage = 0;
  this.startGarbage = 4;
  this.ai = true;
  this.dead = false;
  this.deadTimer = 0;
  this.deadLimit = 0;
  this.idle = false;
  this.idleTimer = 0;
  this.idleLimit = 60;
  for(var i = 0; i < mapHeight; i ++){
    this.map.push([]);
    for(var j = 0; j < mapWidth; j ++){
      this.map[i][j] = 0;
    }
  }
  for(var i = 0; i < this.startGarbage; i ++)makeGarbage(this.map);
  this.killTile = function(){
    var x;
    var y;
    var clear = true;
    for(var i = 0; i < mapHeight; i ++){
      for(var j = 0; j < mapWidth; j ++){
        if(this.map[i][j] !== 0)clear = false;
      }
    }
    if(clear){
      this.dead = false;
      this.idle = true;
      return;
    }
    do {
      x = random(0, mapWidth-1);
      y = random(0, mapHeight-1);
    } while(this.map[y][x] === 0);
    this.map[y][x] = 0;
    if(this.name === 'player1')new Sprite(blockDestruction, (x-5)*blockSize, (y+6)*blockSize, blockSize, blockSize);
    if(this.name === 'player2')new Sprite(blockDestruction, (x+15)*blockSize, (y+6)*blockSize, blockSize, blockSize);
    display.flip();
  }
  this.reset = function(){
    for(var i = 0; i < mapHeight; i ++){
      for(var j = 0; j < mapWidth; j ++){
        this.map[i][j] = 0;
      }
    }
    this.idle = false;
    this.dead = false;
    this.held = undefined;
    this.fallDelayMax = findFallSpeed(this.level);
    this.garbage = 0;
    this.score = 0;
    this.displayScore = 0;
    this.level = defaults.level;
    this.lines = 0;
    this.goal  = findGoal(this.level);
    for(var i = 0; i < this.startGarbage; i ++)makeGarbage(this.map);
    bag.init(this);
    bag.draw(this);
    wasDown['left'] = false;
    wasDown['right']= false;
    wasDown['up']   = false;
    wasDown['down'] = false;
    wasDown['w'] = false;
    wasDown['a']= false;
    wasDown['s']   = false;
    wasDown['d'] = false;
    wasDown['space'] = false;
    wasDown['enter'] = false;
    this.canHold = true;
    display.flip();
  }
  this.update = function(){
    this.blinkTimer ++;
    if(this.blinkTimer >= this.blinkLimit){
      if(this.blink)this.blink = false;
      else this.blink = true;
      display.flip();
      this.blinkTimer = 0;
    }
    if(this.playing){
      if(this.idle){
        this.idleTimer ++;
        if(this.idleTimer >= this.idleLimit){
          this.idleTimer = 0;
          this.idle = false;
          if(!this.ai)this.playing = false;
          else this.reset();
        }
      }
      if(this.dead){
        this.deadTimer ++;
        if(this.deadTimer >= this.deadLimit){
          this.deadTimer = 0;
          this.killTile();
          this.killTile();
          this.killTile();
          this.killTile();
          this.killTile();
          this.killTile();
          this.killTile();
          this.killTile();
        }
      }
      if(!this.dead && !this.idle){
        if(this.shape.ai){
          this.shape.aiTimer ++;
          if(this.shape.aiTimer >= this.shape.aiLimit){
            this.shape.aiTimer = 0;
            this.shape.aiLimit = 0;
            //this.shape.aiLimit = random(5, 25);
            this.shape.goBestMove();
          }
        }
        if(this.shape.locking){
          this.shape.lockDelay --;
          if(this.shape.lockDelay < 0){
            this.shape.lockDelay = this.shape.lockDelayMax;
            this.shape.lock();
          }
        }
        this.shape.fallDelay --;
        if(this.shape.fallDelay < 0){
          this.shape.fallDelay = this.shape.fallDelayMax;
          this.shape.move(0, 1);
        }
        var beforeScore = Math.round(this.displayScore);
        this.displayScore += (this.score-this.displayScore)/1;
        var afterScore = Math.round(this.displayScore);
        if(beforeScore != afterScore)display.flip();
      }
    }
  }
}

var mapWidth  = 10;
var mapHeight = 22;
var frameRate = 100/6;
var defaults = {};
defaults.level = 1;
defaults.lockDelay = 10;
var p1 = new Player(1, true);
var p2 = new Player(2, true);

var sprites = [];
var paused = false;
function oob(x, y, map, top){
  if(top === undefined)top = false;
  if(!top)return x < 0 || x > map[0].length-1 || y < 0 || y > map.length-1;
  return x < 0 || x > map[0].length-1 || y > map.length-1;
}
var display = {};
display.width  = 32;
display.height = 30;
display.flip = function(){
  ctx.fillStyle = 'black';
  ctx.fill();
  for(var i = 1; i < display.height-9; i ++){
    for(var j = 0; j < display.width; j ++){
      ctx.drawImage(bg, j*blockSize, i*blockSize, blockSize, blockSize);
    }
  }
  var gridOffsetX = 0;
  var gridOffsetY = 1;
  var x;
  var y;
  for(var i = 0; i < display.height; i ++){
    gridY = i+gridOffsetY;
    if(gridY < mapHeight){
      for(var j = 0; j < display.width; j ++){
        gridX = j+gridOffsetX;
        if(gridX < mapWidth){ 
          x = blockSize
          y = blockSize*6;  
          if(p1.playing){
            k = p1.map[gridY][gridX];
            if(paused)k = 0;
            if(k > 0 && !p1.dead && !p1.idle){
              for(var l = 0; l < p1.shape.destinations.length; l ++){
                var t = p1.shape.destinations[l];
                if(gridX === t.x && gridY === t.y){
                  ctx.drawImage(blockDestination, t.x*blockSize+x, t.y*blockSize+y, blockSize, blockSize);
                }
              }
            }
            drawBlock(gridX*blockSize+x, gridY*blockSize+y, k);
            if(k == 0 && !paused && !p1.dead && !p1.idle){
              for(var l = 0; l < p1.shape.destinations.length; l ++){
                var t = p1.shape.destinations[l];
                if(gridX === t.x && gridY === t.y){
                  ctx.drawImage(blockDestination, t.x*blockSize+x, t.y*blockSize+y, blockSize, blockSize);
                }
              }
            }
          } else {
            k = 0;
            drawBlock(gridX*blockSize+x, gridY*blockSize+y, k);
          }
          x = blockSize*21
          y = blockSize*6;
          if(p2.playing){
            if(p2.shape != undefined){
              k = p2.map[gridY][gridX];
              if(paused)k = 0;
              if(k > 0 && !p2.dead && !p2.idle){
                for(var l = 0; l < p2.shape.destinations.length; l ++){
                  var t = p2.shape.destinations[l];
                  if(gridX === t.x && gridY === t.y){
                    ctx.drawImage(blockDestination, t.x*blockSize+x, t.y*blockSize+y, blockSize, blockSize);
                  }
                }
              }
              drawBlock(gridX*blockSize+x, gridY*blockSize+y, k);
              if(k == 0 && !paused && !p2.dead && !p2.idle){
                for(var l = 0; l < p2.shape.destinations.length; l ++){
                  var t = p2.shape.destinations[l];
                  if(gridX === t.x && gridY === t.y){
                    ctx.drawImage(blockDestination, t.x*blockSize+x, t.y*blockSize+y, blockSize, blockSize);
                  }
                }
              }
            }
          } else {
            k = 0;
            drawBlock(gridX*blockSize+x, gridY*blockSize+y, k);
          }
        }
      }
    }
  }
  ctx.drawImage(matrix, 0, blockSize*6, (mapWidth+2)*blockSize, (mapHeight+gridOffsetY)*blockSize);
  ctx.drawImage(matrix, blockSize*20, blockSize*6, (mapWidth+2)*blockSize, (mapHeight+gridOffsetY)*blockSize);
  x = (mapWidth+2)*blockSize;

  drawString(208, 352, 'WINS');
  drawString(208, 400, 'LEVEL');
  drawString(208, 448, 'GOAL');

  ctx.drawImage(box, 0, 0, 96, 96);
  ctx.drawImage(box, blockSize*6, 0, 96, 96);
  if(p1.playing && p1.bag.initialized)drawBlockSample(blockSize, blockSize, p1.bag.next);
  if(p1.held !== undefined){
    drawBlockSample(blockSize*7, blockSize, p1.held);
  }
  drawString(blockSize, blockSize, 'NEXT');
  drawString(blockSize*7, blockSize, 'HOLD');
  drawNumber(blockSize, 464, Math.round(p1.displayScore), 8);
  drawNumber(208, 368, p1.wins, 2);
  drawNumber(208, 416, p1.level, 2);
  drawNumber(208, 464, p1.goal-p1.lines, 2);

  
  ctx.drawImage(box, (display.width-6)*blockSize, 0, 96, 96);
  ctx.drawImage(box, (display.width-12)*blockSize, 0, 96, 96);
  if(p2.playing && p2.bag.initialized)drawBlockSample((display.width-5)*blockSize, blockSize, p2.bag.next);
  if(p2.held !== undefined){
    drawBlockSample((display.width-11)*blockSize, blockSize, p2.held);
  }
  drawString((display.width-5)*blockSize, blockSize, 'NEXT');
  drawString((display.width-11)*blockSize, blockSize, 'HOLD');
  drawNumber((display.width-9)*blockSize, 464, Math.round(p2.displayScore), 8);
  drawNumber(272, 368, p2.wins, 2);
  drawNumber(272, 416, p2.level, 2);
  drawNumber(272, 464, p2.goal-p2.lines, 2);

  if(!paused){
    for(var i = 0; i < sprites.length; i ++){
      var t = sprites[i];
      ctx.drawImage(t.sheet, t.spriteX, t.spriteY, t.width, t.height, t.x+6*blockSize, t.y, t.width, t.height);
    }
  } else {
    drawString(blockSize*3, blockSize*17, 'PAUSED');
    drawString(blockSize*23, blockSize*17, 'PAUSED');
  }
  if(p1.ai)drawString(blockSize*4, blockSize*10, 'DEMO');
  if((p1.ai || !p1.playing) && p1.blink && !paused){
    drawString(blockSize, blockSize*17, 'PUSH SPACE');
  }
  if(p2.ai)drawString(blockSize*24, blockSize*10, 'DEMO');
  if((p2.ai || !p2.playing) && p2.blink && !paused){
    drawString(blockSize*21, blockSize*17, 'PUSH ENTER');
  }
  if((p1.dead || p1.idle) && !p1.ai)drawString(blockSize, blockSize*17, 'GAME OVER!');
  if((p2.dead || p2.idle) && !p2.ai)drawString(blockSize*21, blockSize*17, 'GAME OVER!');
  drawString(blockSize*13, 0, 'TETRIS');
}

var blockIndex = 1;
var blockSize = 16;

var blockDestruction = new Image();
blockDestruction.src = 'https://i.imgur.com/3K3A2SG.png';
var blockDestination = new Image();
blockDestination.src = 'https://i.imgur.com/JUR9rFL.png';

var blockSamples = new Image();
blockSamples.src = 'https://i.imgur.com/g548Q3O.png';
var blockSamplesCoords = {
  'I':{x:0,  y:0, width:4, height:4},
  'O':{x:4,  y:0, width:4, height:4},
  'J':{x:8,  y:0, width:4, height:4},
  'L':{x:12, y:0, width:4, height:4},
  'Z':{x:16, y:0, width:4, height:4},
  'T':{x:20, y:0, width:4, height:4},
  'S':{x:24, y:0, width:4, height:4},
}
function drawBlockSample(x, y, name){
  ctx.drawImage(
    blockSamples, 
    blockSamplesCoords[name].x*blockSize, 
    blockSamplesCoords[name].y*blockSize, 
    blockSamplesCoords[name].width*blockSize, 
    blockSamplesCoords[name].height*blockSize, 
    x, 
    y, 
    blockSamplesCoords[name].width*blockSize, 
    blockSamplesCoords[name].height*blockSize
    );
}

var matrix = new Image();
matrix.src = 'https://i.imgur.com/wGcmvNU.png';

var box = new Image();
box.src = 'https://i.imgur.com/p0fWLYe.png';

var chars = new Image();
chars.src = 'https://i.imgur.com/87f3UkH.png';
var charsCoords = {};
var charsOrder = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ! ';
var x = 0;
var y = 0;
var charsWidth = 160;
var charsHeight= 72;
for(var i = 0; i < charsOrder.length; i ++){
  if(x > charsWidth/blockSize-1){
    x = 0;
    y ++;
  }
  var c = charsOrder.charAt(i);
  charsCoords[c] = {};
  charsCoords[c].x = x;
  charsCoords[c].y = y;
  x ++;
}
function drawString(x, y, string){
  for(var i = 0; i < string.length; i ++){
    var c = string.charAt(i);
    if(c != ' '){
      ctx.drawImage(
        chars, 
        charsCoords[c].x*blockSize, 
        charsCoords[c].y*(blockSize+2), 
        blockSize, 
        blockSize,
        x+blockSize*i, 
        y, 
        blockSize, 
        blockSize
        );
    }
  }
}
function drawNumber(x, y, number, place){
  string = number.toString();
  if(string.length > place){
    string = '';
    for(var i = 0; i < place; i ++)string += '0';
  }
  while(string.length < place)string = '0'+string;
  for(var i = 0; i < string.length; i ++){
    var c = string.charAt(i);
    ctx.drawImage(chars, charsCoords[c].x*blockSize, charsCoords[c].y*(blockSize+2), blockSize, blockSize, x+blockSize*i, y, blockSize, blockSize)
  }
}

var bg = new Image();
bg.src = 'https://i.imgur.com/zeQpekQ.png';

var blocks = new Image();
blocks.src = 'https://i.imgur.com/GrIgPHU.png';
function drawBlock(x, y, index){
  ctx.drawImage(blocks, blockSize*index, 0, blockSize, blockSize, x, y, blockSize, blockSize)
}

var canvas = document.createElement('canvas');
canvas.id = 'canvas';
canvas.width = blockSize*display.width;
canvas.height = blockSize*display.height;
document.body.appendChild(canvas);

var ctx = canvas.getContext('2d');
ctx.rect(0, 0, canvas.width, canvas.height);

var origin = {x:3, y:0};

var colors = {'red':1, 'orange':2, 'yellow':3, 'green':4, 'cyan':5, 'blue':6, 'purple':7};
var shapeNames = ['O', 'T', 'J', 'L', 'I', 'S', 'Z'];
var bag = {};
bag.init = function(player){
  player.bag.initialized = true;
  player.bag.list = [];
  player.bag.list.length = 0;
  for(var i = 0; i < shapeNames.length; i ++)player.bag.list.push(shapeNames[i]);
  player.bag.list.shuffle();
  for(var i = 0; i < random(0, shapeNames.length-1); i ++)player.bag.list.shift();
  player.bag.next = player.bag.list[0];
}
bag.draw = function(player){
  new Shape(player.bag.next, player);
  player.bag.list.shift();
  if(player.bag.list.length === 0){
    for(var i = 0; i < shapeNames.length; i ++)player.bag.list.push(shapeNames[i]);
    player.bag.list.shuffle();
  }
  player.bag.next = player.bag.list[0];
}
var shapes = {
  'O':{tiles:[[{x:1, y:0}, {x:2, y:0}, {x:1, y:1}, {x:2, y:1}]], color:'orange'},
  'T':{tiles:[
    [{x:1, y:0}, {x:0, y:1}, {x:1, y:1}, {x:2, y:1}],
    [{x:1, y:0}, {x:1, y:1}, {x:2, y:1}, {x:1, y:2}],
    [{x:0, y:1}, {x:1, y:1}, {x:2, y:1}, {x:1, y:2}],
    [{x:1, y:0}, {x:0, y:1}, {x:1, y:1}, {x:1, y:2}],
    ], color:'blue'},
  'J':{tiles:[
    [{x:0, y:0}, {x:0, y:1}, {x:1, y:1}, {x:2, y:1}],
    [{x:1, y:0}, {x:2, y:0}, {x:1, y:1}, {x:1, y:2}],
    [{x:0, y:1}, {x:1, y:1}, {x:2, y:1}, {x:2, y:2}],
    [{x:1, y:0}, {x:1, y:1}, {x:0, y:2}, {x:1, y:2}],
    ], color:'yellow'},
  'L':{tiles:[
    [{x:2, y:0}, {x:0, y:1}, {x:1, y:1}, {x:2, y:1}],
    [{x:1, y:0}, {x:1, y:1}, {x:1, y:2}, {x:2, y:2}],
    [{x:0, y:1}, {x:1, y:1}, {x:2, y:1}, {x:0, y:2}],
    [{x:0, y:0}, {x:1, y:0}, {x:1, y:1}, {x:1, y:2}],
    ], color:'green'},
  'I':{tiles:[
    [{x:0, y:1}, {x:1, y:1}, {x:2, y:1}, {x:3, y:1}],
    [{x:2, y:0}, {x:2, y:1}, {x:2, y:2}, {x:2, y:3}],
    [{x:0, y:2}, {x:1, y:2}, {x:2, y:2}, {x:3, y:2}],
    [{x:1, y:0}, {x:1, y:1}, {x:1, y:2}, {x:1, y:3}],
    ], color:'red'},
  'S':{tiles:[
    [{x:1, y:0}, {x:2, y:0}, {x:0, y:1}, {x:1, y:1}],
    [{x:1, y:0}, {x:1, y:1}, {x:2, y:1}, {x:2, y:2}],
    [{x:1, y:1}, {x:2, y:1}, {x:0, y:2}, {x:1, y:2}],
    [{x:0, y:0}, {x:0, y:1}, {x:1, y:1}, {x:1, y:2}],
    ], color:'purple'},
  'Z':{tiles:[
    [{x:0, y:0}, {x:1, y:0}, {x:1, y:1}, {x:2, y:1}],
    [{x:2, y:0}, {x:1, y:1}, {x:2, y:1}, {x:1, y:2}],
    [{x:0, y:1}, {x:1, y:1}, {x:1, y:2}, {x:2, y:2}],
    [{x:1, y:0}, {x:0, y:1}, {x:1, y:1}, {x:0, y:2}],
    ], color:'cyan'},
};
function Sprite(sheet, x, y, width, height, interval, loop){
  var self = this;
  if(interval === undefined)interval = 'none';
  if(loop === undefined)loop = false;
  this.x = x;
  this.y = y;
  this.spriteX = this.width*this.index;
  this.spriteY = 0;
  this.sheet = sheet;
  this.width = width;
  this.height= height;
  this.index = 0;
  this.frames = this.sheet.width/this.width;
  this.loop  = loop;
  this.kill = function(){
    for(var i = 0; i < sprites.length; i ++){
      var t = sprites[i];
      if(t.width === this.width && t.height === this.height && t.index === this.index && t.frames === this.frames){
        sprites.splice(i, 1);
        clearInterval(this.interval);
        return;
      }
    }
  }
  this.update = function(){
    this.spriteX = this.width*this.index;
    this.index ++;
    if(this.index == this.frames){
      if(loop)this.index = 0;
      else this.kill();
    }
  }
  sprites.push(this);
  this.update();
}
function calculateScore(map){
  var score = 0;
  var aggregateHeight = 0;
  var completeLines = 0;
  var holes = 0;
  var holeDepth = 0;
  var bumpiness = 0;
  var column = [];
  var segments = [];
  var tiles = [];
  var chainMap = [];
  for(var i = 0; i < mapHeight; i ++){
    chainMap.push([]);
    for(var j = 0; j < mapWidth; j ++)chainMap[i].push({filled:false});
  }
  for(var i = 0; i < mapHeight; i ++){
    for(var j = 0; j < mapWidth; j ++){
      if(map[i][j] === 0)break;
      if(j === mapWidth-1){
        completeLines ++;
        // map.splice(i, 1);
        // var temp = [];
        // map.unshift(temp);
        // for(var k = 0; k < mapWidth; k ++)temp.push(0);
      }
    }
  }
  if(completeLines > 0){
    for(var i = 0; i < mapHeight; i ++){
      for(var j = 0; j < mapWidth; j ++){
        var t;
        if(oob(j, i, map))t = 0;
        else t = map[i][j];
        if(t != 0 && !chainMap[i][j].filled){
          floodFill(j, i, map, chainMap, tiles);
          segments.push(tiles)
          tiles = [];
          tiles.length = 0;
        }
      }
    }
    for(var i = 0; i < segments.length; i ++){
      var s = segments[i];
      for(var j = 0; j < s.length; j ++){
        var t = s[j];
        map[t.y][t.x] = 0;
      }
      var done = false;
      var diters = 0;
      while(!done){
        diters ++;
        for(var j = 0; j < s.length; j ++){
          t = s[j];
          t.y ++;
        }
        for(var j = 0; j < s.length; j ++){
          t = s[j];
          if(t.y > mapHeight-1 || map[t.y][t.x] != 0){
            done = true;
          }
        }
        if(done){
          for(var j = 0; j < s.length; j ++){
            t = s[j];
            t.y --;
          }
        }
      }
      for(var j = 0; j < s.length; j ++){
        var t = s[j];
        map[t.y][t.x] = t.id;
      }
    }
  }
  for(var j = 0; j < mapWidth; j ++){
    column[j] = {};
    column[j].height = 0;
    for(var i = 0; i < mapHeight; i ++){
      if(map[i][j] > 0){
        column[j].height = mapHeight-i;
        aggregateHeight += column[j].height;
        break;
      }
    }
  }
  for(var j = 1; j < mapWidth; j ++){
    bumpiness += Math.abs(column[j].height-column[j-1].height);
  }
  for(var j = 0; j < mapWidth; j ++){
    column[j].holes = 0;
    column[j].holeDepth = 0;
    var streak = 0;
    var depth = 0;
    for(var i = mapHeight-1; i >= 0; i --){
      if(map[i][j] > 0){
        column[j].holes += streak;
        streak = 0;
      } else {
        streak ++;
      }
    }
    if(column[j].holes > 0){
      for(var i = mapHeight-1; i >= 0; i --){
        if(map[i][j] == 0){
          column[j].holeDepth = Math.abs(column[j].height-(mapHeight - 1 - i)-1);
          break;
        }
      }
    }
    holes += column[j].holes;
    holeDepth += column[j].holeDepth;
  }
  //score = -0.66569*aggregateHeight + -0.46544*bumpiness + 0.99275*completeLines + -0.24077*holes;
  if(aggregateHeight >= mapHeight-4 || holes > 0)score = -aggregateHeight + -bumpiness + completeLines + -100000*holes;
  else score = -aggregateHeight + -bumpiness + 100*completeLines + -10000*holes;
  for(var j = 0; j < mapWidth; j ++)if(map[0][j] > 0)score = -999999999;
  return {total:score, aggregateHeight:aggregateHeight, bumpiness:bumpiness, completeLines:completeLines, holes:holes, holeDepth:holeDepth};
}
function floodFill(x, y, map, chainMap, tiles){
  chainMap[y][x].filled = true;
  tiles.push({x:x, y:y, id:map[y][x]});
  for(var i = -1; i <= 1; i ++){
    var ry = y+i;
    for(var j = -1; j <= 1; j ++){
      var rx = x+j;
      if(Math.abs(x-rx) + Math.abs(y-ry) === 1 && !oob(rx, ry, map) && map[ry][rx] !== 0 && !chainMap[ry][rx].filled){
        floodFill(rx, ry, map, chainMap, tiles);
      }
    }
  }
}
function Shape(type, owner){
  owner.shape = this;
  this.x = origin.x;
  this.y = origin.y;
  this.width = 0;
  this.height = 0;
  this.type = type;
  this.rotationIndex = 0;
  this.initializing = true;
  this.destinations = [];
  this.segments = [];
  this.tiles = [];
  this.chainMap = [];
  this.fallDelayMax = findFallSpeed(owner.level);
  this.fallDelay = this.fallDelayMax;
  this.lockDelayMax = defaults.lockDelay;
  this.lockDelay = this.lockDelayMax;
  this.locked = false;
  this.locking = false;
  this.o = opponent(owner);
  this.ai = owner.ai;
  this.aiTimer = 0;
  this.aiLimit = 5;
  this.aiRotates = 0;
  this.aiRotatesMax = 4;
  this.bestMove = undefined;
  this.findBest = function(type, map, tetris, lookahead){
    if(lookahead === undefined)lookahead = false;
    this.clear();
    var moves = [];
    var valid = false;
    var startX = this.x;
    var startY = this.y;
    var tempMap = [];
    for(var i = 0; i < mapHeight; i ++){
      tempMap.push([]);
      for(var j = 0; j < mapWidth; j ++){
        tempMap[i].push(map[i][j]);
      }
    }
    var beforeScore = calculateScore(tempMap);
    if(beforeScore.aggregateHeight > 7*mapWidth){
      tetris = false;
      //console.log('Stack is too tall; temporarily deactivating TETRIS mode')
    }
    if(beforeScore.holes > 0){
      tetris = false;
      //console.log('There are holes in the stack; temporarily deactivating TETRIS mode')
    }
    for(var j = 0; j < shapes[type].tiles.length; j ++){
      this.x = -2;
      while(this.x < mapWidth){
        var destinations = [];
        destinations.length = 0;
        valid = true;
        for(var i = 0; i < shapes[type].tiles[j].length; i ++){
          var t = shapes[type].tiles[j][i];
          var x = t.x+this.x;
          var y = t.y+this.y;
          if(oob(x, y, tempMap) || (tetris && x == mapWidth-1) || tempMap[y][x] > 0){
            valid = false;
            break;
          }
        }
        if(valid){
          var airborne = true;
          var tx = this.x;
          var ty = this.y;
          while(airborne){
            ty += 1;
            for(var i = 0; i < shapes[type].tiles[j].length; i ++){
              var t = shapes[type].tiles[j][i];
              if(t.y+ty < 0)continue;
              if(t.y+ty > mapHeight-1 || tempMap[t.y+ty][t.x+tx] !== 0){
                airborne = false;
                ty -= 1;
                break;
              }
            }
          }
          for(var i = 0; i < shapes[type].tiles[j].length; i ++){
            var t = shapes[type].tiles[j][i];
            var x = t.x+tx;
            var y = t.y+ty;
            tempMap[y][x] = colors[shapes[type].color];
            destinations.push({x:x, y:y});
          }
          prospect = {spots:destinations, x:tx, y:ty, rotation:j, score:calculateScore(tempMap)};
          moves.push(prospect);
          for(var i = 0; i < destinations.length; i ++){
            var t = destinations[i];
            tempMap[t.y][t.x] = 0;
          }
        }
        this.x ++;
      }
    }
    if(lookahead){
      for(var i = 0; i < moves.length; i ++){
        var m = moves[i];
        for(var j = 0; j < m.spots; j ++){
          var s = m.spots[j];
          tempMap[s.y][s.x] = 8;
          m.score += findBest(owner.bag.next, tempMap, tetris, false).score;
          tempMap[s.y][s.x] = 0;
        }
      }
    }
    var bestMove = {};
    var scores = [];
    for(var i = 0; i < moves.length; i ++){
      var m = moves[i];
      var score = m.score.total;
      scores.push(score);
    }
    scores.sort(function(a, b){return b-a});
    for(var i = 0; i < moves.length; i ++){
      if(moves[i].score.total === scores[0])bestMove = {x:moves[i].x, y:moves[i].y, rotation:moves[i].rotation, score:moves[i].score};
    }
    this.x = startX;
    this.y = startY;
    this.restore();
    if(bestMove.score.holes > beforeScore.holes && tetris){
      //console.log('Best move creates a hole; temporarily deactivating TETRIS mode.')
      return this.findBest(type, map, false, lookahead)
    }
    return bestMove;
  }
  this.goBestMove = function(){
    var moved = false;
    var lookahead = owner.name === 'player1';
    if(this.bestMove === undefined){
      this.bestMove = this.findBest(this.type, owner.map, true, lookahead);
    }
    if(!owner.testing){
      if(this.rotationIndex != this.bestMove.rotation){
        this.rotate(1);
        moved = true;
      }
      if(this.x < this.bestMove.x)moved = this.move( 1, 0);
      else
      if(this.x > this.bestMove.x)moved = this.move(-1, 0);
      if(this.x === this.bestMove.x && this.rotationIndex == this.bestMove.rotation){
        this.dash( 0, 1);
        this.lock();
        moved = true;
      }
      if(!moved){
        if(this.aiRotates >= this.aiRotatesMax){
          this.bestMove = undefined;
        } else {
          this.rotate(1);
          this.aiRotates ++;
          //console.log("Move blocked, rotating")
        }
      }
    } else {
      this.clear();
      this.x = this.bestMove.x;
      this.rotationIndex = this.bestMove.rotation;
      this.restore();
      this.update();
      this.dash( 0, 1);
      this.lock();
    }

  }
  this.check = function(){
    // for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
    //  var t = shapes[this.type].tiles[this.rotationIndex][i];
    //  var x = t.x+this.x;
    //  var y = t.y+this.y;
    //  if(oob(x, y, owner.map) || owner.map[y][x] > 0){
    //    this.lock();
    //  }
    // }
  }
  this.clear = function(){
    for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
      var t = shapes[this.type].tiles[this.rotationIndex][i];
      var x = t.x+this.x;
      var y = t.y+this.y;
      if(!oob(x, y, owner.map)){
        owner.map[y][x] = 0;
      }
    }
  }
  this.restore = function(){
    for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
      var t = shapes[this.type].tiles[this.rotationIndex][i];
      var x = t.x+this.x;
      var y = t.y+this.y;
      if(!oob(x, y, owner.map)){
        owner.map[y][x] = colors[shapes[this.type].color];
      }
    }
  }
  this.update = function(){
    this.clear();
    var tx = this.x;
    var ty = this.y;
    this.destinations = [];
    this.destinations.length = 0;
    var valid = true;
    while(valid){
      ty += 1;
      for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
        var t = shapes[this.type].tiles[this.rotationIndex][i];
        if(t.y+ty < 0)continue;
        if(t.y+ty > mapHeight-1 || owner.map[t.y+ty][t.x+tx] !== 0){
          valid = false;
          ty -= 1;
          break;
        }
      }
    }
    for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
      var t = shapes[this.type].tiles[this.rotationIndex][i];
      var x = t.x+this.x;
      var y = t.y+this.y;
      if(!oob(x, y, owner.map)){
        owner.map[y][x] = colors[shapes[this.type].color];
      }
      this.destinations.push({x:t.x+tx, y:t.y+ty});
    }
    this.finalX = tx;
    this.finalY = ty;
    display.flip();
  }
  this.hold = function(){
    if(owner.canHold){
      this.lockDelay = this.lockDelayMax;
      for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
        var t = shapes[this.type].tiles[this.rotationIndex][i];
        var x = t.x+this.x;
        var y = t.y+this.y;
        if(!oob(x, y, owner.map)){
          owner.map[y][x] = 0;
        }
      }
      if(owner.held === undefined){
        owner.held = this.type;
        bag.draw(owner);
      } else {
        new Shape(owner.held, owner);
        owner.held = this.type;
      }
      owner.canHold = false;
      display.flip();
    }
  }
  this.size = function(){
    this.width = 0;
    this.height = 0;
    var x1 = 0;
    var x2 = 0;
    var y1 = 0;
    var y2 = 0;
    for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
      var t = shapes[this.type].tiles[this.rotationIndex][i];
      if(t.x < x1)x1 = t.x;
      if(t.x > x2)x2 = t.x;
      if(t.y < y1)y1 = t.y;
      if(t.y > y2)y2 = t.y;
    }
    this.width = Math.abs(x1-x2);
    this.height = Math.abs(y1-y2);
  }
  this.rotate = function(difference){
    if(difference === undefined)difference = 1;
    var targetIndex = this.rotationIndex+difference;
    if(targetIndex >= shapes[this.type].tiles.length)targetIndex = 0;
    if(targetIndex < 0)targetIndex = shapes[this.type].tiles.length-1;
    for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
      var t = shapes[this.type].tiles[this.rotationIndex][i];
      var x = t.x+this.x;
      var y = t.y+this.y;
      if(!oob(x, y, owner.map)){
        owner.map[y][x] = 0;
      }
    }
    var ox = 0;
    var oy = 0;
    var iters = 0;
    var outside = true;
    var valid = true;
    while(outside){
      outside = false;
      for(var i = 0; i < shapes[this.type].tiles[targetIndex].length; i ++){
        var t = shapes[this.type].tiles[targetIndex][i];
        if(oob(t.x+this.x+ox, t.y+this.y+oy, owner.map) || (t.y+this.y+oy !== -1 && owner.map[t.y+this.y+oy][t.x+this.x+ox] !== 0)){
          outside = true;
          break;
        }
      }
      if(!outside){
        valid = true;
        break;
      }
      if(iters === 0){
        ox = 0;
        oy = -1;
      }else
      if(iters === 1){
        ox = 0;
        oy = 1;
      }else
      if(iters === 2){
        ox = -1;
        oy = 0;
      }else
      if(iters === 3){
        ox = 1;
        oy = 0;
      }else
      if(iters === 4){
        ox = -2;
        oy = 0;
      }else
      if(iters === 5){
        ox = 2;
        oy = 0;
      }else{
        valid = false;
        break;
      }
      iters ++;
    }
    if(valid){
      this.lockDelay = this.lockDelayMax;
      this.locking = false;
      this.x += ox;
      this.y += oy;
      this.rotationIndex = targetIndex;
      this.size();
    }
    this.update();
    wasDown['up'] = false;
    wasDown['w'] = false;
  }
  this.dash = function(dx, dy){
    if(dx === 0 && dy > 0){
      for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
        var t = shapes[this.type].tiles[this.rotationIndex][i];
        var x = t.x+this.x;
        var y = t.y+this.y;
        if(!oob(x, y, owner.map)){
          owner.map[y][x] = 0;
        }
      }
      owner.score += Math.abs(this.y-this.finalY)*2;
      this.x = this.finalX;
      this.y = this.finalY;
      this.update();
    } else {
      while(true)if(!this.move(dx, dy))break;
      if(!this.locked)this.update();
    }
  }
  this.move = function(dx, dy, points){
    if(points === undefined)points = 0;
    var moved = false;
    if(this.locked){
      return;
    }
    var tx = this.x + dx;
    var ty = this.y + dy;
    var valid = true;
    this.clear();
    for(var i = 0; i < shapes[this.type].tiles[this.rotationIndex].length; i ++){
      var t = shapes[this.type].tiles[this.rotationIndex][i];
      if(oob(t.x+tx, t.y+ty, owner.map, true) || (t.y+this.y !== -1 && owner.map[t.y+ty][t.x+tx] !== 0)){
        if(dy === 1 && (t.y+ty > mapHeight-1 || owner.map[t.y+ty][t.x+tx] !== 0)){
          this.locking = true;
          wasDown['down'] = false;
          wasDown['s'] = false;
        }
        valid = false;
        break;
      }
    }
    if(valid){
      if(dx === 0 && dy === 1)owner.score += points;
      this.x = tx;
      this.y = ty;
      moved = true;
      this.lockDelay = this.lockDelayMax;
      this.locking = false;
      this.locked = false;
    }
    this.update();
    if(!(dx === 0 && dy === 0))if(this.initializing)this.initializing = false;
    return moved;
  }
  this.lock = function(){
    this.locked = true;
    this.locking = false;
    var gameOver = false;
    var chain = 0;
    var checking = true;
    var iters = 0;
    while(checking){
      iters ++;
      checking = false;
      for(var i = 0; i < mapHeight; i ++){
        var k = 0;
        for(var j = 0; j < mapWidth; j ++){
          var t;
          if(oob(j, i, owner.map))t = 0;
          else t = owner.map[i][j];
          if(t != 0){
            if(i === 0){
              gameOver = true;
            }
          }
          if(t != 0 && k === j)k ++;
          else k = 0;
          if(j === mapWidth-1 && k > 0){
            chain ++;
            owner.goal --;
            owner.map.splice(i, 1);
            var temp = [];
            owner.map.unshift(temp);
            for(var l = 0; l < mapWidth; l ++){
              temp.push(0);
              if(owner.name === 'player1')new Sprite(blockDestruction, (l-5)*blockSize, (i+6)*blockSize, blockSize, blockSize);
              if(owner.name === 'player2')new Sprite(blockDestruction, (l+15)*blockSize, (i+6)*blockSize, blockSize, blockSize);
            }
            checking = true;
          }
        }
      }
      if(chain > 0){
        this.segments = [];
        this.segments.length = 0;
        this.tiles = [];
        this.tiles.length = 0;
        this.chainMap = [];
        this.chainMap.length = 0;
        for(var i = 0; i < mapHeight; i ++){
          this.chainMap.push([]);
          for(var j = 0; j < mapWidth; j ++)this.chainMap[i].push({filled:false});
        }
        for(var i = 0; i < mapHeight; i ++){
          for(var j = 0; j < mapWidth; j ++){
            var t;
            if(oob(j, i, owner.map))t = 0;
            else t = owner.map[i][j];
            if(t != 0 && !this.chainMap[i][j].filled){
              floodFill(j, i, owner.map, this.chainMap, this.tiles);
              this.segments.push(this.tiles)
              this.tiles = [];
              this.tiles.length = 0;
            }
          }
        }
        for(var i = 0; i < this.segments.length; i ++){
          var s = this.segments[i];
          for(var j = 0; j < s.length; j ++){
            var t = s[j];
            owner.map[t.y][t.x] = 0;
          }
          var done = false;
          var diters = 0;
          while(!done){
            diters ++;
            for(var j = 0; j < s.length; j ++){
              t = s[j];
              t.y ++;
            }
            for(var j = 0; j < s.length; j ++){
              t = s[j];
              if(t.y > mapHeight-1 || owner.map[t.y][t.x] != 0){
                done = true;
              }
            }
            if(done){
              for(var j = 0; j < s.length; j ++){
                t = s[j];
                t.y --;
              }
            }
          }
          for(var j = 0; j < s.length; j ++){
            var t = s[j];
            owner.map[t.y][t.x] = t.id;
          }
        }
      }
    }
    if(gameOver){
      gameOver = false;
      owner.dead = true;
      if(this.o.playing && !(this.o.ai && !owner.ai) && !(owner.ai && !this.o.ai)){
        this.o.dead = true;
        this.o.wins ++;
      }
      return;
    } else {
      if(chain === 1)owner.score += 100 * owner.level;
      if(chain === 2)owner.score += 300 * owner.level;
      if(chain === 3)owner.score += 500 * owner.level;
      if(chain === 4)owner.score += 800 * owner.level;
      if(chain >=  5)owner.score += 1000 * (chain-4) * owner.level;
      if(this.o.playing){
        if(chain >= 2){
          if((owner.ai && this.o.ai) || (!owner.ai && !this.o.ai)){
            this.o.garbage += chain-1;
          }
        }
      }
      if(owner.goal <= 0){
        owner.goal = findGoal(owner.level) - owner.goal;
        owner.level ++;
        this.fallDelayMax = findFallSpeed(owner.level);
      }
      while(owner.garbage > 0){
        owner.garbage --;
        makeGarbage(owner.map);
      }
      display.flip();
      owner.canHold = true;
      bag.draw(owner);
      wasDown['left'] = false;
      wasDown['right']= false;
      wasDown['up']   = false;
      wasDown['down'] = false;
      wasDown['w'] = false;
      wasDown['a']= false;
      wasDown['s']   = false;
      wasDown['d'] = false;
    }
  }
  this.size();
  this.check();
  this.update();
}
keyDown = {};
keyDown['left'] = false;
keyDown['right']= false;
keyDown['up']   = false;
keyDown['down'] = false;
keyDown['p']  = false;
keyDown['w']  = false;
keyDown['a']  = false;
keyDown['s']    = false;
keyDown['q']  = false;
keyDown['\'']   = false;
keyDown['leftShift']= false;
keyDown['rightShift']= false;
keyDown['space']= false;
keyDown['enter']= false;
wasDown = {};
wasDown['left'] = false;
wasDown['right']= false;
wasDown['up']   = false;
wasDown['down'] = false;
wasDown['p']  = false;
wasDown['w']  = false;
wasDown['a']  = false;
wasDown['s']    = false;
wasDown['d']  = false;
wasDown['q']  = false;
wasDown['\'']   = false;
wasDown['leftShift']= false;
wasDown['rightShift']= false;
wasDown['space']= false;
wasDown['enter']= false;
function keydown(e){
  switch(e.keyCode){
    case 37:
      if(!wasDown['left'] && !keyDown['left'])wasDown['left'] = true;
      keyDown['left'] = true;
      break;
    case 39:
      if(!wasDown['right'] && !keyDown['right'])wasDown['right'] = true;
      keyDown['right'] = true;
      break;
    case 38:
      if(!wasDown['up'] && !keyDown['up'])wasDown['up'] = true;
      keyDown['up'] = true;
      break;
    case 40:
      if(!wasDown['down'] && !keyDown['down'])wasDown['down'] = true;
      keyDown['down'] = true;
      break;
    case 65:
      if(!wasDown['a'] && !keyDown['a'])wasDown['a'] = true;
      keyDown['a'] = true;
      break;
    case 68:
      if(!wasDown['d'] && !keyDown['d'])wasDown['d'] = true;
      keyDown['d'] = true;
      break;
    case 87:
      if(!wasDown['w'] && !keyDown['w'])wasDown['w'] = true;
      keyDown['w'] = true;
      break;
    case 83:
      if(!wasDown['s'] && !keyDown['s'])wasDown['s'] = true;
      keyDown['s'] = true;
      break;
    case 80:
      if(!wasDown['p'] && !keyDown['p'])wasDown['p'] = true;
      keyDown['p'] = true;
      break;
    case 81:
      if(!wasDown['q'] && !keyDown['q'])wasDown['q'] = true;
      keyDown['q'] = true;
      break;
    case 222:
      if(!wasDown['\''] && !keyDown['\''])wasDown['\''] = true;
      keyDown['\''] = true;
      break;
    case 13:
      if(!wasDown['enter'] && !keyDown['enter'])wasDown['enter'] = true;
      keyDown['enter'] = true;
      break;
    case 16:
      if (e.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT){
        keyDown['leftShift'] = true;
      } else if (e.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT){
        keyDown['rightShift'] = true;
      }
      break;
    case 32:
      if(!wasDown['space'] && !keyDown['space'])wasDown['space'] = true;
      keyDown['space'] = true;
      break;
  }
  if(!paused){
    if(wasDown['space']){
      if(p1.ai){
        p1.ai = false;
        p1.reset();
      } else {
        p1.shape.dash( 0, 1);
      }
      wasDown['space'] = false;
    }
    if(wasDown['enter']){
      if(p2.ai){
        p2.ai = false;
        p2.reset();
      } else {
        p2.shape.dash( 0, 1);
      }
      wasDown['enter'] = false;
    }
    if(p1.playing && !p1.ai){
      if(wasDown['a'])p1.shape.move(-1, 0);
      if(wasDown['d'])p1.shape.move( 1, 0);
      if(wasDown['w'])p1.shape.rotate();
      if(wasDown['s']){
        if(p1.shape.locking)p1.shape.lock();
        else p1.shape.move( 0, 1);
      }
      if(keyDown['leftShift']){
        p1.shape.hold();
      }
    }
    if(p2.playing && !p2.ai){
      if(wasDown['left'])p2.shape.move(-1, 0);
      if(wasDown['right'])p2.shape.move( 1, 0);
      if(wasDown['up'])p2.shape.rotate();
      if(wasDown['down']){
        if(p2.shape.locking)p2.shape.lock();
        else p2.shape.move( 0, 1, 1);
      }
      if(keyDown['rightShift'])p2.shape.hold();
    }
  }
  if(wasDown['p']){
    if(!paused){
      paused = true;
    } else {
      paused = false;
    }
    display.flip();
    wasDown['p'] = false;
  }
}
function keyup(e){
  switch(e.keyCode){
    case 37:
      keyDown['left'] = false;
      wasDown['left'] = false;
      break;
    case 39:
      keyDown['right'] = false;
      wasDown['right'] = false;
      break;
    case 38:
      keyDown['up'] = false;
      wasDown['up'] = false;
      break;
    case 40:
      keyDown['down'] = false;
      wasDown['down'] = false;
      break;
    case 65:
      keyDown['a'] = false;
      wasDown['a'] = false;
      break;
    case 68:
      keyDown['d'] = false;
      wasDown['d'] = false;
      break;
    case 87:
      keyDown['w'] = false;
      wasDown['w'] = false;
      break;
    case 83:
      keyDown['s'] = false;
      wasDown['s'] = false;
      break;
    case 80:
      keyDown['p'] = false;
      wasDown['p'] = false;
      break;
    case 81:
      keyDown['q'] = false;
      wasDown['q'] = false;
      break;
    case 222:
      keyDown['\''] = false;
      wasDown['\''] = false;
      break;
    case 16:
      if(keyDown['leftShift'])keyDown['leftShift'] = false;
      if(keyDown['rightShift'])keyDown['rightShift'] = false;
      break;
    case 13:
      keyDown['enter'] = false;
      wasDown['enter'] = false;
      break;
    case 32:
      keyDown['space'] = false;
      wasDown['space'] = false;
      break;
  }
}
if(p1.playing){
  bag.init(p1);
  bag.draw(p1);
}
if(p2.playing){
  bag.init(p2);
  bag.draw(p2);
}
document.onkeydown = keydown;
document.onkeyup   = keyup;
function onEnterFrame(){
  if(!paused){
    p1.update();
    p2.update();
  }
  var updated = false;
  for(var i = 0; i < sprites.length; i ++){
    var sprite = sprites[i];
    sprite.update();
    updated = true;
  }
  if(updated)display.flip();
}
var enterFrame = setInterval(onEnterFrame, frameRate);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.