用python做一个俄罗斯方块游戏(1)—— 形状和面板

我一直认为,编写游戏,是python初学者寻求进阶的一个有效途径。俄罗斯方块是一款很老的游戏,但对于python初学者而言还是有相当的难度。

目前看到网上的大部分实现都是面向过程的,虽然能够实现功能,但是整个程序的可扩展性和可维护性都很差,而且写这样的程序,对我们这些初级程序员的提高是有限的。

我们期望能够构建出结构清晰,扩展性强的,容易维护的代码,而不是所有逻辑和细节都糅合在一起的超级大杂烩。

最终效果

俄罗斯方块

初步设计

游戏面板(Board):15 * 25 的方格,操作在游戏面板中进行;

形状(Shape):一共有7种形状:L、J、T、Z、S、I、O

形态(Style):每种形状都可以进行旋转,旋转后呈现不同的形态(或者方向),例如L形状,就有4种形态。

方格(Cell):游戏面板和形状都是由方格组成的,是最小单位。在初步设想中,方格有两个状态:填充和空白,看上去我们可以用简单的0和1来表示方格,即1表示填充,0表示空白;不过稍微思考下,方格的状态其实有3种:1.活动方格;2.非活动方格(着陆方格);3.空白方格。

因此我们这样来表示3种状态(这里用了二进制来表示显得规整一点,其实就是0,1,2三个值):

# CELL STATE
BLANK  = 0b00
ACTIVE = 0b01
LANDED = 0b10

每一个Style形态用方格的BLANK和ACTIVE状态表示组成,因此简单的二维数组就能表示。比如L形状的四种形态,可以表示成:

s1 = [[1, 1, 1], 
      [1, 0, 0]]

s2 = [[1, 1], 
      [0, 1], 
      [0, 1]]
 
s3 = [[0, 0, 1], 
      [1, 1, 1]]

s4 = [[1, 0], 
      [1, 0], 
      [1, 1]]

这样形状L以及4种形态就可以表示为:

L = [s1, s2, s3, s4]

依此类推,所有的7种形状和各自的不同形态可以表示成如下形式的常量:

# SHAPES
L = [[(1, 1, 1), (1, 0, 0)], [(1, 1), (0, 1), (0, 1)], [(0, 0, 1), (1, 1, 1)], [(1, 0), (1, 0), (1, 1)]]
J = [[(1, 1, 1), (0, 0, 1)], [(0, 1), (0, 1), (1, 1)], [(1, 0, 0), (1, 1, 1)], [(1, 1), (1, 0), (1, 0)]]
T = [[(0, 1, 0), (1, 1, 1)], [(1, 0), (1, 1), (1, 0)], [(1, 1, 1), (0, 1, 0)], [(0, 1), (1, 1), (0, 1)]]
Z = [[(1, 1, 0), (0, 1, 1)], [(0, 1), (1, 1), (1, 0)]]
S = [[(0, 1, 1), (1, 1, 0)], [(1, 0), (1, 1), (0, 1)]]
I = [[(1,), (1,), (1,), (1,)], [(1, 1, 1, 1)]]
O = [[(1, 1), (1, 1)]]

下面来看游戏面板(Board)。游戏面板是方格的集合,同样用二维数组来表示游戏面板,举个例子,如下是一个5 * 5 的空白游戏面板:

board = [[0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0]]

面板中的任何一个方格,可以用坐标(x, y)表示,获取方式:

cell = board[y][x]

shape如何“画”到board上?因为shape的方格都是活动方格,所以L形状展示在面板上的方式如下:

board = [[0, 0, 0, 0, 0],
         [0, 1, 1, 1, 0],
         [0, 1, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0]]

而shape着陆以后,shape的方格状态变为LANDED,board就会变成如下形式:

board = [[0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0],
         [0, 2, 2, 2, 0],
         [0, 2, 0, 0, 0]]

我们注意到,游戏面板虽然可以用二维数组表示出来,但是应该还需要定义一些通用的行为,比如获取或者设置某个坐标cell状态、如何把整个shape“画”到面板上方法,还有清除行、判断是否到达顶部等等可能的行为,因此我们建立一个Board类:

class Board:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.current_shape = None
        self.init_cells()

    def __str__(self):
        return str(self.cells)

    def init_cells(self):
        self.cells = []
        for _ in range(self.height):
            self.cells.append([BLANK] * self.width)

    def get_cell(self, x, y):
        return self.cells[y][x]

    def set_cell(self, x, y, state=ACTIVE):
        self.cells[y][x] = state

    def _blit_shape(self, state):
        for offset_y, row in enumerate(self.current_shape.style):
            for offset_x, value in enumerate(row):
                if value:
                    self.set_cell(self.current_shape.x + offset_x, 
                        self.current_shape.y + offset_y, state)

    @property
    def is_over_top(self):
        for cell in self.cells[0]:
            if cell:
                return True
        return False

    def find_full_rows(self):
        index = []
        for y in range(self.height -1, -1, -1):
            if sum(self.cells[y]) == LANDED * self.width:
                index.append(y)
        return index

    def clear_full_rows(self):
        index = self.find_full_rows()
        if (l := len(index)) > 0:
            for y in index:
                self.cells.pop(y)
            for _ in range(l):
                self.cells.insert(0, [BLANK] * self.width)

    def update(self):
        for y in range(self.height):
            for x in range(self.width):
                if self.cells[y][x] == ACTIVE:
                    self.cells[y][x] = 0
        if self.current_shape:
            self._blit_shape(ACTIVE)

说明:

  1. 游戏会以形状到顶而结束,因此这里定义了一个is_over_top的属性来判断是否到顶;
  2. 因为游戏会消除满行,所以find_full_rows用来找到满行,返回满行的索引,这里的索引必须是降序排列的;clear_full_rows用来清除满行,因为满行索引是降序,所以pop的顺序也是从后往前,这样索引就不会乱;
  3. 这里添加了一个属性,current_shape,表示当前面板中活动的shape(网格值为1);
  4. _blit_shape方法用来将当前的shape“画”到board上,若state属性为LANDED,则表示该shape已经着陆;
  5. 最后的update方法,是用来刷新面板的:“动的部分”是面板上活动的shape,所以每次刷新都把活动的shape清除(即把状态为ACTIVE的方格设置成BLANK),清除之后再画出来——因为此时shape会有新的位置或者新的style。

接下来构建Shape类:

class Shape:
    def __init__(self, style):
        self.styles = styles[style]
        self.styles_len = len(self.styles)
        self.i = 0
        self.board = None
        self.x = 0
        self.y = 0

    def __repr__(self):
        return str(self.style)

    @staticmethod
    def random_shape():
        return Shape(random.choice('LJTZSIO'))

    @property
    def i(self):
        return self._i
    @i.setter
    def i(self, value):
        self._i = value
        self.style = self.styles[value]
        self.length = len(self.style[0])
        self.height = len(self.style)

    @property
    def next_style(self):
        if self.i + 1 >= self.styles_len:
            return self.styles[0]
        return self.styles[self.i + 1]

    @property
    def margin_left(self):
        return self.x

    @property
    def margin_right(self):
        return self.board.width - self.x - 1

    @property
    def margin_bottom(self):
        return self.board.height - self.y - 1
    
    def move_left(self, value=1):
        self.x -= value

    def move_right(self, value=1):
        self.x += value

    def move_down(self, value=1):
        self.y += value

    def land(self):
        self.board._blit_shape(LANDED)
        self.board.current_shape = None

    def rotate(self):
        if (self.i + 1) >= self.styles_len:
            self.i = 0
        else:
            self.i += 1

说明:

  1. 我们没必要给style专门构建一个类,而是把style当作一个属性;styles属性则代表了该shape的所有形状,与之相关的i属性表示当前style在styles列表中的索引,一旦设置i的值,则当前的style跟着变化;next_style表示下一次翻转的形状;
  2. board属性代表了当前shape所属的borad,x与y表示位置坐标;
  3. 几个margin属性表示了当前shape距离面板边缘的距离;
  4. 几个move方法则表示形状的左、右、下的移动;rotate方法表示翻转;land方法表示着陆
  5. 静态方法random_shape会创建一个随机的shape实例

两个最基本的类已经构建完成,接下来就是实现游戏逻辑了。我们下一章见~

猜你喜欢

转载自blog.csdn.net/weixin_49775731/article/details/126601191