在这篇文章中,我们将创建俄罗斯方块游戏。
1.俄罗斯方块游戏
大多数读者可能都熟悉俄罗斯方块——一款由俄罗斯软件工程师 Alexey Pajitnov 于 1984 年创建的流行且令人上瘾的视频游戏。
让我们来看看游戏的各个部分和规则。
2.棋盘
游戏由一个 10 个单元格宽和 20 个单元格高的棋盘组成,如下所示。
俄
罗
斯
方
块
板
是
一
个
10
∗
20
的
单
元
格
。
俄罗斯方块板是一个 10 * 20 的单元格。
俄罗斯方块板是一个10∗20的单元格。
3.俄罗斯方块 (又名 Tetrominoes)
在俄罗斯方块中,方块以 4 个块为单位从棋盘顶部垂直落下。这些块被称为 Tetrominoes,但在这篇文章中,我们将它们简单地称为“俄罗斯方块块”。
在上图中,我们可以看到棋盘底部有许多俄罗斯方块块(每个颜色不同)。
有七种不同的俄罗斯方块块。我们在代码中使用字母“O”、“I”、“S”、“Z”、“L”、“J”和“T”来表示它们。
俄
罗
斯
方
块
俄罗斯方块
俄罗斯方块
4.键盘控制
俄罗斯方块块从棋盘的顶部落到底部。
- 1.按 A 将块向左移动
- 2.D 向右移动块
- 3.J 向左旋转块
- 4.L 向右旋转块
- 5.I持有当前的块以备将来使用
- 6.S 将块向下移动 1 个单元格。这也称为“软下降”。
- 7.W 将块垂直下降到尽可能低的单元格。它也被称为“硬下降”。
5.游戏规则
如果您通过在块掉落时智能地移动和放置它们来使一行中的所有单元格充满,则整行会清除,并且您会根据清除的行数获得分数。
如果一个动作清除了一行,您将获得 40 分。如果一次清除两行,您将获得 100 分,如果清除三行,您将获得 300 分。
俄罗斯方块清除行的例子。在左侧,由于最右侧列上的蓝色块,我们显示四行完全填满。此状态更改为右侧显示的状态,为用户提供俄罗斯方块或 1200 分。
单次清除四行可获得 1200 分!这被称为俄罗斯方块,是您一次所能获得的最高分。
你的目标是在俄罗斯方块堆变得太高之前获得尽可能多的分数。
6.使用 OpenCV 和 numpy 创建俄罗斯方块
让我们看看如何使用 OpenCV 的绘图函数和键盘处理程序以及 numpy 来创建俄罗斯方块游戏。
首先,我们将导入一些标准库。
import cv2
import numpy as np
from random import choice
现在,我们将制作棋盘,初始化一些其他变量,并将参数 SPEED 定义为俄罗斯方块下落的速度。
SPEED = 1 # 控制俄罗斯方块的速度
# 制作棋盘
board = np.uint8(np.zeros([20, 10, 3]))
# 初始化一些变量
quit = False
place = False
drop = False
switch = False
held_piece = ""
flag = 0
score = 0
俄罗斯方块有七种不同的形状。
7.七种俄罗斯方块
# 所有的俄罗斯方块
next_piece = choice(["O", "I", "S", "Z", "L", "J", "T"])
新的俄罗斯方块总是出现在屏幕上的特定位置。
接下来,我们将编写一个函数:
- 1.创建一个俄罗斯方块
- 2.为俄罗斯方块指定颜色。
下面我们有一个函数来获取给定俄罗斯方块的生成位置和颜色。
def get_info(piece):
if piece == "I":
coords = np.array([[0, 3], [0, 4], [0, 5], [0, 6]])
color = [255, 155, 15]
elif piece == "T":
coords = np.array([[1, 3], [1, 4], [1, 5], [0, 4]])
color = [138, 41, 175]
elif piece == "L":
coords = np.array([[1, 3], [1, 4], [1, 5], [0, 5]])
color = [2, 91, 227]
elif piece == "J":
coords = np.array([[1, 3], [1, 4], [1, 5], [0, 3]])
color = [198, 65, 33]
elif piece == "S":
coords = np.array([[1, 5], [1, 4], [0, 3], [0, 4]])
color = [55, 15, 215]
elif piece == "Z":
coords = np.array([[1, 3], [1, 4], [0, 4], [0, 5]])
color = [1, 177, 89]
else:
coords = np.array([[0, 4], [0, 5], [1, 4], [1, 5]])
color = [2, 159, 227]
return coords, color
8.显示棋盘
现在让我们编写一个用于显示棋盘和捕获键盘事件的函数。
def display(board, coords, color, next_info, held_info, score, SPEED):
# 生成显示
border = np.uint8(127 - np.zeros([20, 1, 3]))
border_ = np.uint8(127 - np.zeros([1, 34, 3]))
dummy = board.copy()
dummy[coords[:,0], coords[:,1]] = color
right = np.uint8(np.zeros([20, 10, 3]))
right[next_info[0][:,0] + 2, next_info[0][:,1]] = next_info[1]
left = np.uint8(np.zeros([20, 10, 3]))
left[held_info[0][:,0] + 2, held_info[0][:,1]] = held_info[1]
dummy = np.concatenate((border, left, border, dummy, border, right, border), 1)
dummy = np.concatenate((border_, dummy, border_), 0)
dummy = dummy.repeat(20, 0).repeat(20, 1)
dummy = cv2.putText(dummy, str(score), (520, 200), cv2.FONT_HERSHEY_DUPLEX, 1, [0, 0, 255], 2)
# 给玩家的说明
dummy = cv2.putText(dummy, "A - move left", (45, 200), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "D - move right", (45, 225), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "S - move down", (45, 250), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "W - hard drop", (45, 275), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "J - rotate left", (45, 300), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "L - rotate right", (45, 325), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "I - hold", (45, 350), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
cv2.imshow("Tetris", dummy)
key = cv2.waitKey(int(1000/SPEED))
return key
9.主循环
这是代码的主要部分。我们有一个 while 循环,每次迭代时我们都会在游戏中放置一个新的部分。
在俄罗斯方块中,您可以按某个键来保留块。保留块可以通过与当前块交换以便在将来使用。
在下面的代码中,我们首先检查用户是否想使用 switch 变量将当前块与保留块交换。
if __name__ == "__main__":
while not quit:
# 检查用户是否想要交换当前块与保留块
if switch:
# 交换当前块与保留块
held_piece, current_piece = current_piece, held_piece
switch = False
如果 switch
变量设置为 false
,我们将next_piece
分配给current_piece
并随机选择一个新的 next_piece。
else:
# 生成下一块并更新当前一块
current_piece = next_piece
next_piece = choice(["I", "T", "L", "J", "Z", "S", "O"])
if flag > 0:
flag -= 1
接下来,我们确定current_piece
、next_piece
和held_piece
的颜色和位置。
# 确定当前、下一个和保留的块的颜色和位置
if held_piece == "":
held_info = np.array([[0, 0]]), [0, 0, 0]
else:
held_info = get_info(held_piece)
next_info = get_info(next_piece)
coords, color = get_info(current_piece)
if current_piece == "I":
top_left = [-2, 3]
这个 if 语句只是检查游戏是否需要终止(即俄罗斯方块堆得太高),我们通过检查下一块的生成位置是否与另一块重叠来做到这一点。
if not np.all(board[coords[:,0], coords[:,1]] == 0):
break
接下来,我们在主循环中添加另一个 while 循环。这个新循环的每次迭代对应于块向下移动一格。
首先,我们使用 display() 函数显示棋盘并接收键盘输入。
我们还复制了原始位置。
while True:
# 显示棋盘并按下按键
key = display(board, coords, color, next_info, held_info, score, SPEED)
# 创建位置的副本
dummy = coords.copy()
上面的 key 变量存储按下键盘输入的 ASCII 代码。根据按下的键,我们采取不同的行动。
a 和 d
键控制块的左右移动。
if key == ord("a"):
# 如果俄罗斯方块不靠左墙,则向左移动块
if np.min(coords[:,1]) > 0:
coords[:,1] -= 1
if current_piece == "I":
top_left[1] -= 1
elif key == ord("d"):
# 如果俄罗斯方块不靠右墙,则向右移动块
if np.max(coords[:,1]) < 9:
coords[:,1] += 1
if current_piece == "I":
top_left[1] += 1
键 j 和 l
用于旋转块。
为了编码旋转,我们需要处理三种俄罗斯方块——方形块、线条块和所有其他块。
对于方形块,旋转很简单;你什么都不做!
对于任何不是正方形的块,我们可以把它嵌入在一个正方形中,然后旋转它。
线条块嵌入在一个4×4的正方形而不是一个3×3的正方形中,因此我们需要区别对待它。
elif key == ord("j") or key == ord("l"):
# 旋转
# arr 是旋转的附近点的数组,pov 是 arr 内块的索引
if current_piece != "I" and current_piece != "O":
if coords[1,1] > 0 and coords[1,1] < 9:
arr = coords[1] - 1 + np.array([[[x, y] for y in range(3)] for x in range(3)])
pov = coords - coords[1] + 1
elif current_piece == "I":
# 线条块有一个 4x4 的阵列,所以它需要单独的代码
arr = top_left + np.array([[[x, y] for y in range(4)] for x in range(4)])
pov = np.array([np.where(np.logical_and(arr[:,:,0] == pos[0], arr[:,:,1] == pos[1])) for pos in coords])
pov = np.array([k[0] for k in np.swapaxes(pov, 1, 2)])
# 旋转阵列并将块重新定位到它现在的位置
if current_piece != "O":
if key == ord("j"):
arr = np.rot90(arr, -1)
else:
arr = np.rot90(arr)
coords = arr[pov[:,0], pov[:,1]]
最后,我们将处理 w、i、DELETE 和 ESC
键。
按 w
实现硬下降。按 i
保留块。
DELETE 和 ESC
键结束程序。
elif key == ord("w"):
# 硬下降设置为真
drop = True
elif key == ord("i"):
# 退出循环并告诉程序交换当前块与保留块
if flag == 0:
if held_piece == "":
held_piece = current_piece
else:
switch = True
flag = 2
break
elif key == 8 or key == 27:
quit = True
break
如果发生这种碰撞,我们会使用存储在虚拟变量中的坐标副本将新位置更改回原始位置。
# 检查块是否与其他块重叠或是否在棋盘外,如果是,则将位置更改为发生任何事情之前的位置
if np.max(coords[:,0]) < 20 and np.min(coords[:,0]) >= 0:
if not (current_piece == "I" and (np.max(coords[:,1]) >= 10 or np.min(coords[:,1]) < 0)):
if not np.all(board[coords[:,0], coords[:,1]] == 0):
coords = dummy.copy()
else:
coords = dummy.copy()
else:
coords = dummy.copy()
最后,我们对“硬下降”进行编码。我们使用 while 循环来检查俄罗斯方块是否可以向下移动一步,如果它与现有俄罗斯方块碰撞或到达棋盘底部,则停止向下移动。
if drop:
# 检查俄罗斯方块是否可以向下移动一步,如果它与现有俄罗斯方块碰撞或到达棋盘底部,则停止向下移动。
while not place:
if np.max(coords[:,0]) != 19:
# 检查块与现有俄罗斯方块是否碰撞
for pos in coords:
if not np.array_equal(board[pos[0] + 1, pos[1]], [0, 0, 0]):
place = True
break
else:
# 如果俄罗斯方块的位置到达棋盘底部,则放置它
place = True
if place:
break
# 继续下降并检查何时需要放置块
coords[:,0] += 1
score += 1
if current_piece == "I":
top_left[0] += 1
drop = False
如果我们不硬放下,那么我们只需要检查是否需要放置块(即停止移动)。当块到达棋盘底部或击中另一块时,就放置了块。
如果上述情况均不适用,我们将其向下移动一个。
else:
# 检查是否需要放置块
if np.max(coords[:,0]) != 19:
for pos in coords:
if not np.array_equal(board[pos[0] + 1, pos[1]], [0, 0, 0]):
place = True
break
else:
place = True
if place:
# 将块放在棋盘上的位置
for pos in coords:
board[tuple(pos)] = color
# 将place 重置为 False
place = False
break
# 向下移动 1
coords[:,0] += 1
if key == ord("s"):
score += 1
if current_piece == "I":
top_left[0] += 1
最后,对于外部 while 循环的每次迭代(也就是每次放置一块时),我们检查是否对任何行进行了评分并更新了点数。
# 清除行并计算已清除的行数并更新分数
lines = 0
for line in range(20):
if np.all([np.any(pos != 0) for pos in board[line]]):
lines += 1
board[1:line+1] = board[:line]
if lines == 1:
score += 40
elif lines == 2:
score += 100
elif lines == 3:
score += 300
elif lines == 4:
score += 1200
10.完整代码展示
import cv2
import numpy as np
from random import choice
SPEED = 1 # 控制俄罗斯方块的速度
# 创建棋盘
board = np.uint8(np.zeros([20, 10, 3]))
# 初始化一些变量
quit = False
place = False
drop = False
switch = False
held_piece = ""
flag = 0
score = 0
# 所有的俄罗斯方块
next_piece = choice(["O", "I", "S", "Z", "L", "J", "T"])
def get_info(piece):
if piece == "I":
coords = np.array([[0, 3], [0, 4], [0, 5], [0, 6]])
color = [255, 155, 15]
elif piece == "T":
coords = np.array([[1, 3], [1, 4], [1, 5], [0, 4]])
color = [138, 41, 175]
elif piece == "L":
coords = np.array([[1, 3], [1, 4], [1, 5], [0, 5]])
color = [2, 91, 227]
elif piece == "J":
coords = np.array([[1, 3], [1, 4], [1, 5], [0, 3]])
color = [198, 65, 33]
elif piece == "S":
coords = np.array([[1, 5], [1, 4], [0, 3], [0, 4]])
color = [55, 15, 215]
elif piece == "Z":
coords = np.array([[1, 3], [1, 4], [0, 4], [0, 5]])
color = [1, 177, 89]
else:
coords = np.array([[0, 4], [0, 5], [1, 4], [1, 5]])
color = [2, 159, 227]
return coords, color
def display(board, coords, color, next_info, held_info, score, SPEED):
# 生成显示
border = np.uint8(127 - np.zeros([20, 1, 3]))
border_ = np.uint8(127 - np.zeros([1, 34, 3]))
dummy = board.copy()
dummy[coords[:, 0], coords[:, 1]] = color
right = np.uint8(np.zeros([20, 10, 3]))
right[next_info[0][:, 0] + 2, next_info[0][:, 1]] = next_info[1]
left = np.uint8(np.zeros([20, 10, 3]))
left[held_info[0][:, 0] + 2, held_info[0][:, 1]] = held_info[1]
dummy = np.concatenate((border, left, border, dummy, border, right, border), 1)
dummy = np.concatenate((border_, dummy, border_), 0)
dummy = dummy.repeat(20, 0).repeat(20, 1)
dummy = cv2.putText(dummy, str(score), (520, 200), cv2.FONT_HERSHEY_DUPLEX, 1, [0, 0, 255], 2)
# 给玩家的说明
dummy = cv2.putText(dummy, "A - move left", (45, 200), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "D - move right", (45, 225), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "S - move down", (45, 250), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "W - hard drop", (45, 275), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "J - rotate left", (45, 300), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "L - rotate right", (45, 325), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
dummy = cv2.putText(dummy, "I - hold", (45, 350), cv2.FONT_HERSHEY_DUPLEX, 0.6, [0, 0, 255])
cv2.imshow("Tetris", dummy)
key = cv2.waitKey(int(1000 / SPEED))
return key
if __name__ == "__main__":
while not quit:
# 检查用户是否想要交换保留块与当前块
if switch:
# 交换保留块与当前块
held_piece, current_piece = current_piece, held_piece
switch = False
else:
# 生成下一块并更新当前一块
current_piece = next_piece
next_piece = choice(["I", "T", "L", "J", "Z", "S", "O"])
if flag > 0:
flag -= 1
# 确定当前、下一个和保留块的颜色和位置
if held_piece == "":
held_info = np.array([[0, 0]]), [0, 0, 0]
else:
held_info = get_info(held_piece)
next_info = get_info(next_piece)
coords, color = get_info(current_piece)
if current_piece == "I":
top_left = [-2, 3]
if not np.all(board[coords[:, 0], coords[:, 1]] == 0):
break
while True:
# 显示棋盘并按下按键
key = display(board, coords, color, next_info, held_info, score, SPEED)
# 创建位置的副本
dummy = coords.copy()
if key == ord("a"):
# 如果块不靠左墙,则向左移动块
if np.min(coords[:, 1]) > 0:
coords[:, 1] -= 1
if current_piece == "I":
top_left[1] -= 1
elif key == ord("d"):
# 如果它不靠在右墙上,则将其向右移动
if np.max(coords[:, 1]) < 9:
coords[:, 1] += 1
if current_piece == "I":
top_left[1] += 1
elif key == ord("j") or key == ord("l"):
# 旋转
# arr 是旋转的附近点的数组,pov 是 arr 内块的索引
if current_piece != "I" and current_piece != "O":
if coords[1, 1] > 0 and coords[1, 1] < 9:
arr = coords[1] - 1 + np.array([[[x, y] for y in range(3)] for x in range(3)])
pov = coords - coords[1] + 1
elif current_piece == "I":
# 线条块有一个 4x4 的阵列,所以它需要单独的代码
arr = top_left + np.array([[[x, y] for y in range(4)] for x in range(4)])
pov = np.array(
[np.where(np.logical_and(arr[:, :, 0] == pos[0], arr[:, :, 1] == pos[1])) for pos in coords])
pov = np.array([k[0] for k in np.swapaxes(pov, 1, 2)])
# 转阵列并将块重新定位到它现在的位置
if current_piece != "O":
if key == ord("j"):
arr = np.rot90(arr, -1)
else:
arr = np.rot90(arr)
coords = arr[pov[:, 0], pov[:, 1]]
elif key == ord("w"):
# 硬下降设置为真
drop = True
elif key == ord("i"):
# 退出循环并告诉程序交换保留块和当前块
if flag == 0:
if held_piece == "":
held_piece = current_piece
else:
switch = True
flag = 2
break
elif key == 8 or key == 27:
quit = True
break
# 检查块是否与其他块重叠或是否在棋盘外,如果是,则将位置更改为发生任何事情之前的位置
if np.max(coords[:, 0]) < 20 and np.min(coords[:, 0]) >= 0:
if not (current_piece == "I" and (np.max(coords[:, 1]) >= 10 or np.min(coords[:, 1]) < 0)):
if not np.all(board[coords[:, 0], coords[:, 1]] == 0):
coords = dummy.copy()
else:
coords = dummy.copy()
else:
coords = dummy.copy()
if drop:
# 循环的每次迭代都将块向下移动 1,如果棋子放到底部或另一块上,则停止并放置它
while not place:
if np.max(coords[:, 0]) != 19:
# 检查块是否碰到其它块
for pos in coords:
if not np.array_equal(board[pos[0] + 1, pos[1]], [0, 0, 0]):
place = True
break
else:
# 如果块的位置在棋盘底部,则放置它
place = True
if place:
break
# 继续下降并检查何时需要放置块
coords[:, 0] += 1
score += 1
if current_piece == "I":
top_left[0] += 1
drop = False
else:
# 检查是否需要放置块
if np.max(coords[:, 0]) != 19:
for pos in coords:
if not np.array_equal(board[pos[0] + 1, pos[1]], [0, 0, 0]):
place = True
break
else:
place = True
if place:
# 将块放在棋盘上的位置
for pos in coords:
board[tuple(pos)] = color
# 将place重置为 False
place = False
break
# 向下移动 1
coords[:, 0] += 1
if key == ord("s"):
score += 1
if current_piece == "I":
top_left[0] += 1
# 清除行并计算已清除的行数并更新分数
lines = 0
for line in range(20):
if np.all([np.any(pos != 0) for pos in board[line]]):
lines += 1
board[1:line + 1] = board[:line]
if lines == 1:
score += 40
elif lines == 2:
score += 100
elif lines == 3:
score += 300
elif lines == 4:
score += 1200