js 一个不成熟的俄罗斯方块

427 阅读5分钟

最近刷抖音看到 渡一Web前端学习频道 这个账号发的 JS 使用位运算实现俄罗斯方块挺有意思的,于是决定试一试。

共实现了下列几项的功能:

  1. 方块布局
  2. 方块移动
  3. 方块消除
  4. 方块左旋

思路

表示方块

id为 obje 表格表示方块在此地图上移动,id为 table 的表格表示地图

<!-- 绝对定位位于上方 方块在此移动 -->
<table border id="obje">
    tr*12>td*6
</table>
<!-- 地图,方块移动到指定条件,被添加到地图上 -->
<table border id="table">
    tr*12>td*6
</table>

数据:一维数组,数组每一项都是一个数字,数据渲染到页面时,数字先转化为6位二进制数再渲染。

// 表示十二行,每一个数据转化为6位二进制,表示6列
let map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let objee = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// map 是 table 对应的数据,objee 是 obje 对应的数据

方块移动

左右移动:对数组每一项数据进行位运算。

左移: n << 1 // 4 << 1 => 8
右移: n >> 1 // 4 >> 1 => 2

image.png 上下移动:对数组进行添加截取

// [1,2,3,4] => [0,1,2,3]
[0].concat(Array.slice(0, -1))

image.png

方块存放到地图上

当方块到达一定条件时,将原本在 obje 的方块,移动到 table 上

// 0b000100 | 0b000011 => 0b000111
objee[index] | item

有个问题,就是如果 obje 和 table 上的方块重叠了怎么办?所以在运算前要先进行一步是否重合判断。

// 判断下一步是否会有和 table 重合的方块。
const result = map.every((item, index) => {
      return (item & [0].concat(objee.slice(0, -1))[index]) === 0
    })
// 另一个限制是方块到达最下方也要停下。
if(!result || objee[objee.length - 1] !== 0) {
    // TODO...
}

方块消除

因为方块是使用二进制表示的,所以一排如果全部为红色即值为 63,则可消除当前行。

map = map.reduce((mapClone, item, index) => {
    if((objee[index] | item) === 63){
      mapClone.unshift(0)
    }
    else {
      mapClone.push(objee[index] | item)
    }
    return mapClone
  },[])

左旋转

  1. 求出方块所在的九宫格最左上角的坐标。
// 最左上角坐标的 x,y
let minX = 100
let minY = 100
let temp = 0
 objArr.map((item, i) => {
  item.forEach((n, j) => {
    if(n === '1' && cell>j)  {
      minY=j
    }
    if(n === '1' && row>i)  {
      minX=i
    }
  })
})

image.png

  1. 运算出旋转后的位置
temp = y
y = y + 1 - minX + minY - 1
x = minX + 3 + minY - 1 - temp

image.png

实现

布局

游戏布局使用的是 table 表格,通过改变表格背景颜色实现方块的不同显示。

分为两层显示,上层 obje 显示的是新方块的移动。下层 table 是底部地图。

HTML

<!-- 方块移动布局 -->
<table border id="obje">
    tr*12>td*6
</table>
<!-- 地图 -->
<table border id="table">
    tr*12>td*6
</table>

<button id="left">左移</button>
<button id="right">右移</button>
<button id="rotate-left">左旋转</button>
<button id="rotate-right">右旋转</button>
<button id="p">暂停</button>

CSS

*   {
    margin: auto 0;
    padding: 0;
}
td {
    width: 30px;
    height: 30px;
}
button {
    width: 50px;
    height: 35px
}
/* 被选中的方块会 添加该类名 */
.select-color {
    opacity: 1 !important;
    background-color: red;
}
/* 使用定位将页面分为上下两个图层 */
#obje {
    position: absolute;
}
/* 初始上层为透明 */
#obje td {
    opacity: 0;
}

数据渲染

将数组数据转化为二进制 0 为白色,1 为红色

 // 将数组的数据渲染到表格上 (数据数组,表格对象)
function renderTable(arr, table) {
    arr.forEach((r, i) => {
      parseInt(r).toString(2).padStart(6,'0').split('').forEach((c, j) => {
        table.rows[i].cells[j].classList.remove('select-color')
        if(c === '1') {
          table.rows[i].cells[j].classList.add('select-color')
        }
      })
    })
}

 // 获取视图对象
  const table = document.getElementById('table')

  // 地图数组 每一项都代表一行
  let map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

  // 将数组数据渲染到表格
  renderTable(map, table)

  const obje = document.getElementById('obje')
  // 移动地图数组 每一项都代表一行
  let objee = [8, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  renderTable(objee, obje)

生成新方块

// 生成新的方块
function createSquare() {
    let square = [
      [8, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [8, 12, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [28, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [8, 24, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [1, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [4, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]
    const i = parseInt(Math.random() * 7)
    return square[i]
}

自动下降

// 每隔 1 秒下降一格
let timeout = setInterval(() => {
    // map 与 obje 下一步有无重叠方块 无为 true 有为 false
    const result = map.every((item, index) => {
      return (item & [0].concat(objee.slice(0, -1))[index]) === 0
    })
    // 判断是否到达最下方边界
    if(!result || objee[objee.length - 1] !== 0) {
      // 将这个方块放到 map 地图里面
      map = map.reduce((mapClone, item, index) => {
        if((objee[index] | item) === 63){
          mapClone.unshift(0)
        }
        else {
          mapClone.push(objee[index] | item)
        }
        return mapClone
      },[])
      // 方块到达底部,生成新的方块
      objee = createSquare()
      renderTable(map, table)
      renderTable(objee, obje)
      return 
    }
    // objee.unshift(0) objee.pop(0)
    objee = [0].concat(objee.slice(0, -1))
    renderTable(objee, obje)
}, 1000)

左右移动

  // 向左按钮
  let leftBtn = document.getElementById('left')
  leftBtn.addEventListener('click', () => {
    moveLeft(objee, obje, 32)
  }, false)
  // 向右按钮
  let rightBtn = document.getElementById('right')
  rightBtn.addEventListener('click', () => {
    moveRight(objee, obje, 1)
  }, false)
  
  // 向左移动
  function moveLeft(arr, table, i) {
    // 判断
    if(arr.every(item => (item & i) == 0 ? true : false)) {
      if(map.every((item, index) => {
        return (item & [0].concat(objee.slice(0, -1))[index]) === 0
      })) {
        objee = arr.map((n) => {
          return i === 32 ? n << 1: n >> 1
        })
      }
      renderTable(objee, table)
    }
  }

停止下移

// 暂停按钮
let pBtn = document.getElementById('p')
pBtn.addEventListener('click', () => {
    clearInterval(timeout)
}, false)

左旋转

到了旋转这里,才发现用两层地图有多难受了,功能虽然实现了,但是感觉太繁琐了。

let rotateLeft = document.getElementById('rotate-left')
  rotateLeft.addEventListener('click', () => {
    // 将一维数组,转化为二维数组(二进制)
    objArr = objee.map(item => {
      return parseInt(item).toString(2).padStart(6,'0').split('')
    })
    // 求方块所在九宫格最左上的坐标
    let row = 100
    let cell = 100
    let a = []
    let temp = 0
    objArr.map((item, i) => {
      item.forEach((n, j) => {
        if(n === '1' && cell>j)  {
          cell=j
        }
        if(n === '1' && row>i)  {
          row=i
        }
      })
    })
    // 计算出值是 1 的坐标
    const b = objArr.map((item, i) => {
      return item.map((n, j) => {
        if(n === '1')  {
          a.push([i,j])
        }
        return 0
      })
    })
    // 运算旋转后的坐标
    a.map( item => {
      temp = item[1]
      item[1] = item[0] + 1 - row + cell - 1
      item[0] = row + 3 + cell - 1 - temp
    })
    // 旋转
    a.map( item => {
      b[item[0]][item[1]] = '1'
    })
    // 二维数组再转回一维数组
    let c = b.map( item => {
      return parseInt(parseInt(item.join('')),2)
    })
    // 这里我只判断了不能和原地图重叠,还需要判断地图边界
    // 旋转后不能与原来的坐标重合
    if(map.every((item, index) => {
      return (item & c[index]) === 0
    })) {
      objee = c
    }
    renderTable(objee, obje)
  }, false)

莫喷莫喷