Falling Blocks in Python
Pygame Online · 225 lines · Left and Right, Up to turn, Down and Space to drop
Turn and drop the falling pieces to fill whole rows.
How it works
Turning a piece
Each piece is a few cells inside a small square box. A quarter turn maps every cell (x, y) to (size - 1 - y, x).
def rotate(cells, size): """A quarter turn clockwise inside the piece's box.""" return [(size - 1 - y, x) for x, y in cells]
Does it fit?
One function answers the question every move asks: is every cell inside the board and on an empty square? Moving, turning, the ghost piece and game over all use it.
def fits(cells, px, py): """True if the piece fits with its box at column px, row py.""" for x, y in cells: col, row = px + x, py + y if col < 0 or col >= COLS or row >= ROWS or board[row][col]: return False return True
Landing and clearing rows
A landed piece is copied into the board. Rows without an empty square are dropped and new empty rows go on top. More rows at once score more, times the level.
def land(): """Fix the piece into the board, clear full rows, then bring the next piece.""" global board, score, lines, level for x, y in cells: board[py + y][px + x] = PIECES[current][2] kept = [row for row in board if None in row] cleared = ROWS - len(kept) board = [[None] * COLS for _ in range(cleared)] + kept score += LINE_POINTS[cleared] * level lines += cleared level = lines // 10 + 1 spawn()
Make it yours
Change the code above and press Run again. Some ideas:
- Add a hold slot: press C to swap the current piece with a saved one.
- Show the next three pieces instead of one.
- Make levels come every 5 lines instead of 10.
- Add a new piece shape to
PIECESand see how it plays.
The whole program
Plain pygame: it runs the same on a computer with Python and pygame installed.
import random import pygame pygame.init() WIDTH, HEIGHT = 480, 360 CELL = 16 COLS, ROWS = 10, 20 BOARD_X, BOARD_Y = 160, 20 # where the board's top-left corner is drawn screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Falling Blocks") clock = pygame.time.Clock() font = pygame.font.Font(None, 26) number_font = pygame.font.Font(None, 38) big_font = pygame.font.Font(None, 52) BG = (16, 18, 22) WELL = (24, 27, 33) YELLOW = (255, 212, 59) BLUE = (48, 105, 152) TEXT = (230, 232, 235) DIM = (140, 146, 156) # Each piece: its cells inside a square box, the size of that box, its colour. PIECES = { "I": ([(0, 1), (1, 1), (2, 1), (3, 1)], 4, (90, 200, 230)), "O": ([(0, 0), (1, 0), (0, 1), (1, 1)], 2, (255, 212, 59)), "T": ([(1, 0), (0, 1), (1, 1), (2, 1)], 3, (170, 120, 220)), "S": ([(1, 0), (2, 0), (0, 1), (1, 1)], 3, (110, 200, 120)), "Z": ([(0, 0), (1, 0), (1, 1), (2, 1)], 3, (235, 95, 85)), "J": ([(0, 0), (0, 1), (1, 1), (2, 1)], 3, (70, 130, 210)), "L": ([(2, 0), (0, 1), (1, 1), (2, 1)], 3, (240, 150, 60)), } LINE_POINTS = [0, 100, 300, 500, 800] # for clearing 0, 1, 2, 3 or 4 rows at once START_KEYS = (pygame.K_UP, pygame.K_SPACE, pygame.K_RETURN) def rotate(cells, size): """A quarter turn clockwise inside the piece's box.""" return [(size - 1 - y, x) for x, y in cells] def fits(cells, px, py): """True if the piece fits with its box at column px, row py.""" for x, y in cells: col, row = px + x, py + y if col < 0 or col >= COLS or row >= ROWS or board[row][col]: return False return True def take_from_bag(): """Pieces come from a shuffled bag of all seven, so none is missing for long.""" if not bag: bag.extend(PIECES) random.shuffle(bag) return bag.pop() def spawn(): """The next piece appears at the top. If there is no room, the game is over.""" global current, cells, size, px, py, next_name, fall_timer, state, best, wait current = next_name next_name = take_from_bag() cells = PIECES[current][0] size = PIECES[current][1] px, py = (COLS - size) // 2, 0 fall_timer = 0 if not fits(cells, px, py): state = "over" best = max(best, score) wait = 30 def new_game(): global board, bag, next_name, score, lines, level board = [[None] * COLS for _ in range(ROWS)] # None or the colour of a block bag = [] next_name = take_from_bag() score, lines, level = 0, 0, 1 spawn() def move(dx, dy): """Move the piece if there is room; True if it moved.""" global px, py if fits(cells, px + dx, py + dy): px += dx py += dy return True return False def turn(): """Rotate, nudging sideways if the turned piece would hit a wall or a block.""" global cells, px turned = rotate(cells, size) for nudge in (0, -1, 1, -2, 2): if fits(turned, px + nudge, py): cells = turned px += nudge return def land(): """Fix the piece into the board, clear full rows, then bring the next piece.""" global board, score, lines, level for x, y in cells: board[py + y][px + x] = PIECES[current][2] kept = [row for row in board if None in row] cleared = ROWS - len(kept) board = [[None] * COLS for _ in range(cleared)] + kept score += LINE_POINTS[cleared] * level lines += cleared level = lines // 10 + 1 spawn() def draw_cell(x, y, color): pygame.draw.rect(screen, color, (x + 1, y + 1, CELL - 2, CELL - 2), border_radius=3) def draw_text(text, text_font, color, center): image = text_font.render(text, True, color) screen.blit(image, image.get_rect(center=center)) new_game() state = "ready" # "ready", "playing" or "over" best = 0 hold_frames = 0 # how long Left or Right has been held wait = 0 # frames until a new game may start running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if state == "playing": if event.key == pygame.K_LEFT: move(-1, 0) elif event.key == pygame.K_RIGHT: move(1, 0) elif event.key == pygame.K_UP: turn() elif event.key == pygame.K_DOWN: if move(0, 1): score += 1 fall_timer = 0 elif event.key == pygame.K_SPACE: # hard drop while move(0, 1): score += 2 land() elif event.key in START_KEYS and wait == 0: if state == "over": new_game() state = "playing" if wait > 0: wait -= 1 if state == "playing": keys = pygame.key.get_pressed() # Holding Left or Right keeps moving, after a short pause. if keys[pygame.K_LEFT] or keys[pygame.K_RIGHT]: hold_frames += 1 if hold_frames > 12 and hold_frames % 4 == 0: move(-1 if keys[pygame.K_LEFT] else 1, 0) else: hold_frames = 0 # The piece falls one row every few frames: fewer at higher levels. delay = max(4, 50 - level * 5) if keys[pygame.K_DOWN]: delay = 3 # soft drop fall_timer += 1 if fall_timer >= delay: fall_timer = 0 if move(0, 1): if keys[pygame.K_DOWN]: score += 1 else: land() screen.fill(BG) pygame.draw.rect(screen, WELL, (BOARD_X, BOARD_Y, COLS * CELL, ROWS * CELL)) for row in range(ROWS): for col in range(COLS): if board[row][col]: draw_cell(BOARD_X + col * CELL, BOARD_Y + row * CELL, board[row][col]) if state == "playing": # The ghost shows where the piece would land. ghost_y = py while fits(cells, px, ghost_y + 1): ghost_y += 1 for x, y in cells: rect = (BOARD_X + (px + x) * CELL + 1, BOARD_Y + (ghost_y + y) * CELL + 1, CELL - 2, CELL - 2) pygame.draw.rect(screen, DIM, rect, 1, border_radius=3) for x, y in cells: draw_cell(BOARD_X + (px + x) * CELL, BOARD_Y + (py + y) * CELL, PIECES[current][2]) pygame.draw.rect(screen, BLUE, (BOARD_X - 3, BOARD_Y - 3, COLS * CELL + 6, ROWS * CELL + 6), 2, border_radius=4) # Left: score, lines, level. Right: the next piece and the keys. for i, (label, value) in enumerate([("Score", score), ("Lines", lines), ("Level", level), ("Best", best)]): draw_text(label, font, DIM, (80, 40 + i * 70)) draw_text(str(value), number_font, YELLOW if label == "Score" else TEXT, (80, 68 + i * 70)) draw_text("Next", font, DIM, (400, 40)) next_cells, next_size, next_color = PIECES[next_name] for x, y in next_cells: draw_cell(400 - next_size * CELL // 2 + x * CELL, 60 + y * CELL, next_color) for i, line in enumerate(["Up: turn", "Left, Right: move", "Down: drop faster", "Space: drop"]): draw_text(line, font, DIM, (400, 200 + i * 26)) if state != "playing": panel = pygame.Rect(0, 0, 300, 120) panel.center = (BOARD_X + COLS * CELL // 2, HEIGHT // 2) pygame.draw.rect(screen, BG, panel, border_radius=12) pygame.draw.rect(screen, BLUE, panel, 2, border_radius=12) draw_text("Blocks" if state == "ready" else "Game over", big_font, YELLOW, (panel.centerx, panel.y + 40)) hint = "Press Up or Space to start" if state == "ready" else "Press Up or Space to play again" draw_text(hint, font, TEXT, (panel.centerx, panel.y + 88)) pygame.display.flip() clock.tick(60) pygame.quit()