开始
1. 创建一个宽为 200px
,高为 360px
的背景容器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>俄罗斯方块</title>
<style>
.container {
position: relative;
width: 200px;
height: 360px;
background-color: #000;
}
</style>
</head>
<body>
<div class="container"></div>
</body>
</html>
2. 在该容器上创建一个 20 * 20
的块元素
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>俄罗斯方块</title>
<style>
.container {
position: relative;
width: 200px;
height: 360px;
background-color: #000;
}
.activity-model {
width: 20px;
height: 20px;
background-color: cadetblue;
border: 1px solid #eeeeee;
box-sizing: border-box;
position: absolute;
}
</style>
</head>
<body>
<div class="container">
<div class="activity-model"></div>
</div>
</body>
</html>
3. 控制该元素的移动,每次移动 20px
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>俄罗斯方块</title>
<style>
.container {
position: relative;
width: 200px;
height: 360px;
background-color: #000;
}
.activity-model {
width: 20px;
height: 20px;
background-color: cadetblue;
border: 1px solid #eeeeee;
box-sizing: border-box;
position: absolute;
}
</style>
</head>
<body>
<div class="container">
<div class="activity-model"></div>
</div>
<script>
const STEP = 20
init()
function init() {
onKeyDown()
}
function onKeyDown() {
document.onkeydown = event => {
switch (event.keyCode) {
case 38:
move(0, -1)
break;
case 39:
move(1, 0)
break;
case 40:
move(0, 1)
break;
case 37:
move(-1, 0)
break;
default:
break;
}
}
}
function move(x, y) {
const activityModelEle = document.getElementsByClassName("activity-model")[0]
activityModelEle.style.top = parseInt(activityModelEle.style.top || 0) + y * STEP + "px"
activityModelEle.style.left = parseInt(activityModelEle.style.left || 0) + x * STEP + "px"
}
</script>
</body>
</html>
构建 L
形状的模型
1. 将容器进行分割,分割为 18
行,10
列。行高,列高均为20

const STEP = 20
const ROW_COUNT = 18, COL_COUNT = 10
2. 以 16宫格
为基准,定义 L
形状的 4
个方块的位置
const ROW_COUNT = 18, COL_COUNT = 10
const MODELS = [
{
0: {
row: 2,
col: 0
},
1: {
row: 2,
col: 1
},
2: {
row: 2,
col: 2
},
3: {
row: 1,
col: 2
}
}]
3. 创建 L
型模型,根据 16
宫格中的数据将模型渲染到页面上
const ROW_COUNT = 18, COL_COUNT = 10
const MODELS = [
{
0: {
row: 2,
col: 0
},
1: {
row: 2,
col: 1
},
2: {
row: 2,
col: 2
},
3: {
row: 1,
col: 2
}
}]
let currentModel = {}
init()
function init() {
createModel()
onKeyDown()
}
function createModel() {
currentModel = MODELS[0]
for (const key in currentModel) {
const divEle = document.createElement('div')
divEle.className = "activity-model"
document.getElementById("container").appendChild(divEle)
}
locationBlocks()
}
function locationBlocks() {
const eles = document.getElementsByClassName("activity-model")
for (let i = 0; i < eles.length; i++) {
const activityModelEle = eles[i]
const blockModel = currentModel[i]
activityModelEle.style.top = blockModel.row * STEP + "px"
activityModelEle.style.left = blockModel.col * STEP + "px"
}
}
控制该模型进行移动
function locationBlocks() {
const eles = document.getElementsByClassName("activity-model")
for (let i = 0; i < eles.length; i++) {
const activityModelEle = eles[i]
const blockModel = currentModel[i]
activityModelEle.style.top = (currentY + blockModel.row) * STEP + "px"
activityModelEle.style.left = (currentX + blockModel.col) * STEP + "px"
}
}
function move(x, y) {
currentX += x
currentY += y
locationBlocks()
}
控制模型旋转

规律
- 以 16宫格 的中心点为基准进行旋转
- 观察上图中旋转后每个块元素发生的位置的变化
- 以第1,2个L模型为例,可以观察到:
- 块元素1的坐标(列, 行)变化:(0, 2) -> (1, 0)
- 块元素2的坐标(列, 行)变化:(1, 2) -> (1, 1)
- 块元素3的坐标(列, 行)变化:(2, 2) -> (1, 2)
- 块元素4的坐标(列, 行)变化:(2, 1) -> (2, 2)
- …
- 其基本变化规律是
移动后的行 = 移动前的列
移动后的列 = 3 - 移动前的行
旋转模型
function onKeyDown() {
document.onkeydown = event => {
switch (event.keyCode) {
case 38:
rotate()
break;
case 39:
move(1, 0)
break;
case 40:
move(0, 1)
break;
case 37:
move(-1, 0)
break;
default:
break;
}
}
}
function rotate() {
for (const key in currentModel) {
const blockModel = currentModel[key]
let temp = blockModel.row
blockModel.row = blockModel.col
blockModel.col = 3 - temp
}
locationBlocks()
}
控制模型只在容器中移动
function locationBlocks() {
checkBound()
const eles = document.getElementsByClassName("activity-model")
for (let i = 0; i < eles.length; i++) {
const activityModelEle = eles[i]
const blockModel = currentModel[i]
activityModelEle.style.top = (currentY + blockModel.row) * STEP + "px"
activityModelEle.style.left = (currentX + blockModel.col) * STEP + "px"
}
}
function checkBound() {
let leftBound = 0, rightBound = COL_COUNT, bottomBound = ROW_COUNT
for (const key in currentModel) {
const blockModel = currentModel[key]
if ((blockModel.col + currentX) < 0) {
currentX++
}
if ((blockModel.col + currentX) >= rightBound) {
currentX--
}
if ((blockModel.row + currentY) >= bottomBound) {
currentY--
}
}
}
当模型触底时,将块元素变为灰色
固定在底部,同时生成一个新的模型
声明样式类
.fixed-model {
width: 20px;
height: 20px;
background-color: #fefefe;
border: 1px solid #333333;
box-sizing: border-box;
position: absolute;
}
触底时固定,生成新模型
- 需要注意的是:当模型触底被固定后,我们需要重新再生成一个新的模型,再生成新模型的时候,需要重置 16宫格 的位置,否则新创建的模型的位置会出现在底部,并将上一模型覆盖掉
function createModel() {
currentModel = MODELS[0]
currentY = 0
currentY = 0
for (const key in currentModel) {
const divEle = document.createElement('div')
divEle.className = "activity-model"
document.getElementById("container").appendChild(divEle)
}
locationBlocks()
}
function checkBound() {
let leftBound = 0, rightBound = COL_COUNT, bottomBound = ROW_COUNT
for (const key in currentModel) {
const blockModel = currentModel[key]
if ((blockModel.col + currentX) < 0) {
currentX++
}
if ((blockModel.col + currentX) >= rightBound) {
currentX--
}
if ((blockModel.row + currentY) >= bottomBound) {
currentY--
fixedBottomModel()
}
}
}
function fixedBottomModel() {
const activityModelEles = document.getElementsByClassName('activity-model')
;[...activityModelEles].forEach((ele, i) => {
ele.className = "fixed-model"
const blockModel = currentModel[i]
fixedBlocks[`${currentY + blockModel.row}_${currentX + blockModel.col}`] = ele
})
createModel()
}
判断块元素与块元素之间的碰撞,分为左右接触
和底部接触
记录所有块元素的位置
const fixedBlocks = {}
当块元素被固定到底部的时候,将块元素存储在fixedBlocks
中
function fixedBottomModel() {
const activityModelEles = document.getElementsByClassName('activity-model')
;[...activityModelEles].forEach((ele, i) => {
ele.className = "fixed-model"
const blockModel = currentModel[i]
fixedBlocks[`${currentY + blockModel.row}_${currentX + blockModel.col}`] = ele
})
createModel()
}
处理模型之间的碰撞(左右接触)
function move(x, y) {
if (isMeet(currentX + x, currentY + y, currentModel)) {
return
}
currentX += x
currentY += y
locationBlocks()
}
function rotate() {
const cloneCurrentModel = JSON.parse(JSON.stringify(currentModel))
for (const key in cloneCurrentModel) {
const blockModel = cloneCurrentModel[key]
let temp = blockModel.row
blockModel.row = blockModel.col
blockModel.col = 3 - temp
}
if (isMeet(currentX, currentY, cloneCurrentModel)) {
return
}
currentModel = cloneCurrentModel
locationBlocks()
}
function isMeet(x, y, model) {
for (const key in model) {
const blockModel = model[key]
if (fixedBlocks[`${y + blockModel.row}_${x + blockModel.col}`]) {
return true
}
}
return false
}
处理模型之间的碰撞(底部接触)
function move(x, y) {
if (isMeet(currentX + x, currentY + y, currentModel)) {
if (y != 0) {
fixedBottomModel()
}
return
}
currentX += x
currentY += y
locationBlocks()
}
处理被铺满的行
判断一行是否被铺满
function fixedBottomModel() {
const activityModelEles = document.getElementsByClassName('activity-model')
;[...activityModelEles].forEach((ele, i) => {
ele.className = "fixed-model"
const blockModel = currentModel[i]
fixedBlocks[`${currentY + blockModel.row}_${currentX + blockModel.col}`] = ele
})
isRemoveLine()
createModel()
}
function isRemoveLine() {
for (let i = 0; i < ROW_COUNT; i++) {
let flag = true
for (let j = 0; j < COL_COUNT; j++) {
if (!fixedBlocks[`${i}_${j}`]) {
flag = false
break
}
}
if (flag) {
console.log("该行已经被铺满了")
}
}
}
清理被铺满的一行
function isRemoveLine() {
for (let i = 0; i < ROW_COUNT; i++) {
let flag = true
for (let j = 0; j < COL_COUNT; j++) {
if (!fixedBlocks[`${i}_${j}`]) {
flag = false
break
}
}
if (flag) {
removeLine(i)
}
}
}
function removeLine(line) {
for (let i = 0; i < COL_COUNT; i++) {
document.getElementById("container").removeChild(fixedBlocks[`${line}_${i}`])
fixedBlocks[`${line}_${i}`] = null
}
}
让被清理行之上的块元素下落
function removeLine(line) {
for (let i = 0; i < COL_COUNT; i++) {
document.getElementById("container").removeChild(fixedBlocks[`${line}_${i}`])
fixedBlocks[`${line}_${i}`] = null
}
downLine(line)
}
function downLine(line) {
for (let i = line - 1; i >= 0; i--) {
for (let j = 0; j < COL_COUNT; j++) {
if (!fixedBlocks[`${i}_${j}`]) continue
fixedBlocks[`${i + 1}_${j}`] = fixedBlocks[`${i}_${j}`]
fixedBlocks[`${i + 1}_${j}`].style.top = (i + 1) * STEP + "px"
fixedBlocks[`${i}_${j}`] = null
}
}
}
创建多种模型样式
定义模型样式
const MODELS = [
{
0: {
row: 2,
col: 0
},
1: {
row: 2,
col: 1
},
2: {
row: 2,
col: 2
},
3: {
row: 1,
col: 2
}
},
{
0: {
row: 1,
col: 1
},
1: {
row: 0,
col: 0
},
2: {
row: 1,
col: 0
},
3: {
row: 2,
col: 0
}
},
{
0: {
row: 1,
col: 1
},
1: {
row: 2,
col: 1
},
2: {
row: 1,
col: 2
},
3: {
row: 2,
col: 2
}
},
{
0: {
row: 0,
col: 0
},
1: {
row: 0,
col: 1
},
2: {
row: 0,
col: 2
},
3: {
row: 0,
col: 3
}
},
{
0: {
row: 1,
col: 1
},
1: {
row: 1,
col: 2
},
2: {
row: 2,
col: 2
},
3: {
row: 2,
col: 3
}
}
]
创建模型的时候随机选取不同的模型样式
function createModel() {
const randow = Math.floor(Math.random() * MODELS.length)
currentModel = MODELS[randow]
currentY = 0
currentY = 0
for (const key in currentModel) {
const divEle = document.createElement('div')
divEle.className = "activity-model"
document.getElementById("container").appendChild(divEle)
}
locationBlocks()
}
模型自动降落
let mInterval = null
function createModel() {
const randow = Math.floor(Math.random() * MODELS.length)
currentModel = MODELS[randow]
currentY = 0
currentY = 0
for (const key in currentModel) {
const divEle = document.createElement('div')
divEle.className = "activity-model"
document.getElementById("container").appendChild(divEle)
}
locationBlocks()
autoDown()
}
function autoDown() {
if (mInterval) {
clearInterval(mInterval)
}
mInterval = setInterval(() => {
move(0, 1)
}, 600)
}
游戏结束
判断游戏结束
function createModel() {
if (isGameOver()) {
console.log("游戏结束!")
return
}
const randow = Math.floor(Math.random() * MODELS.length)
currentModel = MODELS[randow]
currentY = 0
currentY = 0
for (const key in currentModel) {
const divEle = document.createElement('div')
divEle.className = "activity-model"
document.getElementById("container").appendChild(divEle)
}
locationBlocks()
autoDown()
}
function isGameOver() {
for (let i = 0; i < COL_COUNT; i++) {
if (fixedBlocks[`0_${i}`]) return true
}
return false
}
结束游戏
function createModel() {
if (isGameOver()) {
gameOver()
return
}
const randow = Math.floor(Math.random() * MODELS.length)
currentModel = MODELS[randow]
currentY = 0
currentY = 0
for (const key in currentModel) {
const divEle = document.createElement('div')
divEle.className = "activity-model"
document.getElementById("container").appendChild(divEle)
}
locationBlocks()
autoDown()
}
function gameOver() {
if (mInterval) {
clearInterval(mInterval)
}
alert("大吉大利,今晚吃鸡!")
}
扩展:计分 + 最高分 + 重新开始游戏
结构 + 样式
body {
display: flex;
}
#scores {
margin-left: 20px;
}
<div id="container" class="container">
</div>
<div id="scores">
<p>最高分:<span id="max-score">0</span></p>
<p>分数:<span id="current-score">0</span></p>
<button onclick="reset()">重新开始</button>
</div>
逻辑
let maxScore = 0
let score = 0
function removeLine(line) {
for (let i = 0; i < COL_COUNT; i++) {
document.getElementById("container").removeChild(fixedBlocks[`${line}_${i}`])
fixedBlocks[`${line}_${i}`] = null
}
score += COL_COUNT
document.getElementById("current-score").innerHTML = score
downLine(line)
}
function gameOver() {
if (mInterval) {
clearInterval(mInterval)
}
maxScore = Math.max(maxScore, score)
document.getElementById("max-score").innerHTML = maxScore
alert("大吉大利,今晚吃鸡!")
}
function reset() {
const container = document.getElementById("container")
const childs = container.childNodes;
for (let i = childs.length - 1; i >= 0; i--) {
container.removeChild(childs[i]);
}
fixedBlocks = {}
score = 0
document.getElementById("current-score").innerHTML = score
init()
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>俄罗斯方块</title>
<style>
body {
display: flex;
}
.container {
position: relative;
width: 200px;
height: 360px;
background-color: #000;
}
.activity-model {
width: 20px;
height: 20px;
background-color: cadetblue;
border: 1px solid #eeeeee;
box-sizing: border-box;
position: absolute;
}
.fixed-model {
width: 20px;
height: 20px;
background-color: #fefefe;
border: 1px solid #333333;
box-sizing: border-box;
position: absolute;
}
#scores {
margin-left: 20px;
}
</style>
</head>
<body>
<div id="container" class="container">
</div>
<div id="scores">
<p>最高分:<span id="max-score">0</span></p>
<p>分数:<span id="current-score">0</span></p>
<button onclick="reset()">重新开始</button>
</div>
<script>
const STEP = 20
const ROW_COUNT = 18, COL_COUNT = 10
const MODELS = [
{
0: {
row: 2,
col: 0
},
1: {
row: 2,
col: 1
},
2: {
row: 2,
col: 2
},
3: {
row: 1,
col: 2
}
},
{
0: {
row: 1,
col: 1
},
1: {
row: 0,
col: 0
},
2: {
row: 1,
col: 0
},
3: {
row: 2,
col: 0
}
},
{
0: {
row: 1,
col: 1
},
1: {
row: 2,
col: 1
},
2: {
row: 1,
col: 2
},
3: {
row: 2,
col: 2
}
},
{
0: {
row: 0,
col: 0
},
1: {
row: 0,
col: 1
},
2: {
row: 0,
col: 2
},
3: {
row: 0,
col: 3
}
},
{
0: {
row: 1,
col: 1
},
1: {
row: 1,
col: 2
},
2: {
row: 2,
col: 2
},
3: {
row: 2,
col: 3
}
}
]
let currentModel = {}
let currentX = 0, currentY = 0
let fixedBlocks = {}
let mInterval = null
let maxScore = 0
let score = 0
function init() {
createModel()
onKeyDown()
}
init()
function createModel() {
if (isGameOver()) {
gameOver()
return
}
const randow = Math.floor(Math.random() * MODELS.length)
currentModel = MODELS[randow]
currentY = 0
currentY = 0
for (const key in currentModel) {
const divEle = document.createElement('div')
divEle.className = "activity-model"
document.getElementById("container").appendChild(divEle)
}
locationBlocks()
autoDown()
}
function locationBlocks() {
checkBound()
const eles = document.getElementsByClassName("activity-model")
for (let i = 0; i < eles.length; i++) {
const activityModelEle = eles[i]
const blockModel = currentModel[i]
activityModelEle.style.top = (currentY + blockModel.row) * STEP + "px"
activityModelEle.style.left = (currentX + blockModel.col) * STEP + "px"
}
}
function onKeyDown() {
document.onkeydown = event => {
switch (event.keyCode) {
case 38:
rotate()
break;
case 39:
move(1, 0)
break;
case 40:
move(0, 1)
break;
case 37:
move(-1, 0)
break;
default:
break;
}
}
}
function move(x, y) {
if (isMeet(currentX + x, currentY + y, currentModel)) {
if (y != 0) {
fixedBottomModel()
}
return
}
currentX += x
currentY += y
locationBlocks()
}
function rotate() {
const cloneCurrentModel = JSON.parse(JSON.stringify(currentModel))
for (const key in cloneCurrentModel) {
const blockModel = cloneCurrentModel[key]
let temp = blockModel.row
blockModel.row = blockModel.col
blockModel.col = 3 - temp
}
if (isMeet(currentX, currentY, cloneCurrentModel)) {
return
}
currentModel = cloneCurrentModel
locationBlocks()
}
function checkBound() {
let leftBound = 0, rightBound = COL_COUNT, bottomBound = ROW_COUNT
for (const key in currentModel) {
const blockModel = currentModel[key]
if ((blockModel.col + currentX) < 0) {
currentX++
}
if ((blockModel.col + currentX) >= rightBound) {
currentX--
}
if ((blockModel.row + currentY) >= bottomBound) {
currentY--
fixedBottomModel()
}
}
}
function fixedBottomModel() {
const activityModelEles = document.getElementsByClassName('activity-model')
;[...activityModelEles].forEach((ele, i) => {
ele.className = "fixed-model"
const blockModel = currentModel[i]
fixedBlocks[`${currentY + blockModel.row}_${currentX + blockModel.col}`] = ele
})
isRemoveLine()
createModel()
}
function isMeet(x, y, model) {
for (const key in model) {
const blockModel = model[key]
if (fixedBlocks[`${y + blockModel.row}_${x + blockModel.col}`]) {
return true
}
}
return false
}
function isRemoveLine() {
for (let i = 0; i < ROW_COUNT; i++) {
let flag = true
for (let j = 0; j < COL_COUNT; j++) {
if (!fixedBlocks[`${i}_${j}`]) {
flag = false
break
}
}
if (flag) {
removeLine(i)
}
}
}
function removeLine(line) {
for (let i = 0; i < COL_COUNT; i++) {
document.getElementById("container").removeChild(fixedBlocks[`${line}_${i}`])
fixedBlocks[`${line}_${i}`] = null
}
score += COL_COUNT
document.getElementById("current-score").innerHTML = score
downLine(line)
}
function downLine(line) {
for (let i = line - 1; i >= 0; i--) {
for (let j = 0; j < COL_COUNT; j++) {
if (!fixedBlocks[`${i}_${j}`]) continue
fixedBlocks[`${i + 1}_${j}`] = fixedBlocks[`${i}_${j}`]
fixedBlocks[`${i + 1}_${j}`].style.top = (i + 1) * STEP + "px"
fixedBlocks[`${i}_${j}`] = null
}
}
}
function autoDown() {
if (mInterval) {
clearInterval(mInterval)
}
mInterval = setInterval(() => {
move(0, 1)
}, 600)
}
function isGameOver() {
for (let i = 0; i < COL_COUNT; i++) {
if (fixedBlocks[`0_${i}`]) return true
}
return false
}
function gameOver() {
if (mInterval) {
clearInterval(mInterval)
}
maxScore = Math.max(maxScore, score)
document.getElementById("max-score").innerHTML = maxScore
alert("大吉大利,今晚吃鸡!")
}
function reset() {
const container = document.getElementById("container")
const childs = container.childNodes;
for (let i = childs.length - 1; i >= 0; i--) {
container.removeChild(childs[i]);
}
fixedBlocks = {}
score = 0
document.getElementById("current-score").innerHTML = score
init()
}
</script>
</body>
</html>