Python小游戏之300行代码实现俄罗斯方块


Posted in Python onJanuary 04, 2019

前言

本文代码基于 python3.6 和 pygame1.9.4。

俄罗斯方块是儿时最经典的游戏之一,刚开始接触 pygame 的时候就想写一个俄罗斯方块。但是想到旋转,停靠,消除等操作,感觉好像很难啊,等真正写完了发现,一共也就 300 行代码,并没有什么难的。

先来看一个游戏截图,有点丑,好吧,我没啥美术细胞,但是主体功能都实现了,可以玩起来。

Python小游戏之300行代码实现俄罗斯方块

现在来看一下实现的过程。

外形

俄罗斯方块整个界面分为两部分,一部分是左边的游戏区域,另一部分是右边的显示区域,显示得分、速度、下一个方块样式等。这里就不放截图了,看上图就可以。

游戏区域跟贪吃蛇一样,是由一个个小方格组成的,为了看得直观,我特意画了网格线。

import sys
import pygame
from pygame.locals import *

SIZE = 30 # 每个小方格大小
BLOCK_HEIGHT = 20 # 游戏区高度
BLOCK_WIDTH = 10 # 游戏区宽度
BORDER_WIDTH = 4 # 游戏区边框宽度
BORDER_COLOR = (40, 40, 200) # 游戏区边框颜色
SCREEN_WIDTH = SIZE * (BLOCK_WIDTH + 5) # 游戏屏幕的宽
SCREEN_HEIGHT = SIZE * BLOCK_HEIGHT # 游戏屏幕的高
BG_COLOR = (40, 40, 60) # 背景色
BLACK = (0, 0, 0)


def print_text(screen, font, x, y, text, fcolor=(255, 255, 255)):
 imgText = font.render(text, True, fcolor)
 screen.blit(imgText, (x, y))


def main():
 pygame.init()
 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
 pygame.display.set_caption('俄罗斯方块')

 font1 = pygame.font.SysFont('SimHei', 24) # 黑体24
 font_pos_x = BLOCK_WIDTH * SIZE + BORDER_WIDTH + 10 # 右侧信息显示区域字体位置的X坐标
 font1_height = int(font1.size('得分')[1])

 score = 0  # 得分

 while True:
 for event in pygame.event.get():
  if event.type == QUIT:
  sys.exit()

 # 填充背景色
 screen.fill(BG_COLOR)
 # 画游戏区域分隔线
 pygame.draw.line(screen, BORDER_COLOR,
    (SIZE * BLOCK_WIDTH + BORDER_WIDTH // 2, 0),
    (SIZE * BLOCK_WIDTH + BORDER_WIDTH // 2, SCREEN_HEIGHT), BORDER_WIDTH)
 # 画网格线 竖线
 for x in range(BLOCK_WIDTH):
  pygame.draw.line(screen, BLACK, (x * SIZE, 0), (x * SIZE, SCREEN_HEIGHT), 1)
 # 画网格线 横线
 for y in range(BLOCK_HEIGHT):
  pygame.draw.line(screen, BLACK, (0, y * SIZE), (BLOCK_WIDTH * SIZE, y * SIZE), 1)

 print_text(screen, font1, font_pos_x, 10, f'得分: ')
 print_text(screen, font1, font_pos_x, 10 + font1_height + 6, f'{score}')
 print_text(screen, font1, font_pos_x, 20 + (font1_height + 6) * 2, f'速度: ')
 print_text(screen, font1, font_pos_x, 20 + (font1_height + 6) * 3, f'{score // 10000}')
 print_text(screen, font1, font_pos_x, 30 + (font1_height + 6) * 4, f'下一个:')

 pygame.display.flip()


if __name__ == '__main__':
 main()

方块

接下来就是要定义方块,方块的形状一共有以下 7 种:

Python小游戏之300行代码实现俄罗斯方块

I 型

Python小游戏之300行代码实现俄罗斯方块

O 型

Python小游戏之300行代码实现俄罗斯方块

T 型

Python小游戏之300行代码实现俄罗斯方块

S 型

Python小游戏之300行代码实现俄罗斯方块

Z 型

Python小游戏之300行代码实现俄罗斯方块

L 型

Python小游戏之300行代码实现俄罗斯方块

J 型

这里我做了多次的更改,因为方块最大的长度是长条形的,为4格,所以我统一用了 4 × 4 的方格来定义。这也是可以的,只是后来发现不方便。

为了直观,直接以一个二维数组来定义方块,其中 . 表示空的, 0 表示实心的。(用 . 表示空是为了看得直观,如果用空格会看不清。)
例如 I 行,以 4 × 4 方格定义为

['.0..',
 '.0..',
 '.0..',
 '.0..']

['....',
 '....',
 '0000',
 '....']

方块最难的是需要实现旋转功能,比如 I 型,就有横和竖两种形态。所谓旋转,表面上看,是把方块顺时针旋转了 90°,但实际做的时候,我们并不需要正真的去实现这个“旋转”的效果。

最终实现的时候,这些图形都是我们画在界面上的,而每一次刷新,界面上所有内容都会被清空重画,所以旋转只是画当前方块的时候不再画之前的形状,而是画旋转后的形状。

比如这个 I 型,定义成了 4 × 4 的形状,但实际上只需要 1 × 4 或 4 × 1 就可以了,其他剩下的地方都是空的。它不像 T 型,T 型不是一个矩形,如果用一个矩形来定义,必然有 2 个位置是空的。那么,I 型真的有必要定义成 4 × 4 吗?

答案是肯定的。想想看,如果是 4 × 1 的一个横条,旋转后变成 1 × 4 的竖条,这个位置怎么确定?好像有点困难。但是如果是 4 × 4 的正方形,我们只需要固定起点坐标(左上角)不变,把竖条的 4 × 4 直接替换掉横条的 4 × 4 区域,是不是就实现旋转了?而且位置很容易计算。

另外一点,在有些情况下是不可以旋转的。比如 I 型的竖条,在紧贴左右边框的时候是不可以旋转的。这点我有印象,可以肯定。但是对于其他的形状,我就不是很确定了,我百度搜了下,找了个网页版的俄罗斯方块玩了下,发现也是不可以的。例如:

Python小游戏之300行代码实现俄罗斯方块

在紧贴右边框的时候是无法旋转的。如果要每一个形状都去判断一下,那实在是太烦了。从方块的定义入手,就可以很简单的实现。

例如竖条行,定义是:

['.0..',
 '.0..',
 '.0..',
 '.0..']

竖条是可以贴边的,所以当它在最左边的时候,X 轴坐标是 -1,这是因为定义中左边一竖排是空的。我们只需判定,当方块所定义的形状(包括空的部分)完全在游戏区域内时才可以旋转。

我之前所说,全都定义成 4 × 4 不好,原因就在这里,对于 T 型等其他形状,无法做这个判定。所以,对于 T 型等形状,我们可以定义成 3 × 3 的格式:

['.0.',
 '000',
 '...']

还有一种情况是无法旋转的,就是旋转后的位置已经被别的方块占了。另外下落,左右移动,都要做这个判断。既然这些是一致的,那么就可以用同一个方法来判断。

先要定义一个 game_area 变量,用于存放整个游戏区域当前的状态:

game_area = [['.'] * BLOCK_WIDTH for _ in range(BLOCK_HEIGHT)]

初始状态全是空的,所以全部用 . 初始化就可以了。

另外,需要一些变量定义当前下落方块的状态

cur_block = None # 当前下落方块
cur_pos_x, cur_pos_y = 0, 0 # 当前下落方块的坐标

方块我们是以二维数组的方式定义的,并且存在空行和空列,如果我们遍历这个二维数组判断其所在的区域在当前游戏区域内是否已经被别的方块所占,这个是可以实现的。我们考虑另外一种情况,一个竖条形,左边一排是空的,这空的一排是可以移出游戏区域的,这个怎么判断?每次左移的时候都去判断一下左边一排全都是空吗?这太麻烦了。并且方块都是固定的,所以这些我们可以提前定义好。最终方块定义如下:

from collections import namedtuple

Point = namedtuple('Point', 'X Y')
Block = namedtuple('Block', 'template start_pos end_pos name next')

# S形方块
S_BLOCK = [Block(['.00',
     '00.',
     '...'], Point(0, 0), Point(2, 1), 'S', 1),
   Block(['0..',
     '00.',
     '.0.'], Point(0, 0), Point(1, 2), 'S', 0)]

方块需要包含两个方法,获取随机一个方块和旋转时获取旋转后的方块

BLOCKS = {'O': O_BLOCK,
   'I': I_BLOCK,
   'Z': Z_BLOCK,
   'T': T_BLOCK,
   'L': L_BLOCK,
   'S': S_BLOCK,
   'J': J_BLOCK}


def get_block():
 block_name = random.choice('OIZTLSJ')
 b = BLOCKS[block_name]
 idx = random.randint(0, len(b) - 1)
 return b[idx]


# 获取旋转后的方块
def get_next_block(block):
 b = BLOCKS[block.name]
 return b[block.next]

判断是否可以旋转,下落,移动的方法也很容易实现了

def _judge(pos_x, pos_y, block):
 nonlocal game_area
 for _i in range(block.start_pos.Y, block.end_pos.Y + 1):
  if pos_y + block.end_pos.Y >= BLOCK_HEIGHT:
   return False
  for _j in range(block.start_pos.X, block.end_pos.X + 1):
   if pos_y + _i >= 0 and block.template[_i][_j] != '.' and game_area[pos_y + _i][pos_x + _j] != '.':
    return False
 return True

停靠

最后一个问题是停靠,当方块下落到底或者遇到别的方块之后,就不能在下落了。我将此称之为“停靠”,有个名字说起来也方便一点。

首先是要判断是否可以停靠,停靠发生之后,就是将当前方块的非空点画到游戏区域上,说白了,就是将cur_block的非空点按对应位置复制到game_area里去。并且计算是否有一排被全部填满了,全部填满则消除。

def _dock():
 nonlocal cur_block, next_block, game_area, cur_pos_x, cur_pos_y, game_over
 for _i in range(cur_block.start_pos.Y, cur_block.end_pos.Y + 1):
  for _j in range(cur_block.start_pos.X, cur_block.end_pos.X + 1):
   if cur_block.template[_i][_j] != '.':
    game_area[cur_pos_y + _i][cur_pos_x + _j] = '0'
 if cur_pos_y + cur_block.start_pos.Y <= 0:
  game_over = True
 else:
  # 计算消除
  remove_idxs = []
  for _i in range(cur_block.start_pos.Y, cur_block.end_pos.Y + 1):
   if all(_x == '0' for _x in game_area[cur_pos_y + _i]):
    remove_idxs.append(cur_pos_y + _i)
  if remove_idxs:
   # 消除
   _i = _j = remove_idxs[-1]
   while _i >= 0:
    while _j in remove_idxs:
     _j -= 1
    if _j < 0:
     game_area[_i] = ['.'] * BLOCK_WIDTH
    else:
     game_area[_i] = game_area[_j]
    _i -= 1
    _j -= 1
  cur_block = next_block
  next_block = blocks.get_block()
  cur_pos_x, cur_pos_y = (BLOCK_WIDTH - cur_block.end_pos.X - 1) // 2, -1 - cur_block.end_pos.Y

至此,整个俄罗斯方块的主体功能就算是完成了。

这里很多参数是可以调的,例如觉得旋转别扭,可以直接调整方块的定义,而无需去改动代码逻辑。

源码下载:http://xiazai.3water.com/201901/yuanma/python-Tetris.rar

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python使用calendar输出指定年份全年日历的方法
Apr 04 Python
编写Python脚本来实现最简单的FTP下载的教程
May 04 Python
举例讲解Python设计模式编程中的访问者与观察者模式
Jan 26 Python
Python中shutil模块的常用文件操作函数用法示例
Jul 05 Python
python脚本实现数据导出excel格式的简单方法(推荐)
Dec 30 Python
Python基于matplotlib实现绘制三维图形功能示例
Jan 18 Python
学习python可以干什么
Feb 26 Python
Python函数和模块的使用总结
May 20 Python
PyQt5 加载图片和文本文件的实例
Jun 14 Python
Python 实现顺序高斯消元法示例
Dec 09 Python
python3.7中安装paddleocr及paddlepaddle包的多种方法
Nov 27 Python
Jmeter调用Python脚本实现参数互相传递的实现
Jan 22 Python
django主动抛出403异常的方法详解
Jan 04 #Python
pyspark操作MongoDB的方法步骤
Jan 04 #Python
详解Appium+Python之生成html测试报告
Jan 04 #Python
python虚拟环境迁移方法
Jan 03 #Python
对django xadmin自定义菜单的实例详解
Jan 03 #Python
在Python中关于使用os模块遍历目录的实现方法
Jan 03 #Python
Python代码打开本地.mp4格式文件的方法
Jan 03 #Python
You might like
获取PHP警告错误信息的解决方法
2013/06/03 PHP
php socket客户端及服务器端应用实例
2014/07/04 PHP
php中fgetcsv()函数用法实例
2014/11/28 PHP
解读PHP中上传文件的处理问题
2016/05/29 PHP
PHP+Mysql+Ajax实现淘宝客服或阿里旺旺聊天功能(前台页面)
2017/06/16 PHP
Laravel框架实现的上传图片到七牛功能详解
2019/09/06 PHP
php中加密解密DES类的简单使用方法示例
2020/03/26 PHP
JQuery中$之选择器用法介绍
2011/04/05 Javascript
javascript实现的闭包简单实例
2015/07/17 Javascript
js显示当前日期时间和星期几
2015/10/22 Javascript
jQuery学习笔记之Ajax用法实例详解
2015/12/01 Javascript
详解JavaScript逻辑Not运算符
2015/12/04 Javascript
学习jQuey中的return false
2015/12/18 Javascript
jQuery实现页面点击后退弹出提示框的方法
2016/08/24 Javascript
AngularJs验证重复密码的方法(两种)
2016/11/25 Javascript
canvas实现十二星座星空图
2017/02/14 Javascript
提高JavaScript执行效率的23个实用技巧
2017/03/01 Javascript
jQuery实现form表单序列化转换为json对象功能示例
2018/05/23 jQuery
JQuery事件委托原理与用法实例分析
2019/05/13 jQuery
微信小程序错误this.setData报错及解决过程
2019/09/18 Javascript
zbar解码二维码和条形码示例
2014/02/07 Python
pycharm 使用心得(一)安装和首次使用
2014/06/05 Python
在Python中处理日期和时间的基本知识点整理汇总
2015/05/22 Python
在Django中编写模版节点及注册标签的方法
2015/07/20 Python
使用python和Django完成博客数据库的迁移方法
2018/01/05 Python
Python函数定义及传参方式详解(4种)
2019/03/18 Python
Python正则表达式匹配和提取IP地址
2019/06/06 Python
使用Python将语音转换为文本的方法
2020/08/10 Python
关于python中导入文件到list的问题
2020/10/31 Python
Django数据库迁移常见使用方法
2020/11/12 Python
美国在线眼镜店:GlassesShop
2018/11/15 全球购物
牛津在线药房:Oxford Online Pharmacy
2020/11/16 全球购物
车间工艺员岗位职责
2013/12/09 职场文书
中专自我鉴定
2014/02/05 职场文书
2015年安全月活动总结
2015/03/26 职场文书
APP界面设计技巧和注意事项
2022/04/29 杂记