<header>
  <h1>TETRIS</h1>
</header>
  
<div id="preview-screen"></div>

<div id="screen"></div>

<div id="score">0</div>

<button id="start">Play</button>

<footer>
  <p>Created by <a href="https://remybeumier.be" target="_blank">Rémy Beumier</a></p>
</footer>

* {
  box-sizing: border-box;
}

html {
  
}

body {
  font-family: Arial;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
/*   background: linear-gradient(-45deg, #e53935 0 20px, #fb8c00  20px 40px, #fdd835 40px 60px, #43a047 60px 80px, #00acc1 80px 100px, #3949ab 100px 120px, #8e24aa 120px 140px, transparent 140px), linear-gradient(135deg, #e53935 0 20px, #fb8c00  20px 40px, #fdd835 40px 60px, #43a047 60px 80px, #00acc1 80px 100px, #3949ab 100px 120px, #8e24aa 120px 140px, #fff 140px); */
/*   background: linear-gradient(115deg, #e53935 0 40px, #fb8c00  40px 120px, #fdd835 60px 35%, #43a047 35% 65%, #00acc1 65% calc(100% - 120px), #3949ab calc(100% - 120px) calc(100% - 40px), #8e24aa calc(100% - 40px) 100%); */
  background: linear-gradient(-45deg, #3949ab, #80deea);
}

h1 {
  text-align: center;
  font-size: 32px;
  letter-spacing: 1rem;
  margin-right: -1rem;
  color: white;
}

#screen {
  background: #eee;
  border: solid 5px white;
  width: 210px;
  height: 410px;
  display: flex;
  flex-wrap: wrap;
}

#screen div {
  width: 20px;
  height: 20px;
  border: solid 1px white;
}

#screen div:nth-of-type(n+201) {
  border: transparent;
}

#preview-screen {
  display: flex;
  flex-wrap: wrap;
  width: 90px;
  height: 90px;
  margin-bottom: 20px;
  border: solid 5px white;
  background: #eee;
}

#preview-screen div {
  width: 20px;
  height: 20px;
  border: solid 1px white;
}

#preview-screen div:nth-of-type(n+17) {
  border: transparent;
}

.tetromino {
/*   background: red; */
}

#score {
  padding: 5px;
  color: white;
  
}

#start {
  width: 210px;
/*   border: solid 5px white; */
  cursor: pointer;
  background: white;
  border: none;
  padding: 5px;
  margin-bottom: 20px;
}

#start:hover,
#start:focus {
  background: #eee;
  border-color: #eee;
}

footer {
  font-size: 14px;
  padding: 10px;
  color: #fff;
}

footer a {
  text-decoration: none;
  color: #ccc;
}

footer a:hover {
  text-decoration: underline;
  color: #fff;
}

// fix space bar moveBottom() morking weird with line -> next piece appears too low
// change tetrominoes appearing -> only first block, not full shape !!
// add mobile swipe


var screen = document.querySelector("#screen");
var previewScreen = document.querySelector("#preview-screen");
// create play screen
for (let i = 0; i<200; i++) {
  createEl("div", "", screen);
}
// create frozen screen
for (let i = 0; i<20; i++) {
  createEl("div", "", screen, "taken");
}
// create preview screen
for (let i = 0; i<20; i++) {
  createEl("div", "", previewScreen);
}

let squares = Array.from(document.querySelectorAll("#screen div"));

const scoreEl = document.querySelector("#score");
const startEl = document.querySelector("#start");
const width = 10;
let nextRandom = 0;
let timerId;
let speed = 500;
let score = 0;
let started = false;
let over = false;
const colors = ["#e53935", "#fb8c00", "#fdd835", "#43a047", "#00acc1", "#3949ab", "#8e24aa"];
// red, orange, yellow, green, cyan, indigo, purple

// the tetrominoes
const lt = [
  [1, width+1, width*2+1, width*2+2],
  [width, width+1, width+2, width*2],
  [0, 1, width+1, width*2+1],
  [2, width, width+1, width+2]
];

const jt = [
  [1, width+1, width*2+1, 2],
  [width, width+1, width+2, width*2+2],
  [1, width+1, width*2+1, width*2],
  [0, width, width+1, width+2]
];

const st = [
  [1, width+1, width+2, width*2+2],
  [width+1, width+2, width*2, width*2+1],
  [1, width+1, width+2, width*2+2],
  [width+1, width+2, width*2, width*2+1]
];

const zt = [
  [2, width+1, width+2, width*2+1],
  [width, width+1, width*2+1, width*2+2],
  [2, width+1, width+2, width*2+1],
  [width, width+1, width*2+1, width*2+2]
];

const tt = [
  [1, width, width+1, width+2],
  [1, width+1, width+2, width*2+1],
  [width, width+1, width+2, width*2+1],
  [1, width, width+1, width*2+1]
];

const ot = [
  [1, 2, width+1, width+2],
  [1, 2, width+1, width+2],
  [1, 2, width+1, width+2],
  [1, 2, width+1, width+2]
];

const it = [
  [1, width+1, width*2+1, width*3+1],
  [width, width+1, width+2, width+3],
  [1, width+1, width*2+1, width*3+1],
  [width, width+1, width+2, width+3]
];

const tetrominoes = [lt, jt, st, zt, tt, ot, it];

let currentPosition = 3;
let currentRotation = 0;

let random = Math.floor(Math.random() * tetrominoes.length);
let current = tetrominoes[random][currentRotation];

// draw the tetromino
function draw() {
  current.forEach(index => {
    squares[currentPosition + index].classList.add("tetromino");
    squares[currentPosition + index].style.backgroundColor = colors[random];
  });
}

function undraw() {
  current.forEach(index => {
    squares[currentPosition + index].classList.remove("tetromino");
    squares[currentPosition + index].style.backgroundColor = "";
  });
}

// assign function to keyCodes
function control(e) {
  if (e.keyCode === 37) {
    moveLeft();
  }
  else if (e.keyCode === 38) {
    rotate();
  }
  else if (e.keyCode === 39) {
    moveRight();
  }
  else if (e.keyCode === 40) {
    moveDown();
  }
  else if (e.keyCode === 32) {
    console.log("space -> move bottom");
    e.preventDefault();
    moveBottom();
  }
  else if (e.keyCode === 27) {
    startPause();
  }
  else {
    console.log(e.keyCode);
  }
}

function moveDown() {
  undraw();
  currentPosition += width;
  draw();
  freeze();
}

function moveBottom() {
  while (!current.some(index => squares[currentPosition + index + width].classList.contains("taken"))) {
    undraw();
    currentPosition += width;
    draw();
  }
  freeze();
}

//freeze when tetromino touches taken square
function freeze() {
  if (current.some(index => squares[currentPosition + index + width].classList.contains("taken"))) {
    current.forEach(index => squares[currentPosition + index].classList.add("taken"));
    // start a new tetromino falling
    random = nextRandom;
    nextRandom = Math.floor(Math.random() * tetrominoes.length);
    current = tetrominoes[random][currentRotation];
    currentPosition = 3;
    draw();
    displayShape();
    addScore();
    gameOver();
  }
}

// move tetromino left if room
function moveLeft() {
  undraw();
  if (!isAtLeft()) {
    currentPosition -= 1;
  }
  if (current.some(index => squares[currentPosition + index].classList.contains("taken"))) {
    currentPosition += 1;
  }
  draw();
}

function isAtLeft() {
  return current.some(index=> (currentPosition + index) % width === 0)
}

// move tetromino right if room
function moveRight() {
  undraw();
  if (!isAtRight()) {
    currentPosition += 1;
  }
  if (current.some(index => squares[currentPosition + index].classList.contains("taken"))) {
    currentPosition -= 1;
  }
  draw();
}

function isAtRight() {
  return current.some(index=> (currentPosition + index + 1) % width === 0)  
}

// FIX ROTATION OF TETROMINOS A THE EDGE --> https://github.com/kubowania/Tetris-Basic/commit/c7e804e936c0624c947116a9c83b67005c257d94
function checkRotatedPosition(P){
  P = P || currentPosition;
  if ((P+1) % width < 4) {
    if (isAtRight()) {
      currentPosition += 1;
      checkRotatedPosition(P);
    }
  }
  else if (P % width > 5) {
    if (isAtLeft()) {
      currentPosition -= 1;
      checkRotatedPosition(P);
    }
  }
}

// rotate tetromino
function rotate() {
  undraw();
  currentRotation ++;
  if (currentRotation === tetrominoes[random].length) {
    currentRotation = 0;
  }
  current = tetrominoes[random][currentRotation];
  checkRotatedPosition();
  draw();
}

// show next tetromino in preview
const displaySquares = document.querySelectorAll("#preview-screen div");
const displayWidth = 4;
const displayIndex = 0;

const upNextTetrominoes = [
  [1, displayWidth+1, displayWidth*2+1, displayWidth*2+2], //lTetromino
  [1, displayWidth+1, displayWidth*2+1, 2], //jTetromino
  [1, displayWidth+1, displayWidth+2, displayWidth*2+2], //sTetromino
  [2, displayWidth+1, displayWidth+2, displayWidth*2+1], //zTetromino
  [1, displayWidth, displayWidth+1, displayWidth+2], //tTetromino
  [1, 2, displayWidth+1, displayWidth+2], //oTetromino
  [1, displayWidth+1, displayWidth*2+1, displayWidth*3+1] //iTetromino
];

// display the preview tetromino
function displayShape() {
  displaySquares.forEach(square => {
    square.classList.remove("tetromino");
    square.style.backgroundColor = "";
  });
  upNextTetrominoes[nextRandom].forEach(index => {
    displaySquares[displayIndex + index].classList.add("tetromino");
    displaySquares[displayIndex + index].style.backgroundColor = colors[nextRandom];
  });
}

function startPause() {
  // pause
  if (timerId) {
    clearInterval(timerId);
    timerId = null;
    startEl.innerHTML = "Play";
    // document.removeEventListener("keyup", control);
  }
  // resume
  else if (started) {
    startEl.innerHTML = "Pause";
    draw();
    timerId = setInterval(moveDown, speed);
    document.addEventListener("keyup", control);
  }
  // restart
  else if (over) {
    resetGame();
    started = true;
    startEl.innerHTML = "Pause";
    draw();
    timerId = setInterval(moveDown, speed);
    nextRandom = Math.floor(Math.random() * tetrominoes.length);
    displayShape();
    document.addEventListener("keyup", control);
  }
  // play
  else {
    started = true;
    startEl.innerHTML = "Pause";
    draw();
    timerId = setInterval(moveDown, speed);
    nextRandom = Math.floor(Math.random() * tetrominoes.length);
    displayShape();
    document.addEventListener("keyup", control);
  }
}

startEl.addEventListener("click", (e) => {
  e.preventDefault();
  startPause();
});

function addScore() {
  for (let i=0; i< 199; i+=width) {
    const row = [i, i+1, i+2, i+3, i+4, i+5, i+6, i+7, i+8, i+9];
    if (row.every(index => squares[index].classList.contains("taken"))) {
      score += 10;
      scoreEl.innerHTML = score;
      row.forEach(index => {
        squares[index].classList.remove("taken");
        squares[index].classList.remove("tetromino");
        squares[index].style.backgroundColor = "";
      });
      const squaresRemoved = squares.splice(i, width);
      squares = squaresRemoved.concat(squares);
      squares.forEach(cell => screen.appendChild(cell));
    }
  }
}

function gameOver() {
  if (current.some(index => squares[currentPosition + index].classList.contains("taken"))) {
    scoreEl.innerHTML = "X";
    clearInterval(timerId);
    timerId = null;
    started = false;
    over = true;
    startEl.innerHTML = "Restart";
    document.removeEventListener("keyup", control);
  }
}

function resetGame() {
  for (let i=0; i<200; i++) {
    squares[i].classList.remove("tetromino", "taken");
    squares[i].style.backgroundColor = "transparent";
  }
  for (let i=0; i<20; i++) {
    displaySquares[i].classList.remove("tetromino");
    displaySquares[i].style.backgroundColor = "transparent";
  }
  score = 0;
  scoreEl.innerHTML = score;
}

function createEl(el, txt, prnt, cls) {
  var newEl = document.createElement(el);
  newEl.classList.add(cls);
  newEl.innerText = txt;
  prnt.appendChild(newEl);
}
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.