python 数据逆时针旋转270度_Python自动耍俄罗斯方块

6f543dcb23639ef6114c3764c5ffef6e.png

d77ce2b32a35705487f80d60044c5de8.gif

小笨聪前面的文章总是用 Python 来进行爬虫,今天就换一下口味,体验一下 Python 自动耍俄罗斯方块。

小游戏 | Python自动玩俄罗斯方块​mp.weixin.qq.com
abc89b89eae458d9634f370ffac1a579.png

俄罗斯方块(Tetris) 规则:由小方块组成的不同形状的板块陆续从屏幕上方落下来,玩家通过调整板块的位置和方向,使它们在屏幕底部拼出完整的一条或几条。这些完整的横条会随即消失,给新落下来的板块腾出空间,与此同时,玩家得到分数奖励。没有被消除掉的方块不断堆积起来,一旦堆到屏幕顶端,玩家便告输,游戏结束。(来自百度百科)

af8ebcff9caa241d30be081df8b3cd85.png
俄罗斯方块游戏界面

我们此次的任务就是在这条规则的基础上,利用 Python 和简单的 AI 算法,实现自动寻找最优位置和调整方块形态,达到快速高效得分的目的。(最终效果见文末视频

代码分三部分:

  1. utils.py:基础设置和界面设计
  2. ai.py:AI算法和具体方式
  3. AITetris.py:实现游戏主循环

一.基础设置和界面设计

1.定义一个俄罗斯方块

方块共有7种形状,每块包含四个人小方块,分别写出小方块的相对坐标、定义获取小方块旋转角度、绝对坐标和相对坐标边界的函数。

 1class tetrisShape():
 2    def __init__(self, shape=0):
 3        # 空块
 4        self.shape_empty = 0
 5        # 一字型块
 6        self.shape_I = 1
 7        # L型块
 8        self.shape_L = 2
 9        # 向左的L型块
10        self.shape_J = 3
11        # T型块
12        self.shape_T = 4
13        # 田字型块
14        self.shape_O = 5
15        # 反向Z型块
16        self.shape_S = 6
17        # Z型块
18        self.shape_Z = 7
19        # 每种块包含的四个小方块相对坐标分布
20        self.shapes_relative_coords = [
21                                         [[0, 0], [0, 0], [0, 0], [0, 0]],
22                                         [[0, -1], [0, 0], [0, 1], [0, 2]],
23                                         [[0, -1], [0, 0], [0, 1], [1, 1]],
24                                         [[0, -1], [0, 0], [0, 1], [-1, 1]],
25                                         [[0, -1], [0, 0], [0, 1], [1, 0]],
26                                         [[0, 0], [0, -1], [1, 0], [1, -1]],
27                                         [[0, 0], [0, -1], [-1, 0], [1, -1]],
28                                         [[0, 0], [0, -1], [1, 0], [-1, -1]]
29                                      ]
30        self.shape = shape
31        self.relative_coords = self.shapes_relative_coords[self.shape]
32    '''获得该形状当前旋转状态的四个小方块的相对坐标分布'''
33    def getRotatedRelativeCoords(self, direction):
34        # 初始分布
35        if direction == 0 or self.shape == self.shape_O:
36            return self.relative_coords
37        # 逆时针旋转90度
38        if direction == 1:
39            return [[-y, x] for x, y in self.relative_coords]
40        # 逆时针旋转180度
41        if direction == 2:
42            if self.shape in [self.shape_I, self.shape_Z, self.shape_S]:
43                return self.relative_coords
44            else:
45                return [[-x, -y] for x, y in self.relative_coords]
46        # 逆时针旋转270度
47        if direction == 3:
48            if self.shape in [self.shape_I, self.shape_Z, self.shape_S]:
49                return [[-y, x] for x, y in self.relative_coords]
50            else:
51                return [[y, -x] for x, y in self.relative_coords]

2.内部板块

游戏主界面分为内部版块和外部板块,将游戏进行中的数据记录与数据处理部分定义在内部板块类中,将游戏数据可视化定义在外部板块类中。在内部版块类中,我们可以操作方块向左、向右、顺时针、逆时针、向下和坠落等运动。

在方块移动时,也要实时判断方块是否越界或落地并及时处理,如果落地则将方块合并并整行消除,再创建新的方块,以此循环。

 1class InnerBoard():
 2    def __init__(self, width=10, height=22):
 3        # 宽和长, 单位长度为小方块边长
 4        self.width = width
 5        self.height = height
 6        self.reset()
 7    '''判断当前俄罗斯方块是否可以移动到某位置'''
 8    def ableMove(self, coord, direction=None):
 9        assert len(coord) == 2
10        if direction is None:
11            direction = self.current_direction
12        for x, y in self.current_tetris.getAbsoluteCoords(direction, coord[0], coord[1]):
13            # 超出边界
14            if x >= self.width or x < 0 or y >= self.height or y < 0:
15                return False
16            # 该位置有俄罗斯方块了
17            if self.getCoordValue([x, y]) > 0:
18                return False
19        return True
20    '''向右移动'''
21    def moveRight(self):
22        if self.ableMove([self.current_coord[0]+1, self.current_coord[1]]):
23            self.current_coord[0] += 1
24    '''向左移动'''
25    def moveLeft(self):
26        if self.ableMove([self.current_coord[0]-1, self.current_coord[1]]):
27            self.current_coord[0] -= 1
28    '''顺时针转'''
29    def rotateClockwise(self):
30        if self.ableMove(self.current_coord, (self.current_direction-1) % 4):
31            self.current_direction = (self.current_direction-1) % 4
32    '''逆时针转'''
33    def rotateAnticlockwise(self):
34        if self.ableMove(self.current_coord, (self.current_direction+1) % 4):
35            self.current_direction = (self.current_direction+1) % 4
36    '''向下移动'''
37    def moveDown(self):
38        removed_lines = 0
39        if self.ableMove([self.current_coord[0], self.current_coord[1]+1]):
40            self.current_coord[1] += 1
41        else:
42            x_min, x_max, y_min, y_max = self.current_tetris.getRelativeBoundary(self.current_direction)
43            # 简单起见, 有超出屏幕就判定游戏结束
44            if self.current_coord[1] + y_min < 0:
45                self.is_gameover = True
46                return removed_lines
47            self.mergeTetris()
48            removed_lines = self.removeFullLines()
49            self.createNewTetris()
50        return removed_lines

3.外部板块和侧面板

 1class ExternalBoard(QFrame):
 2    score_signal = pyqtSignal(str)
 3    def __init__(self, parent, grid_size, inner_board):
 4        super().__init__(parent)
 5        self.grid_size = grid_size
 6        self.inner_board = inner_board
 7        self.setFixedSize(grid_size * inner_board.width, grid_size * inner_board.height)
 8        self.initExternalBoard()
 9    '''外部板块初始化'''
10    def initExternalBoard(self):
11        self.score = 0
12    '''把内部板块结构画出来'''
13    def paintEvent(self, event):
14        painter = QPainter(self)
15        for x in range(self.inner_board.width):
16            for y in range(self.inner_board.height):
17                shape = self.inner_board.getCoordValue([x, y])
18                drawCell(painter, x * self.grid_size, y * self.grid_size, shape, self.grid_size)
19        for x, y in self.inner_board.getCurrentTetrisCoords():
20            shape = self.inner_board.current_tetris.shape
21            drawCell(painter, x * self.grid_size, y * self.grid_size, shape, self.grid_size)
22        painter.setPen(QColor(0x777777))
23        painter.drawLine(0, self.height()-1, self.width(), self.height()-1)
24        painter.drawLine(self.width()-1, 0, self.width()-1, self.height())
25        painter.setPen(QColor(0xCCCCCC))
26        painter.drawLine(self.width(), 0, self.width(), self.height())
27        painter.drawLine(0, self.height(), self.width(), self.height())
28    '''数据更新'''
29    def updateData(self):
30        self.score_signal.emit(str(self.score))
31        self.update()
32
33
34 '''
35 侧面板, 右边显示下一个俄罗斯方块的形状
36 '''
37class SidePanel(QFrame):
38    def __init__(self, parent, grid_size, inner_board):
39        super().__init__(parent)
40        self.grid_size = grid_size
41        self.inner_board = inner_board
42        self.setFixedSize(grid_size * 5, grid_size * inner_board.height)
43        self.move(grid_size * inner_board.width, 0)
44    '''画侧面板'''
45    def paintEvent(self, event):
46        painter = QPainter(self)
47        x_min, x_max, y_min, y_max = self.inner_board.next_tetris.getRelativeBoundary(0)
48        dy = 3 * self.grid_size
49        dx = (self.width() - (x_max - x_min) * self.grid_size) / 2
50        shape = self.inner_board.next_tetris.shape
51        for x, y in self.inner_board.next_tetris.getAbsoluteCoords(0, 0, -y_min):
52            drawCell(painter, x * self.grid_size + dx, y * self.grid_size + dy, shape, self.grid_size)
53    '''更新数据'''
54    def updateData(self):
55        self.update()   

二、AI算法和具体方式

此处AI 算法基本思想就是,遍历当前可操作的俄罗斯方块和下一个可操作的俄罗斯方块(根据不同的策略,即选择不同的位置和旋转角度)下落到底部后组成的所有可能的未来场景,从这些未来场景中选择一个最优的,其对应的当前可操作的俄罗斯方块的行动策略即为当前解,具体的代码实现如下:

 1# 简单的AI算法
 2for d_now in current_direction_range:
 3    x_now_min, x_now_max, y_now_min, y_now_max = self.inner_board.current_tetris.getRelativeBoundary(d_now)
 4    for x_now in range(-x_now_min, self.inner_board.width - x_now_max):
 5        board = self.getFinalBoardData(d_now, x_now)
 6        for d_next in next_direction_range:
 7            x_next_min, x_next_max, y_next_min, y_next_max = self.inner_board.next_tetris.getRelativeBoundary(d_next)
 8            distances = self.getDropDistances(board, d_next, range(-x_next_min, self.inner_board.width-x_next_max))
 9            for x_next in range(-x_next_min, self.inner_board.width-x_next_max):
10                score = self.calcScore(copy.deepcopy(board), d_next, x_next, distances)
11                if not action or action[2] < score:
12                    action = [d_now, x_now, score]
13return action

未来场景优劣评定考虑的因素有:

1)可消除的行数;

2)堆积后的俄罗斯方块内的虚洞数量;

3)堆积后的俄罗斯方块内的小方块数量;

4)堆积后的俄罗斯方块的最高点;

5)堆积后的俄罗斯方块的高度(每一列都有一个高度)标准差;

6)堆积后的俄罗斯方块的高度一阶前向差分;

7)堆积后的俄罗斯方块的高度一阶前向差分的标准差;

9)堆积后的俄罗斯方块的最高点和最低点之差。

代码实现如下:

 1 # 下个俄罗斯方块以某种方式模拟到达底部
 2 board = self.imitateDropDown(board, self.inner_board.next_tetris, d_next, x_next, distances[x_next])
 3 width, height = self.inner_board.width, self.inner_board.height
 4 # 下一个俄罗斯方块以某方案行动到达底部后的得分(可消除的行数)
 5 removed_lines = 0
 6# 空位统计
 7 hole_statistic_0 = [0] * width
 8 hole_statistic_1 = [0] * width
 9 # 方块数量
10 num_blocks = 0
11 # 空位数量
12 num_holes = 0
13 # 每个x位置堆积俄罗斯方块的最高点
14 roof_y = [0] * width
15 for y in range(height-1, -1, -1):
16    # 是否有空位
17    has_hole = False
18    # 是否有方块
19    has_block = False
20    for x in range(width):
21        if board[x + y * width] == tetrisShape().shape_empty:
22            has_hole = True
23            hole_statistic_0[x] += 1
24        else:
25            has_block = True
26            roof_y[x] = height - y
27                if hole_statistic_0[x] > 0:
28                 hole_statistic_1[x] += hole_statistic_0[x]
29                 hole_statistic_0[x] = 0
30            if hole_statistic_1[x] > 0:
31                 num_blocks += 1
32        if not has_block:
33           break
34        if not has_hole and has_block:
35           removed_lines += 1
36 ......

三、实现游戏主循环

定义俄罗斯方块游戏类初始化(包括块大小、下落速度、水平布局、AI控制等),并含有开始、暂停、界面更新等事件。

 1class TetrisGame(QMainWindow):
 2    def __init__(self):
 3        super().__init__()
 4        # 是否暂停ing
 5        self.is_paused = False
 6        # 是否开始ing
 7        self.is_started = False
 8        self.initUI()
 9    '''界面初始化'''
10    def initUI(self):
11        # 块大小
12        self.grid_size = 22
13        # 游戏帧率
14        self.fps = 100
15        self.timer = QBasicTimer()
16        # 水平布局
17        layout_horizontal = QHBoxLayout()
18        self.inner_board = InnerBoard()
19        self.external_board = ExternalBoard(self, self.grid_size, self.inner_board)
20
21     .
22     .
23     .
24
25  '''按键事件'''
26    def keyPressEvent(self, event):
27        if not self.is_started or self.inner_board.current_tetris == tetrisShape().shape_empty:
28            super(TetrisGame, self).keyPressEvent(event)
29            return
30        key = event.key()
31        # P键暂停
32        if key == Qt.Key_P:
33            self.pause()
34            return
35        if self.is_paused:
36            return
37        else:
38            super(TetrisGame, self).keyPressEvent(event)
39        self.updateWindow()
40
41
42if __name__ == '__main__':
43    app = QApplication([])

最终效果视频展示

25dedabf05d3f39d088d9ef94a631d16.png
Python自动耍俄罗斯方块https://www.zhihu.com/video/1158808534270595072

以上就是本次Python自动耍俄罗斯方块的过程,微信公众号“学编程的金融客”后台回复“ 俄罗斯方块 ”即可获取源码。【完】

小游戏 | Python自动玩俄罗斯方块​mp.weixin.qq.com
abc89b89eae458d9634f370ffac1a579.png

往期推荐

1.爬取流浪地球影评

2.爬取《明日之子》评论和弹幕

3.北上广深租房图鉴

4.爬取抖音视频

5.母亲节祝福代码

你的点赞和关注就是对我最大的支持!

1cd3400fe574f6202b246238dc893fb6.png
保存扫码关注公众号呗
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值