Namide

About

Hi, I'm Namide 🦄

I like video games and programming.

function tetris ({ el = document.body, theme = [...'🖤🧱🍑🥝🍒🍓🍄🥑🍌🎮🌚'] }) {
  let frame = 0, velocity, fullLineTimer, history, score, level, screen = 0, raf, tmp, highScore = localStorage.getItem('tetris') || 0, disposes = [], pawns

  const getControl = createControls()
  disposes.push(() => cancelAnimationFrame(raf))

  tick()
  return () => disposes.forEach(dispose => dispose())

  function createControls() {
    const KEYS = { ArrowUp: 'rotateRight', ArrowRight: 'right', ArrowDown: 'down', ArrowLeft: 'left', KeyW: 'rotateRight', KeyD: 'right', KeyS: 'down', KeyA: 'left', Enter: 'enter', KeyQ: 'rotateLeft', KeyE: 'rotateRight', KeyZ: 'rotateLeft', KeyX: 'rotateRight' }
    const PADS = { 12: 'rotateRight', 7: 'rotateRight', 1: 'rotateRight', 2: 'rotateLeft', 6: 'rotateLeft', 15: 'right', 0: 'down', 13: 'down', 14: 'left', 9: 'enter' }
    let startFrame = -1, key = 0, onUp, onDown

    window.addEventListener('keyup', onUp = ({ code }) => key = key === KEYS[code] ? 0 : key)
    window.addEventListener('keydown', onDown = event => {
      if (KEYS[event.code]) { event.preventDefault(key = KEYS[event.code]) }
    })
    disposes.push(() => window.removeEventListener('keyup', onUp))
    disposes.push(() => window.removeEventListener('keydown', onDown))

    return () => {
      const gamepad = navigator.getGamepads()[0]
      const pad = PADS[gamepad && gamepad.buttons.findIndex(({ pressed }) => pressed)]
      const control = { rotateRight: 0, rotateLeft: 0, right: 0, down: 0, left: 0, enter: 0 }
      startFrame = startFrame === -1 && (key || pad) ? frame : startFrame
      const delay = { down: 1, rotateRight: 12, rotateLeft: 12 }[key || pad] || 8 
      const ok = ((frame - startFrame) % delay === 0)
      if (key) { control[key] = ok ? 1 : 0 }
      if (pad) { control[pad] = ok ? 1 : 0 }
      startFrame = !key && !pad ? startFrame = -1 : startFrame
      return control
    }
  }

  function start() {
    screen = 1, score = 0, fullLineTimer = 0, velocity = 40, frame = 0, level = 1
    history = new Array(22).fill(0).map(() => new Array(11).fill(0)), pawns = []
  }

  function tick() {
    const control = getControl()
    if (screen !== 1 && control.enter) { start() }
    if (screen === 1) {
      if (pawns.length < 3) { addPawns() }
      if (fullLineTimer < 1) { movePawn(control) }
      testLine()
    }
    render()
    raf = requestAnimationFrame(tick.bind(tetris))
    frame++
  }

  function isLineFull(line) { return line && line.reduce((count, pixel) => pixel ? count + 1 : count, 0) > 9 }
  function fullLineCount(grid) { return grid && grid.reduce((total, line) => total + (isLineFull(line) ? 1 : 0), 0) }

  function testLine() {    
    if ((tmp = fullLineCount(history)) < 1) return
    if (!fullLineTimer) {
      score += 27 + 3 ** tmp, level = (score >> 6) || 1, velocity = (3 + 40 * 0.8 ** level) | 0, fullLineTimer = 10 * tmp
      return 
    } 
    if (fullLineTimer < 2) {
      while (fullLineCount(history) > 0) {
        history.splice(history.reduce((result, line, i) => isLineFull(line) ? i : result, -1), 1)
        history.unshift([])
      }
    } else {
      history = history.map(line => isLineFull(line) ? line.map((_, index) => index > 0 ? theme[2 + ((fullLineTimer / 15) | 0) % 2] : 0) : line)
    }
    return --fullLineTimer
  }

  function isInGround(pawn) {
    if (!pawn || !pawn.grid || !history) return false
    const hitWall = (x, y) => x < 1 || x > 10 || y > 20
    const hitfixed = (x, y) => history[y] && history[y][x]
    return !!pawn.grid.find((row, y) => row.find((val, x) => val && (hitWall(x + pawn.x, y + pawn.y) || hitfixed(x + pawn.x, y + pawn.y))))
  }

  function fixPawn({ x, y, grid, pixel }) { grid.forEach((row, j) => row.forEach((cell, i) => cell ? history[j + y][i + x] = pixel : 0)) }

  function addPawns() {
    const list = new Array(7).fill(0).map((_, i) => i)
    while (list.length) {
      const i = (Math.random() * list.length) | 0
      pawns.push({ x: 5, y: 0, r: 0, model: list[i], grid: pawnGrid({ model: list[i], r: 0 }), pixel: theme[list[i] + 2] })
      list.splice(i, 1)
    }
  }

  function movePawn(control) {
    const pawnFall = Object.assign({}, pawns[0], { y: pawns[0].y + (control.down ? 1 : frame % velocity === 0 ? 1 : 0) })
    if (isInGround(pawnFall)) {
      if (pawns[0].y + fullLineCount(history) <=0) {
        screen = 2
        localStorage.setItem('tetris', highScore = Math.max(score, highScore))
      }
      fixPawn(Object.assign({}, pawns[0], { y: pawns.shift().y }))
    } else {
      const pawnMoved = Object.assign({}, pawnFall, {
        x: pawnFall.x + (control.right ? 1 : control.left ? -1 : 0),
        r: pawnFall.r + (control.rotateRight ? 1 : control.rotateLeft ? 3 : 0),
        grid: pawnGrid(pawnFall)
      })

      pawns[0] = isInGround(pawnMoved) ? pawnFall : pawnMoved
    }
  }

  function pawnGrid ({ model, l, r }) {
    const rotate = (grid, num) => num ? rotate(grid.map((line, i) => line.map((cell, j) => grid[line.length - j - 1][i])), num - 1) : grid
    const list = [
      /* Z */ [[1,1,0],[0,1,1],[0,0,0]], /* S */ [[0,1,1],[1,1,0],[0,0,0]],
      /* L */ [[0,0,1],[1,1,1],[0,0,0]], /* J */ [[1,0,0],[1,1,1],[0,0,0]],
      /* T */ [[0,0,0],[1,1,1],[0,1,0]], /* O */ [[1,1],[1,1]],
      /* I */ [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]]
    ]
    console.log(rotate(list[model], 1))
    return rotate(list[model], r % 4)
  }

  function render() {
    const pipe = (list) => (...args) => (list.reduce((previous, call) => !previous ? call(...args) : previous, false) || ' ')
    const drawBackgrounds = squares => (x, y) => (tmp = squares.find(([X,Y,W,H]) => x>X && x<X+W && y>Y && y<Y+H)) ? tmp[5] : false
    const drawText = texts => (x, y) => (tmp = texts.find(([X, Y]) => x===X && y===Y)) ? tmp[2]() : false
    const drawNextPawn = (x, y) => (tmp = pawns[1].grid[y - 5]) && tmp[x - 15] ? pawns[1].pixel : false
    const drawFixed = (x, y) => (tmp = history[y]) && tmp[x] ? tmp[x] : false
    const drawPawn = (x, y) => (tmp = pawns[0].grid[y - pawns[0].y]) && tmp[x - pawns[0].x] && y > 0 ? pawns[0].pixel : false
    const drawWalls = squares => (x, y) => (tmp = squares.find(([X,Y,W,H]) => ((x>=X && x<=X+W) && (y===Y || y===Y+H)) || ((y>=Y && y<=Y+H) && (x===X || x===X+W)))) ? tmp[4] : false
    const createGrid = fct => new Array(22).fill(0).map((_, y) => new Array(20).fill(0).map((_, x) => fct(x, y)))

    if (screen === 0) {
      const list = [drawText([[15, 6, () => 'TETRIS'], [15, 7, () => '======'], [5, 9, () => 'made with 💜 by namide.com'], [11, 13, () => '(press enter)']])]
      el.innerHTML = createGrid(pipe(list)).map(line => line.join('')).join('\n')
    } else if (screen === 1) {
      const squares = [[0, 0, 11, 21, theme[1], theme[0]], [13, 3, 6, 5, theme[10], theme[0]]]
      const texts = [[15, 10, () => `LEVEL: ${level}`], [15, 11, () => `SCORE: ${score}`], [15, 13, () => `RECORD: ${highScore}`]]
      const list = [drawWalls(squares), drawPawn, drawFixed, drawNextPawn, drawText(texts), drawBackgrounds(squares)]
      el.innerHTML = createGrid(pipe(list)).map(line => line.join('')).join('\n')
    } else if (screen === 2) {
      const list = [drawText([ [13, 6, () => 'GAME OVER'], [13, 7, () => '========='], [10, 10, () => `LEVEL: ${level}`], [10, 11, () => `SCORE: ${score}`], [10, 13, () => `RECORD: ${highScore}`], [11, 18, () => '(press enter)']])]
      el.innerHTML = createGrid(pipe(list)).map(line => line.join('')).join('\n')
    }
  }
}

if (window.tetrisDispose) { window.tetrisDispose() }
window.tetrisDispose = tetris({ el: document.body.querySelector('.game') })