Python Forum
Tetris samples for teaching Python
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Tetris samples for teaching Python
#1
I created two tetris implementations to teach Python to my sons.
Any suggestions for simplifying the logic or improving the readability are welcome.
(Also for changing symbol names, because English is not my first language)

1. Non-OOP style
import random
import turtle

W, H = 10, 20; S = 15

def board_to_canvas(x, y):
    return ((x - W//2) * S, (y - H//2) * S)

def draw_text(text, x, y, color):
    turtle.color(color)
    turtle.goto(*board_to_canvas(x, y))
    turtle.write(text)

def draw_rect(x, y, w, h, color):
    turtle.color(color)
    x1, y1 = board_to_canvas(x, y)
    x2, y2 = x1 + w*S - 1, y1 + h*S - 1,
    turtle.goto(x1, y1)
    turtle.down()
    turtle.begin_fill()
    turtle.goto(x2, y1)
    turtle.goto(x2, y2)
    turtle.goto(x1, y2)
    turtle.goto(x1, y1)
    turtle.up()
    turtle.end_fill()


board = [[None] * W for _ in range(H)]

def draw_board():
    draw_rect(-1, -1, 1, H+1, 'black')
    draw_rect(W, -1, 1, H+1, 'black')
    draw_rect(0, -1, W, 1, 'black')
    for i, row in enumerate(board):
        for j, color in enumerate(row):
            if color:
                draw_rect(j, i, 1, 1, color)

def find_completed_rows():
    for row in board:
        if all(row):
            yield row


BLOCKS = [[] for _ in range(7)]
for i, line in enumerate('''
     oo   o      o   oo  oo    o
oooo oo   ooo  ooo  oo    oo  ooo'''.split('\n')):
    for j, char in enumerate(line):
        if char == 'o':
            BLOCKS[j // 5].append((j%5 - 1, -i + 2))

def draw_block(block, x0, y0):
    for dx, dy in block:
        x, y = x0 + dx, y0 + dy
        if y < H:
            draw_rect(x, y, 1, 1, 'blue')

def can_place_block_clipped(block, x0, y0):
    for dx, dy in block:
        x, y = x0 + dx, y0 + dy
        if not (0 <= x < W and 0 <= y) or (y < H and board[y][x]):
            return False
    return True

def place_block(block, x0, y0):
    complete = True
    for dx, dy in block:
        x, y = x0 + dx, y0 + dy
        if 0 <= x < W and 0 <= y < H:
            board[y][x] = 'blue'
        else:
            complete = False
    return complete


score = 0
t = 0
falling_period = 0
state = 'normal'
block = next_block = None
x, y = 0, 0
falling_generator = None
deleting_rows_generator = None

def move_block(dx, dy):
    global x, y
    new_x, new_y = x + dx, y + dy
    possible = can_place_block_clipped(block, new_x, new_y)
    if possible:
        x, y = new_x, new_y
    return possible

def rotate_block():
    global block
    new_block = [(-y, x) for x, y in block]
    possible = can_place_block_clipped(new_block, x, y)
    if possible:
        block = new_block
    return possible

def reset_block():
    global falling_period, block, next_block, x, y, state
    falling_period = [10, 9, 8, 7, 6, 5, 4][min(t // 100, 6)]
    new_block = None
    while not new_block:
        new_block, next_block = next_block, BLOCKS[random.randrange(7)]
    x, y = W // 2, H - 1
    if not can_place_block_clipped(new_block, x, y):
        state = 'game_over'
        return
    block = new_block
    begin_falling(falling_period)

def begin_falling(period):
    global falling_generator
    def fall():
        global falling_generator
        while True:
            for t in range(period):
                yield
            if not move_block(0, -1):
                break
        falling_generator = None
        place_block_and_begin_deleting()
        yield
    falling_generator = fall()

def place_block_and_begin_deleting():
    global block, state
    old_block = block
    block = None
    if not place_block(old_block, x, y):
        state = 'game_over'
        return
    begin_deleting_rows()

def begin_deleting_rows():
    global deleting_rows_generator
    if any(find_completed_rows()):
        duration = falling_period
        def delete():
            global deleting_rows_generator, score
            for t in range(duration):
                if t in (0, duration // 2):
                    color = ('red', 'blue')[t // (duration//2)]
                    for row in find_completed_rows():
                        row[:] = [color] * W
                yield
            incomplete_rows = [row for row in board if not all(row)]
            n_deleted = H - len(incomplete_rows)
            board[:] = incomplete_rows + [[None] * W for _ in range(n_deleted)]
            score += (n_deleted**2) * 100
            reset_block()
            deleting_rows_generator = None
            yield
        deleting_rows_generator = delete()
    else:
        reset_block()

def update_stage():
    global t
    if block and falling_generator:
        next(falling_generator)
    if deleting_rows_generator:
        next(deleting_rows_generator)
    t += 1

def render_stage():
    turtle.clear()
    draw_board()
    draw_text(f'SCORE: {score}', W + 3, H - 2, 'black')
    draw_text('NEXT BLOCK', W + 3, H - 6, 'black')
    draw_block(next_block, W + 4, H - 8)
    if block:
        draw_block(block, x, y)
    if state == 'game_over':
        draw_text('GAME OVER', W//2 - 2, -3, 'red')

def on_key_left():
    if block:
        move_block(-1, 0)
def on_key_right():
    if block:
        move_block(1, 0)
def on_key_down():
    if block:
        if not move_block(0, -1):
            place_block_and_begin_deleting()
def on_key_space():
    if block:
        begin_falling(period=0)
def on_key_up():
    if block:
        rotate_block()


turtle.tracer(0, 0)
turtle.hideturtle()
turtle.up()
turtle.listen()
turtle.onkey(turtle.bye, 'Escape')
turtle.onkey(on_key_left, 'Left')
turtle.onkey(on_key_right, 'Right')
turtle.onkey(on_key_down, 'Down')
turtle.onkey(on_key_space, 'space')
turtle.onkey(on_key_up, 'Up')
reset_block()
def render():
    turtle.ontimer(render, 100)
    update_stage()
    # This is required because turtle.write() always triggers ScrolledCanvas.update().
    old_canvas_update = turtle.ScrolledCanvas.update
    turtle.ScrolledCanvas.update = lambda self: None
    render_stage()
    turtle.ScrolledCanvas.update = old_canvas_update
    turtle.update()
render()
turtle.mainloop()
2. OOP style
import random
import turtle

W, H = 10, 20; S = 15

def board_to_canvas(x, y):
    return ((x - W//2) * S, (y - H//2) * S)

def draw_text(text, x, y, color):
    turtle.color(color)
    turtle.goto(*board_to_canvas(x, y))
    turtle.write(text)

def draw_rect(x, y, w, h, color):
    turtle.color(color)
    x1, y1 = board_to_canvas(x, y)
    x2, y2 = x1 + w*S - 1, y1 + h*S - 1,
    turtle.goto(x1, y1)
    turtle.down()
    turtle.begin_fill()
    turtle.goto(x2, y1)
    turtle.goto(x2, y2)
    turtle.goto(x1, y2)
    turtle.goto(x1, y1)
    turtle.up()
    turtle.end_fill()

class Board:
    def __init__(self):
        self.rows = [[None] * W for _ in range(H)]
        self.deleting_generator = None

    def draw(self):
        draw_rect(-1, -1, 1, H+1, 'black')
        draw_rect(W, -1, 1, H+1, 'black')
        draw_rect(0, -1, W, 1, 'black')
        for i, row in enumerate(self.rows):
            for j, color in enumerate(row):
                if color:
                    draw_rect(j, i, 1, 1, color)

    def get_color(self, x, y):
        return self.rows[y][x]

    def set_color(self, x, y, color):
        self.rows[y][x] = color

    def find_completed_rows(self):
        for row in self.rows:
            if all(row):
                yield row

    def begin_deleting(self, duration, on_end):
        if any(self.find_completed_rows()):
            completed_rows = list(self.find_completed_rows())
            def delete():
                for t in range(duration):
                    if t in (0, duration // 2):
                        color = ('red', 'blue')[t // (duration//2)]
                        for row in completed_rows:
                            row[:] = [color] * W
                    yield
                incomplete_rows = [row for row in self.rows if not all(row)]
                n_deleted = H - len(incomplete_rows)
                self.rows[:] = incomplete_rows + [[None] * W for _ in range(n_deleted)]
                on_end(n_deleted)
                self.deleting_generator = None
                yield
            self.deleting_generator = delete()
        else:
            on_end(0)

    def update(self):
        if self.deleting_generator:
            next(self.deleting_generator)


BLOCK_SHAPES = [[] for _ in range(7)]
for i, line in enumerate('''
     oo   o      o   oo  oo    o
oooo oo   ooo  ooo  oo    oo  ooo'''.split('\n')):
    for j, char in enumerate(line):
        if char == 'o':
            BLOCK_SHAPES[j // 5].append((j%5 - 1, -i + 2))

class Block:
    def __init__(self, board):
        self.board = board
        self.shape = BLOCK_SHAPES[random.randrange(7)][:]
        self.x, self.y = 0, 0
        self.falling_generator = None

    @property
    def points(self):
        for dx, dy in self.shape:
            yield (self.x + dx, self.y + dy)

    def draw(self):
        for x, y in self.points:
            if y < H:
                draw_rect(x, y, 1, 1, 'blue')

    def can_place_clipped(self):
        for x, y in self.points:
            if not (0 <= x < W and 0 <= y) or (y < H and self.board.get_color(x, y)):
                return False
        return True

    def place(self):
        complete = True
        for x, y in self.points:
            if 0 <= x < W and 0 <= y < H:
                self.board.set_color(x, y, 'blue')
            else:
                complete = False
        return complete

    def move(self, dx, dy):
        backup = (self.x, self.y)
        self.x, self.y = self.x + dx, self.y + dy
        possible = self.can_place_clipped()
        if not possible:
            self.x, self.y = backup
        return possible

    def rotate(self):
        backup = self.shape
        self.shape = [(-dy, dx) for dx, dy in self.shape]
        possible = self.can_place_clipped()
        if not possible:
            self.shape = backup
        return possible

    def begin_falling(self, period, on_end):
        def fall():
            while True:
                for t in range(period):
                    yield
                if not self.move(0, -1):
                    break
            on_end()
            self.falling_generator = None
            yield
        self.falling_generator = fall()

    def update(self):
        if self.falling_generator:
            next(self.falling_generator)

class Stage:
    def __init__(self):
        self.score = 0
        self.t = 0
        self.falling_period = 0
        self.state = 'normal'
        self.board = Board()
        self.block = self.next_block = None
        self.reset_block()

    def reset_block(self):
        self.falling_period = [10, 9, 8, 7, 6, 5, 4][min(self.t // 100, 6)]
        block = None
        while not block:
            block, self.next_block = self.next_block, Block(self.board)
        block.x, block.y = W // 2, H - 1
        if not block.can_place_clipped():
            self.state = 'game_over'
            return
        self.block = block
        block.begin_falling(self.falling_period, on_end=self.place_block_and_begin_deleting)

    def place_block_and_begin_deleting(self):
        block = self.block
        self.block = None
        if not block.place():
            self.state = 'game_over'
            return
        def on_end_deleting(n_deleted):
            self.score += (n_deleted**2) * 100
            self.reset_block()
        self.board.begin_deleting(self.falling_period, on_end_deleting)

    def update(self):
        if self.block:
            self.block.update()
        self.board.update()
        self.t += 1

    def render(self):
        turtle.clear()
        self.board.draw()
        draw_text(f'SCORE: {self.score}', W + 3, H - 2, 'black')
        draw_text('NEXT BLOCK', W + 3, H - 6, 'black')
        self.next_block.x, self.next_block.y = W + 4, H - 8
        self.next_block.draw()
        if self.block:
            self.block.draw()
        if self.state == 'game_over':
            draw_text('GAME OVER', W//2 - 2, -3, 'red')

    def on_key(self, key):
        block = self.block
        if block:
            if key == 'Left':
                block.move(-1, 0)
            elif key == 'Right':
                block.move(1, 0)
            elif key == 'Down':
                if not block.move(0, -1):
                    self.place_block_and_begin_deleting()
            elif key == 'space':
                block.begin_falling(period=0, on_end=self.place_block_and_begin_deleting)
            elif key == 'Up':
                block.rotate()


turtle.tracer(0, 0)
turtle.hideturtle()
turtle.up()
turtle.listen()
turtle.onkey(turtle.bye, 'Escape')
stage = Stage()
for key in 'Left Right Down space Up'.split():
    turtle.onkey(lambda key=key: stage.on_key(key), key)
def render():
    turtle.ontimer(render, 100)
    stage.update()
    # This is required because turtle.write() always triggers ScrolledCanvas.update().
    old_canvas_update = turtle.ScrolledCanvas.update
    turtle.ScrolledCanvas.update = lambda self: None
    stage.render()
    turtle.ScrolledCanvas.update = old_canvas_update
    turtle.update()
render()
turtle.mainloop()
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Deep Q-learning for playing Tetris 1991viet 0 2,248 Mar-31-2020, 07:11 PM
Last Post: 1991viet

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020