Skip to content

Block-packing game

Drag shapes to place them on the grid. Complete lines to score points. Use points to progress through a series of ever-more-expensive inversions of the grid, called "flips".

Can you reach a total score of more than 50 points?

A grid where tiles can be placed, along with a tile and "reserve" space in the lower part of the screen

This might be my most-polished p5.js game. It's also some of the worst code I've ever written, to the point that I've used it as an exercise in navigating unreadable code. The worst part of it is the code responsible for the adaptable layout; it's scattered in multiple places and stubbornly refuses to use an explict representation of rectangles.

I could rewrite it with better classes/modules/datatypes... but where's the fun in that? 😂

function setup() {
  createCanvas(windowWidth, windowHeight);
}
var windowResized = setup
setTimeout(windowResized, 100)

let grid = {}
let piece
let reserved
let drag
let score = 0
let message = ''
let mT = 0
let over = 0
let flips = 0

function draw() {
  background(255);
  
  const g = 10
  
  if (piece == undefined) {
    piece = {}
    let parts = 0
    let x = 0
    let y = 0
    let bounds = [x,y,x,y] // limits
    let dir = [0, 0]
    while (parts < 5) {
      bounds[0] = min(bounds[0], x) // ??
      bounds[1] = min(bounds[1], y)
      bounds[2] = max(bounds[2], x)
      bounds[3] = max(bounds[3], y)
      if (!piece[[x,y]]) {
        piece[[x,y]] = true
        parts ++
      }
      dir = random([dir, [1,0], [-1,0], [0,1], [0,-1]])
      let [dx,dy] = dir
      x += dx
      y += dy
    }
    piece.bounds = bounds
  }
  if (piece.possible == undefined) {
    piece.possible = false
    a: for (let i = -piece.bounds[0]; i < g-piece.bounds[2]; i++) {
      for (let j = -piece.bounds[1]; j < g-piece.bounds[3]; j++) {
        piece.possible = true
        b: for (let k = piece.bounds[0]; k <= piece.bounds[2]; k++) {
          for (let l = piece.bounds[1]; l <= piece.bounds[3]; l++) {
            if (piece[[k, l]] && grid[[i + k, j + l]] > 0) {
              piece.possible = false
              break b
            }
          }
        }
        if (piece.possible) {
          break a
        }
      }
    }
  }
  if (!piece.possible && !over) {
    over = 1
  } else if (piece.possible && over) {
    over = 0
  }
  
  let minSize = min(width, height)
  let maxSize = max(width, height)
  let ps = min(max(abs(width - height), 200), minSize/3)
  let s = min(maxSize - ps, minSize)
  
  let boundsI = piece.bounds[2] - piece.bounds[0] + 1
  let boundsJ = piece.bounds[3] - piece.bounds[1] + 1
  let maxBounds = max(boundsI, boundsJ)
  let sqS = drag ? s/g : ps/maxBounds
  
  let ox = maxSize == width ? 0 : (width - s) / 2
  let oy = maxSize == height ? 0 : (height - s) / 2
  let pox = (maxSize == width ? s : (width - ps) / 2)
  let poy = (maxSize == height ? s : (height - ps) / 2)
  let pcx = sqS * (maxBounds - boundsI) / 2
  let pcy = sqS * (maxBounds - boundsJ) / 2
  
  let mouseI = drag && round((mouseX - drag.sx + pox + pcx - ox) / (s/g) - piece.bounds[0])
  let mouseJ = drag && round((mouseY - drag.sy + poy + pcy - oy) / (s/g) - piece.bounds[1])
  
  translate(ox, oy)
  
  for (let i = 0; i < g; i++) {
    for (let j = 0; j < g; j++) {
      stroke(255)
      let t = 0
      if ((i + j) % 2) {
        fill(240)
      } else {
        fill(230)
      }
      if (grid[[i, j]]) {
        fill(50)
        if (grid[[i, j]] > 1) {
          t = grid[[i, j]]
          grid[[i, j]] ++
        }
        if (grid[[i, j]] > 20) {
          grid[[i, j]] = 1
        }
        if (grid[[i, j]] < 0) {
          t = -grid[[i, j]]
          grid[[i, j]] --
        }
        if (grid[[i, j]] < -20) {
          delete grid[[i, j]]
        }
      }
      if (piece[[i - mouseI, j - mouseJ]]) {
        if (grid[[i, j]]) {
          noStroke()
          fill(250, 0, 0)
        } else {
          fill(200, 150, 150)
        }
      }
      rect(s/g*(i + (t/10)/2), s/g*(j + (t/10)/2), s/g * (1 - t/10), s/g * (1 - t/10))
    }
  }
  
  mT += 5
  if (over && !drag) {
    over ++
    fill(0, min(over - 100, 160))
    rect(0, 0, s, s)
    noStroke()
    fill(255, over - 100)
    textSize(30)
    textAlign(CENTER, CENTER)
    
    let cost = flips * 2 + 1
    if (score >= cost) {
      text(`Can't move.\nFlip?\nCost: ${cost}`, s/2, s/2)
      if (over > 60 && mouseIsPressed && mouseX > ox && mouseX < ox + s && mouseY > oy && mouseY < oy + s) {
        for (let i = 0; i < g; i++) {
          for (let j = 0; j < g; j++) {
            if (grid[[i, j]] && grid[[i, j]] > 0) {
              grid[[i, j]] = -1
            } else {
              grid[[i, j]] = 2
            }
          }
        }
        score -= cost
        if (piece) piece.possible = undefined
        over = 0
        flips ++
        mouseIsPressed = false
      }
    } else {
      text(`Game over!\nNext flip costs: ${cost}\nPoints: ${score}\nTotal score: ${score + flips * flips}`, s/2, s/2)
      if (over > 160 && mouseIsPressed && mouseX > ox && mouseX < ox + s && mouseY > oy && mouseY < oy + s) {
        grid = {}
        piece = undefined
        reserved = undefined
        score = 0
        message = ''
        over = 0
        flips = 0
      }
    }
  } else if (mT < 255) {
    stroke(255)
    fill(0, 255 - mT)
    textSize(10 + sqrt(mT))
    textAlign(CENTER, CENTER)
    text(message, s/2, s/2)
  }
  
  resetMatrix()
  
  let rox = maxSize == width ? pox : pox - ps
  let roy = maxSize == height ? poy : poy - ps
  
  translate(rox, roy) // Reserved
  if (reserved) {
    let boundsI = reserved.bounds[2] - reserved.bounds[0] + 1
    let boundsJ = reserved.bounds[3] - reserved.bounds[1] + 1
    let maxBounds = max(boundsI, boundsJ)
    let sqS = ps/maxBounds
    let pcx = sqS * (maxBounds - boundsI) / 2
    let pcy = sqS * (maxBounds - boundsJ) / 2
    translate(pcx, pcy)
    for (let i = reserved.bounds[0]; i <= reserved.bounds[2]; i++) {
      for (let j = reserved.bounds[1]; j <= reserved.bounds[3]; j++) {
        if (reserved[[i, j]]) {
          stroke(255)
          fill(150, 150, 200)
          rect(sqS*(i - reserved.bounds[0]), sqS*(j - reserved.bounds[1]), sqS, sqS)
        }
      }
    }
  }
  resetMatrix()
  
  fill(0)
  noStroke()
  textSize(15)
  textAlign(CENTER, CENTER)
  translate(rox, roy)
  text(`Reserve`, ps / 2, ps / 2)
  resetMatrix()
  
  translate(pox, poy)
  
  noFill()
  rect(0, 0, ps, ps)
  
  if (drag == undefined && mouseIsPressed && mouseX > pox && mouseX < pox + ps && mouseY > poy && mouseY < poy + ps) {
    drag = {
      sx: mouseX + (mouseX - pox) * (1 / (ps/maxBounds) * s/g - 1),
      sy: mouseY + (mouseY - poy) * (1 / (ps/maxBounds) * s/g - 1),
    }
  }
  if (drag && !mouseIsPressed) { // когато пуснеш?
    if (mouseI + piece.bounds[0] >= 0 && mouseJ + piece.bounds[1] >= 0 && mouseI + piece.bounds[2] < g && mouseJ + piece.bounds[3] < g) {
      let allowed = true
      for (let i = piece.bounds[0]; i <= piece.bounds[2]; i++) {
        for (let j = piece.bounds[1]; j <= piece.bounds[3]; j++) {
          if (piece[[i, j]] && grid[[mouseI + i, mouseJ + j]]) {
            allowed = false
          }
        }
      } 
      if (allowed) {
        let checkI = new Set()
        let checkJ = new Set()
        for (let i = piece.bounds[0]; i <= piece.bounds[2]; i++) {
          for (let j = piece.bounds[1]; j <= piece.bounds[3]; j++) {
            if (piece[[i, j]]) {
              grid[[mouseI + i, mouseJ + j]] = piece[[i, j]]
              checkJ.add(mouseJ + j)
              checkI.add(mouseI + i)
            }
          }
        } 
        let matchI = new Set([...checkI].filter(i => {
          for (let j = 0; j < g; j++) {
            if (grid[[i, j]] != 1) {
              return false;
            }
          }
          return true;
        }))
        let matchJ = new Set([...checkJ].filter(j => {
          for (let i = 0; i < g; i++) {
            if (grid[[i, j]] != 1) {
              return false;
            }
          }
          return true;
        }))
        for (let i of matchI) {
          for (let j = 0; j < g; j++) {
            grid[[i, j]] = -1;
          }
        }
        for (let j of matchJ) {
          for (let i = 0; i < g; i++) {
            grid[[i, j]] = -1;
          }
        }
        let matched = matchI.size + matchJ.size
        if (matched) {
          message = `x${matched}!`
          mT = 0
          score += matched * matched
        }
        mouseIsPressed = false
        piece = undefined
      }
    }
    drag = undefined
  }
  if (drag) {
    translate(mouseX - drag.sx, mouseY - drag.sy)
  }
  if (piece) {
    translate(pcx, pcy)
    for (let i = piece.bounds[0]; i <= piece.bounds[2]; i++) {
      for (let j = piece.bounds[1]; j <= piece.bounds[3]; j++) {
        if (piece[[i, j]]) {
          stroke(255)
          fill(150, 100, 100)
          rect(sqS*(i - piece.bounds[0]), sqS*(j - piece.bounds[1]), sqS, sqS)
        }
      }
    }
  }
  resetMatrix()
  
  if (mouseIsPressed && !drag && mouseX > rox && mouseX < rox + ps && mouseY > roy && mouseY < roy + ps) {
    [reserved, piece] = [piece, reserved]
    if (piece) piece.possible = undefined
    mouseIsPressed = false
  }
  
  fill(0)
  noStroke()
  textSize(15)
  textAlign(CENTER, CENTER)
  translate(maxSize == width ? pox : pox + ps, maxSize == height ? poy : poy + ps)
  text(`Points: ${score}\nFlips: ${flips}`, ps / 2, ps / 2)
  resetMatrix()
}

(Originally seen at https://editor.p5js.org/bojidar-bg/sketches/hydiwBCb2)

Experiments tagged p5 (64/85)

Experiments tagged game (8/11)

Experiments on this site (64/85)