Playing with AI - More Tetris

Improvements to the game

Introduction

It's been some time since I had a chance to work on this project but I'm glad to be back. Since the last blog, I've cleaned up the code and have more defined objects in the game, particularly blocks defined for each Tetromino and how they move. I'll go over the changes I've made so far and get into what my next steps shall be.

Progress

Here's what the current game looks like. It currently will generate a random tetromino which can move left and right. Once the tetromino hits the bottom of the screen, it will despawn and generate a new tetromino at the top of the screen. Note I've also added debug text at the top left so I can get the current X and Y coordinates for the block.

Here are the movement boundaries I've added to the Tetromino class to facilitate the movement:

def is_at_bottom(self, grid):
    for row in range(len(self.shape)):
        for col in range(len(self.shape[row])):
            if self.shape[row][col]:
                if self.y + row >= grid.height:
                    return True
    return False

def can_go_left(self):
    if self.x > 0:
        return True
    return False

def can_go_right(self, grid):
    for row in range(len(self.shape)):
        for col in range(len(self.shape[row])):
            if self.shape[row][col]:
                if self.x + col + 1 >= grid.width:
                    return False
    return True

These functions prevent the block from going out of bounds and the is_at_bottom() function is used to check when to respawn the Tetromino. They will need to change once there are things the Tetromino can interact with (i.e. other blocks), but for now works fine for debugging.

This function is used to generate the blocks from the TetrisGame class:

def generate_tetromino(self):
    block_class = random.choice(block.Tetromino.__subclasses__())
    return block_class(self.board)

And you can see each definition of the Tetromino's below:

class TTetromino(Tetromino):
    def __init__(self, board):
        shape = [[0, 1, 0], [1, 1, 1]]
        color = Colours.PURPLE.value
        x, y = self.get_spawn_point(board)
        super().__init__(shape, color, x, y)


class ITetromino(Tetromino):
    def __init__(self, board):
        shape = [[1, 1, 1, 1]]
        color = Colours.LIGHT_BLUE.value
        x, y = self.get_spawn_point(board)
        super().__init__(shape, color, x, y)


class JTetromino(Tetromino):
    def __init__(self, board):
        shape = [[1, 0, 0], [1, 1, 1]]
        color = Colours.DARK_BLUE.value
        x, y = self.get_spawn_point(board)
        super().__init__(shape, color, x, y)


class LTetromino(Tetromino):
    def __init__(self, board):
        shape = [[0, 0, 1], [1, 1, 1]]
        color = Colours.ORANGE.value
        x, y = self.get_spawn_point(board)
        super().__init__(shape, color, x, y)


class OTetromino(Tetromino):
    def __init__(self, board):
        shape = [[1, 1], [1, 1]]
        color = Colours.YELLOW.value
        x, y = self.get_spawn_point(board)
        super().__init__(shape, color, x, y)

    @staticmethod
    def get_spawn_point(board):
        midpoint = board.width // 2
        return midpoint - 1, 0


class STetromino(Tetromino):
    def __init__(self, board):
        shape = [[0, 1, 1], [1, 1, 0]]
        color = Colours.GREEN.value
        x, y = self.get_spawn_point(board)
        super().__init__(shape, color, x, y)


class ZTetromino(Tetromino):
    def __init__(self, board):
        shape = [[1, 1, 0], [0, 1, 1]]
        color = Colours.RED.value
        x, y = self.get_spawn_point(board)
        super().__init__(shape, color, x, y)

Finally, this simple bit of code sets the spawn point for each of the blocks which I originally thought I'd need to override for each subclass but it turns out it's the same for all of them:

@staticmethod
def get_spawn_point(board):
    midpoint = board.width // 2
    return midpoint - 2, 0

Next Steps

My first goal is to have the correct movement of the blocks before trying to add any collision detection. The issue with this is how complicated rotation turned out to be. I had it working for a while based on a simple rotation algorithm and then checks for being out of bounds but it didn't look like the actual game. Upon doing further research I found what is known as the Super Rotation System that the game uses and the Wiki pages for this are thousands of words long (see https://tetris.fandom.com/wiki/SRS and https://tetris.wiki/Super_Rotation_System). I may need to implement collisions first before getting this to work in order for "wall kicks" to make sense in the game. We will have to wait and see.

The next stage is implementing the stacking of blocks at the bottom of the screen. Currently, I just have the blocks respawning once they reach the bottom but I would like to implement a system where they stay there visually. I think it makes sense that the Tetromino object isn't kept in memory and it is instead transformed to something else but I haven't figured that out yet.

Conclusion

It's good to be back as I mentioned in the introduction. I had actually written most of this code a few weeks ago but hadn't documented any of it (you will be able to see this in my Git history). It's always tough coming back to a project and trying to remember what needs to be done next so it was helpful to write this out and see what I've done and what my next steps will be. It is daunting looking at SRS again but I think it will be a fun challenge to tackle (although I imagine I will need to source some examples from other projects to get me to the finish line).