Learn OpenCV----使用Python和Opencv来完成俄罗斯方块

阅读原文

俄罗斯方块

俄罗斯方块时1984年以为俄罗斯软件工程师开发的一款电子游戏。

我妈曾经非常沉迷。

背景板

在本次编写的程序中,我们的俄罗斯方块的背景板由N个小格组成,每一行有10格,每一列有20格,如下图所示。
在这里插入图片描述

四格拼板

四格拼版时俄罗斯方块游戏的主要组件。顾名思义,每个拼板由四个小方格组成,并且共有七种模式,分别记做“O”,“I”,“S”,“L”,"J"和“T”,具体样式如下图所示。
在这里插入图片描述

键盘控制规则

在本次开发的程序中,共有七个案件能够影响着我们的俄罗斯方块游戏。

  • A键,让方块往左移一格
  • D键,让方块往右移一格
  • J键,让方块逆时针旋转90度
  • L键,让方块顺时针旋转90度
  • I键,保存当前方格以便在之后使用
  • S键,让方块向下移动一格
  • W键,直接让方块向下移动到无法移动

游戏规则

当每行都被方块填满时,改行被清空,玩家获得一定的积分,积分规则如下。

  • 一次清空一行,积分增加40
  • 一次清空两行,积分增加100
  • 一次清空三行,积分增加300
  • 一次增加四行,积分增加1200

开始构建俄罗斯方块游戏

主要使用到了Opencv的绘图函数,键盘处理函数,numpy科学数据库。

导入相关库以及初始化变量

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

显示板

接下来编写显示背景板和键盘控制的函数。

# 这个函数主要创建了背景板,但是用到了这样一个技巧
# 首先构建小型的背景板,该背景板五脏俱全,但是很小,无法良好的展示
# 然后使用repeat函数将小型的背景板等量放大,就像哆啦a梦中的放大手电筒
def display(board, coords, color, next_info, held_info, score, SPEED):

    # 创建一个竖直边界,高度为20,背景色为(127,127,127)
    border = np.uint8(127 - np.zeros([20, 1, 3]))
    # 创建一个水平边界,宽度为34,背景色为(127,127,127)
    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矢量放大,让高等量放大20倍,让宽等量放大20倍
    # (20,10,3)=>(400,200,3)
    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)

    # 在窗口中加入文字操作提示
    # 图像,文字内容,文字起点,字体类型,字体大小,字体颜色(BGR)
    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)
    # 不断等待1000ms后返回按键信息
    # 没有按键按下则返回-1
    key = cv2.waitKey(int(1000/SPEED))

    return key

主循环

为了保证游戏能够有效且持续的进行下去,因此,使用整个while循环来包裹内容,退出的标准是,quit变量为true

# 判断用户是否想用当前方块与持有方块进行交换
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)
    # 用来在旋转过程中更新I型方块的坐标
    if current_piece == "I":
        top_left = [-2, 3]

    # 如果生成的方块所在的坐标的区域不全是黑色的
    # 即,坐标区域有些其他方块,则游戏结束
    if not np.all(board[coords[:,0], coords[:,1]] == 0):
        break

子循环

# 显示背景板并获得用户按下的按键
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:
            # 构建一个3x3的矩形
            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":
        # 对于I形状的方块,需要使用4x4的矩形进行包裹
        arr = top_left + np.array([[[x, y] for y in range(4)] for x in range(4)])
        # I形状的方块在矩形内的坐标
        pov = np.array([np.where(np.logical_and(arr[:,:,0] == pos[0], arr[:,:,1] == pos[1])) for pos in coords])
        # 让坐标变得更规范
        '''
        之前是 [[[2]
                [0]
               [[2]
                [1]]]]
        处理后变成
        [[2 0]
         [2 1]]
        关掉了一个轴,即展平了一个维度
        '''
        pov = np.array([k[0] for k in np.swapaxes(pov, 1, 2)])

    # 对方块进行旋转,并重新定位到之前的位置
    if current_piece != "O":
        if key == ord("j"):
            # 逆时针旋转90度
            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:
     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
     break

 # 下降一格
 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

最后

感兴趣的小伙伴可以去这里免费进行下载带注释的代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值