Tetris 俄罗斯方块游戏

文件建立日期: 2020/03/21
最后修订日期: None
相关软件信息:

win10 Python 3.7.6 Numpy 1.18.1 PySimpleGUI 4.16.0

说明: 本文请随意引用或更改, 只须标示出处及作者, 作者不保证内容絶对正确无误, 如造成任何后果, 请自行负责.

标题: Tetris 俄罗斯方块游戏

写个游戏玩玩….

目标

  1. 不同几何形状的碎片从顶部下降
  2. 在下降过程中,玩家可以横向移动碎片并旋转它们, 直到它们接触到底部或降落在之前放置的棋子上
  3. 掉落的碎片可以加速, 一步一步, 或一次到底.
  4. 游戏的目标是使用棋子水平连成完整的一条线, 当一条线完成时,它会消失了,放置在上方的方块下降了一级.
  5. 完成线会授予积分, 并且累积一定数量的分数会使玩家上移一个级别.
  6. 碎片的速度会随着每个级别的增加而增加.
  7. 一次清除越多行积分会越高.
  8. 四个小方块组成的所有连接在一起的不同形状碎片, 共有七种.

游戏画面

Tetris 俄罗斯方块游戏

代码及说明

  1. 导入的库

    import numpy as np
    import PySimpleGUI as sg
    import random
  1. 方块的类建立
  • 方块图形: 七种方块以各小块相对坐标来定义.

    class Game():
    
        def __init__(self):
            self.pixel  = np.array(
            [[[[0,0], [0,1], [0,2], [0,3]], [[0,0], [1,0], [2,0], [3,0]],  # ____
              [[0,0], [0,1], [0,2], [0,3]], [[0,0], [1,0], [2,0], [3,0]]],
             [[[0,0], [0,1], [1,0], [1,1]], [[0,0], [0,1], [1,0], [1,1]],  # 田
              [[0,0], [0,1], [1,0], [1,1]], [[0,0], [0,1], [1,0], [1,1]]],
             [[[0,1], [1,1], [2,1], [2,0]], [[0,0], [1,0], [1,1], [1,2]],  # ▁▁│
              [[0,1], [0,0], [1,0], [2,0]], [[0,0], [0,1], [0,2], [1,2]]],
             [[[0,0], [0,1], [1,1], [2,1]], [[1,0], [0,0], [0,1], [0,2]],  # │▁▁
              [[0,0], [1,0], [2,0], [2,1]], [[1,0], [1,1], [1,2], [0,2]]],
             [[[0,1], [1,1], [2,1], [1,0]], [[0,0], [0,1], [0,2], [1,1]],  # ▁│▁
              [[0,0], [1,0], [2,0], [1,1]], [[1,0], [1,1], [1,2], [0,1]]],
             [[[0,0], [1,0], [1,1], [2,1]], [[1,0], [1,1], [0,1], [0,2]], # ▔│▁
              [[0,0], [1,0], [1,1], [2,1]], [[1,0], [1,1], [0,1], [0,2]]],
             [[[0,1], [1,1], [1,0], [2,0]], [[0,0], [0,1], [1,1], [1,2]],  # ▁│▔
              [[0,1], [1,1], [1,0], [2,0]], [[0,0], [0,1], [1,1], [1,2]]]])
  • 游戏旗标及参数

    包含格数的度及高度, 方块的种类数目, 游戏起始速度, 方块起始位置, 方块消去分数, 游戏状态(游戏结束, 暂停), 方块是否存在等等旗标及参数.

    其中字体大小主要用来定义方块的大小, 因为每个小方块都是文字框, 只是背景色不一样.

            self.width, self.height = 10, 20
            self.start_x, self.start_y = 4, 0
            self.kind,self.axis, self.timer = 7, 0, 100
    
            self.font, self.background = '微软正黑体 24', 'gray'
            self.pause, self.stop, self.no_blcok = False, True, True
    
            self.block = []
    
            self.lines, self.score, self.level, self.count = 0, 0, 0, 0
            self.plus = [0, 100, 200, 400, 800]
  • 游戏记录参数

    记录每个位置是否被占用及其颜色, 定义每种方块的频色, 每种方块出现的机率, 以及按键和按钮的对应函数.

            self.area = np.zeros((self.height, self.width))
            self.cont = np.full((self.height, self.width), 7)
            self.rate = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6]
            self.colors = ['gold', 'blue', 'tomato', 'red', 'green', 'purple',
                           'brown', self.background]
            self.func = {'New'      :self.new,   'Pause'  :self.wait,
                         'Left:37'  :self.left,  'Up:38'  :self.rotate,
                         'Right:39' :self.right, 'Down:40':self.down,
                         'Escape:27':self.wait,  ' '      :self.space}
  • 游戏类的方法及函数

    • 游戏起始的展示画面, 七种方块旋转动态展示.
        def blocks(self):
            self.count += 1
            if self.count == 50:
                self.count = 0
                old_axis = self.axis-1 if self.axis>0 else 3
                for kind in range(self.kind):
                    self.block = [kind, old_axis, (kind%2)*5+1, (kind//2)*5+1]
                    self.draw(show=False)
                    self.block = [kind, self.axis, (kind%2)*5+1, (kind//2)*5+1]
                    self.draw()
                self.axis = self.axis+1 if self.axis<3 else 0
    • 检查方块连线

      如果每一列的占用总数等于游戏画面的寛度, 代表一连线, 每一连线删除并下移上方所有的画面.

      按消去的列总数, 取得分数, 更新总分及游戏等级, 并调整方块自动下落的速度.

        def check(self):
            count = 0
            if not self.no_block: self.draw(show=False)
            for y in range(self.height-1, -1, -1):
                if np.sum(self.area[y]) == self.width:
                    count += 1
                    self.area[1:y+1], self.area[0] = self.area[0:y], 0
                    self.cont[1:y+1], self.cont[0] = self.cont[0:y], 7
                    for z in range(0, y+1):
                        for x in range(self.width):
                            window.find_element(str(x+10*z)).Update(
                                background_color=self.colors[self.cont[z, x]])
            if not self.no_block: self.draw()
            self.score += min(self.plus[count], 999999)
            self.lines += count
            self.level = int(self.lines//2)
            self.timer = int(100-self.level) if self.level<100 else 1
            window.find_element('Score').Update(value='{:0>6d}'.format(self.score))
            window.find_element('Level').Update(value='{:0>2d}'.format(self.level))
    • 清除画面

      更改所有格子的被占用记录为0, 设置所有格子的颜色为游戏背景色

        def clear(self):
            self.area = np.zeros((self.height, self.width))
            for i in range(self.width*self.height):
                window.find_element(str(i)).Update(
                    background_color=self.background)
    • 方块下降一格

      先取得下降一格的位置, 再认该位置是否没被占用或出界, 如果被占用或出界, 不可下移, 而且设置该方块不再存在, 并设置该方块所有的格子位置被占用.

        def down(self):
            kind, axis, x, y = self.block
            new_block = [kind, axis, x, y+1]
            if self.ok(new_block):
                self.block = new_block
                return True
            else:
                self.no_block = True
                data = self.get_font(self.block)+self.block[2:]
                xi, yi = data[:,0], data[:,1]
                self.area[yi, xi] = 1
                return False
    • 显示方块或不显示方块

      以改变文本框的方式来达成

        def draw(self, show=True):
            color = self.colors[self.block[0]] if show else self.background
            for x, y in self.get_font(self.block)+self.block[2:]:
                window.find_element(str(x+10*y)).Update(background_color=color)
                self.cont[y, x] = self.colors.index(color)
    • 取得方块的图形数据
        def get_font(self, block):
            return self.pixel[block[0], block[1]]
    • 左键左移处理

      先取得左移一格的位置, 再认该位置是否没被占用或出界, 如果被占用或出界, 不可左移

        def left(self):
            kind, axis, x, y = self.block
            new_block = [kind, axis, x-1 if x>0 else x, y]
            if self.ok(new_block):
                self.block = new_block
                return True
            return False
    • 建立新的方块

      新的方块如果被占用, 游戏结束

        def new_block(self):
            self.block = [self.random(), 0, int(self.width/2-1), 0]
            self.draw(self.block)
            if self.ok(self.block):
                self.no_block = False
                self.count = 0
            else:
                self.stop = True
                sg.popup("Game Over", no_titlebar=True, font=self.font)
    • 游戏开始
        def new(self):
            self.pause = False
            self.no_block = True
            self.stop = False
            self.clear()
    • 调整方块的位置计算函数
        def offset(self, x_limit, x_max, x_min):
            return x_max-x_limit+1 if x_max>x_limit-1 else x_min if x_min<0 else 0
    • 检查方块的位置是否出界或被占用
        def ok(self, block):
            if block[3] >= self.height:
                return False
            data = self.get_font(block)+block[2:]
            if not(np.max(data[:,0])<self.width  and np.min(data[:,0])>-1 and
                   np.max(data[:,1])<self.height and np.min(data[:,1])>-1):
                return False
            x, y = data[:,0], data[:,1]
            if np.sum(self.area[y, x]) != 0:
                return False
            return True
    • 按提供的样品空间, 随机选择方块的类别

      该空间类别出现的次数越多, 在游戏中出现的机率越高

        def random(self):
            return random.choice(self.rate)
    • 右键右移处理

      先取得右移一格的位置, 再认该位置是否没被占用或出界, 如果被占用或出界, 不可右移

        def right(self):
            kind, axis, x, y = self.block
            new_block = [kind, axis, x+1 if x<self.width-1 else x, y]
            if self.ok(new_block):
                self.block = new_block
                return True
            return False
    • 上键旋转处理

      旋转时, 必须考虑方块是否会出界或被占用, 所以必须以旋转的方块先调整位置, 再确认是否可移到该位置.

        def rotate(self):
            kind, axis, x, y = self.block
            new_block = [kind, (axis+1)%4, x, y]
            data=self.get_font(new_block)+[x, y]
            x_max, x_min = np.max(data[:,0]), np.min(data[:,0])
            y_max, y_min = np.max(data[:,1]), np.min(data[:,1])
            x_offset = self.offset(self.width, x_max, x_min)
            y_offset = self.offset(self.height, y_max, y_min)
            new_block[2:]= [x-x_offset, y-y_offset]
            if self.ok(new_block):
                self.block = new_block
                return True
            return False
    • 空格键快速下落

      连续一步一步的下移, 直到会出界或被占用.

        def space(self):
            while True:
                self.draw(show=False)
                stop = not self.down()
                self.draw()
                if stop:
                    break
    • 动作前后更新方块
        def update(self, func):
            self.draw(show=False)
            func()
            self.draw()
    • 暂停按钮或按键处理
        def wait(self):
            self.pause = not self.pause
  1. GUI 界面
  • 方块文字框

    def T(key, text=None, color='white', size=(None, None)):
      if text == None:
          text = ' '*5
      return sg.Text(text, key=key, pad=(1,1), size=size, justification='center',
          font=G.font, background_color=G.background, text_color=color)
  • 一般文字框

    def M(text, bg='green'):
      return sg.Text(text, font=G.font, size=(10, 1), justification='center',
          background_color=bg, text_color='white')
  • 按钮对象

    def B(text, key):
      return sg.Button(text, font=G.font, size=(10, 1), key=key,
          bind_return_key=False, focus=False)
  • 游戏类实例化

    G = Game()
  • 游戏区配置

    layout1 = [[T(str(i+j*G.width), color=G.background) for i in range(G.width)]
                for j in range(G.height)]
  • 讯息区配置(分数, 等级, 游戏说明, 按键)

    layout2 = [[M('分数')], [T('Score', text='000000', size=(10, 1))],
             [M('', bg=G.background)],
             [M('等级')], [T('Level', text='00', size=(10, 1))],
             [M('', bg=G.background)],
             [M('左键:左移')], [M('右键:右移')], [M('下键:下移')],
             [M('上键:旋转')], [M('空格键:落下')], [M('ESC键:暂停')],
             [M('', bg=G.background)], [M(' ', bg=G.background)],
             [B('游戏开始', 'New')], [B('游戏暂停', 'Pause')],
             [B('游戏结束', 'Over')]]
  • 整体游戏 GUI 配置

    frame1 = sg.Frame('', layout=layout1, background_color=G.background,
                    border_width=5)
    frame2 = sg.Frame('', layout=layout2, background_color=G.background,
                   border_width=5, size=(None, G.height*32))
    layout = [[frame1, frame2]]
    window = sg.Window('Tetris 俄罗斯方块', layout=layout, finalize=True,
                     use_default_focus=False, return_keyboard_events=True,
                     background_color=G.background)
  • 事件处理及游戏循环处理

    blocks = True
    while True:
    
      event, values = window.read(timeout=10)
    
      if event in [None, 'Over']:
          break
      elif event in ['New', 'Pause', 'Escape:27']:
          blocks = False
          G.func[event]()
          continue
      if blocks:
          G.blocks()
          continue
      if G.pause or G.stop:
          continue
      if G.no_block:
          G.new_block()
      else:
          if event in G.func:
              G.update(G.func[event])
          G.count += 1
          if G.count == G.timer:
              G.count = 0
              G.update(G.down)
      G.check()
    
    window.close()
本作品采用《CC 协议》,转载必须注明作者和本文链接
Jason Yang
讨论数量: 3

不用tkinter了吗 :joy:

4年前 评论
Jason990420

tkinter变化多端, 不好驾驭, 而且一点一滴都要自己来, 不像PySimpleGUI简单多了, 虽然会到很多约束.
另外, 在我的IDLE (Pyscripter) 下, tkinter一出错, 老是要重启. PySimpleGUI就比较不会有这样的问题.
GUI只是辅助, 简单好用就行, 除非要作商用软件.

4年前 评论

看起里写的好累呀~

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!