5. Falling blocks

For V2 microbit
Move left and right with A and B buttons
Rotate with A and B together
from microbit import *
from random import choice
import music


class TetrisGrid:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.grid = self.create_grid(self.width, self.height)

    @staticmethod
    def create_grid(width, height):
        # x -1 to width -1; y -2 to width -2;
        grid = {(x, y): 0 for y in range(-2, height - 2) for x in range(-1, width - 1)}
        # put 1 in sides
        gridedge = {(x, y): 1 for y in range(-2, height - 2) for x in (-1, width - 2)}
        grid.update(gridedge)
        # put 1 in bottom
        gridedge = {(x, height - 3): 1 for x in range(-1, width - 1)}
        grid.update(gridedge)
        return grid


class TetrisBlock:
    bricks = [[9, 9, 9, 0], [9, 9, 0, 9], [9, 9, 9, 9], [9, 9, 0, 0]]

    def __init__(self, x=2, y=-2):
        # x is start pos of top left; 1 to 4 for width = 7
        self.x = x
        self.y = y

    def create_block(self):
        # top left of block at x, y; block row 1 then row 2 left to right
        brick = choice(self.bricks)
        block = {}
        index = 0
        for y0 in range(2):
            for x0 in range(2):
                block[(x0 + self.x, y0 + self.y)] = brick[index]
                index += 1
        return block


class TetrisBoard:
    def __init__(self, width=7, height=8, block_score=1, line_score=5):
        self.do_start_game_message()
        self.width = width
        self.height = height
        self.block_score = block_score
        self.line_score = line_score
        tetris_grid = TetrisGrid(width, height)
        self.grid = tetris_grid.grid
        self.new_block()
        self.run_game()

    def do_start_game_message(self):
        display.scroll("go", delay=80)

    def new_block(self):
        self.block_top_x = 2
        self.block_left_y = -2
        tetris_block = TetrisBlock(self.block_top_x, self.block_left_y)
        self.block = tetris_block.create_block()
        self.newblock = {}
        self.show_block()

    def get_block_top_left(self):
        x_vals = {x for x, y in self.block.keys()}
        y_vals = {y for x, y in self.block.keys()}
        self.block_top_x = min(x_vals)
        self.block_left_y = min(y_vals)

    def show_block(self):
        # can't just display all 4 parts,
        # need to take into account previous blocks on grid
        for (x, y) in self.block.keys():
            if -1 < x < 5 and -1 < y < 5:
                display.set_pixel(x, y, max(self.block[(x, y)], self.grid[(x, y)]))

    def hide_block(self):
        # keep previous blocks on grid
        for (x, y) in self.block.keys():
            if -1 < x < 5 and -1 < y < 5:
                display.set_pixel(x, y, self.grid[(x, y)])

    # A function to rotate the 2x2 brick
    def get_rotate_block(self):
        # clockwise order
        k00 = self.block[(self.block_top_x, self.block_left_y)]
        k01 = self.block[(self.block_top_x + 1, self.block_left_y)]
        k11 = self.block[(self.block_top_x + 1, self.block_left_y + 1)]
        k10 = self.block[(self.block_top_x, self.block_left_y + 1)]
        self.newblock = {}
        self.newblock[(self.block_top_x, self.block_left_y)] = k10
        self.newblock[(self.block_top_x + 1, self.block_left_y)] = k00
        self.newblock[(self.block_top_x + 1, self.block_left_y + 1)] = k01
        self.newblock[(self.block_top_x, self.block_left_y + 1)] = k11

    def rotate_block(self):
        self.get_rotate_block()
        if self.can_move_block():
            self.do_block_move()

    def get_move_block(self, delta_x, delta_y):
        self.newblock = {}
        for (x, y) in self.block.keys():
            self.newblock[(x + delta_x, y + delta_y)] = self.block[(x, y)]

    def move_block(self, delta_x=0, delta_y=0):
        self.get_move_block(delta_x, delta_y)
        if self.can_move_block():
            self.do_block_move()

    def do_block_move(self):
        self.hide_block()
        self.block = {k: v for k, v in self.newblock.items()}
        self.show_block()
        self.get_block_top_left()

    def can_move_block(self):
        for k, v in self.newblock.items():
            if self.newblock[k] > 0 and self.grid[k] > 0:
                return False
        return True

    def drop_block(self, delta_y=1):
        self.get_move_block(0, delta_y)
        can_move_block = self.can_move_block()
        if can_move_block:
            self.do_block_move()
        else:
            self.add_block_to_grid()
            self.score += self.block_score
            if self.is_game_over():
                self.do_game_over()
            else:
                music.play(["C4:1"])
                self.clear_lines_of_grid()
                self.new_block()

    def add_block_to_grid(self):
        # can only each if value is 9
        dict_to_include = {k: v for k, v in self.block.items() if v == 9}
        self.grid.update(dict_to_include)

    def is_game_over(self):
        if self.block_left_y < 0:
            self.gameOn = False
            return True
        else:
            return False

    def do_game_over(self):
        # End of Game
        sleep(1000)
        display.scroll("Game Over: Score: " + str(self.score), delay=80)

    def clear_lines_of_grid(self):
        # check each line one at a time from y=0 to y = 4
        for y in range(0, 5):
            removeLine = True
            for x in range(0, 5):
                if self.grid[(x, y)] != 9:
                    removeLine = False
            if removeLine:
                music.play(["E5:2"])
                self.score += self.line_score
                # Remove the line and make all lines above fall by 1:
                for y2 in range(y, -1, -1):
                    for x in range(0, 5):
                        self.grid[(x, y2)] = self.grid[(x, y2 - 1)]
        # Refresh the LED screen
        for x in range(0, 5):
            for y in range(0, 5):
                display.set_pixel(x, y, self.grid[(x, y)])

    def run_game(self):
        self.gameOn = True
        self.score = 0
        frame_time = 100  # 200
        frameCount = 0
        while self.gameOn:
            sleep(frame_time)
            frameCount += 1
            if button_a.is_pressed() and button_b.is_pressed():
                self.rotate_block()
            elif button_a.is_pressed():
                self.move_block(-1)
            elif button_b.is_pressed():
                self.move_block(1)
            # Every 10 frames try to move the brick down
            if frameCount == 10:
                frameCount = 0
                self.drop_block(1)


while True:
    game = TetrisBoard(block_score=1, line_score=5)
    sleep(2000)
    if button_a.is_pressed() or button_b.is_pressed():
        continue
    else:
        break

Tasks

  1. Modify the code to set the frame_time parameter when the class is initialized.

  2. Use A and B button pressing to play 2 different versions of the game, using different frame_time parameters using code from in task 1. Have A play a slow game, and B a faster game.