May-13-2022, 07:40 AM
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
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()