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
Jason990420

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

5年前 评论

看起里写的好累呀~

5年前 评论