Python 3 Curses Terminal Tetris

Sep 14 2020

Birkaç yıldır ağırlıklı olarak python kullanarak programlamayı öğreniyorum ve Python'un Curses kütüphanesini kullanarak tetris benzeri bir komut satırı oyunu yarattım. Oyun amaçlandığı gibi çalışıyor gibi görünüyor; ancak nesne yönelimli kod yazma deneyimim yok. Bu nedenle, kodun yapısı ve yapının uygun olduğunu varsayarak kodun stili hakkında geri bildirim almak istiyorum.

from copy import deepcopy
import time
import curses
import random


class TetrisPiece:
    def __init__(self, indices, center_of_rotation, color):
        self.indices = indices
        self.center_of_rotation = center_of_rotation
        self.last_move_overlap = False
        self.color = color


class TetrisBoard:
    def __init__(self, num_rows, num_columns):
        self.num_rows = num_rows
        self.num_columns = num_columns

        self.array = [[0] * self.num_columns for _ in range(self.num_rows)]
        self.active_piece = None

    def in_bounds(self, temp_active_piece_indices):
        return all(0 <= i < self.num_rows and 0 <= j < self.num_columns
                   for i, j in temp_active_piece_indices)

    def no_overlap(self, temp_active_piece_indices):
        return all(self.array[i][j] == 0 for i, j in
                   set(temp_active_piece_indices) - set(self.active_piece.indices))

    def add_piece(self, piece):
        # try to place Piece near top center of board
        new_active_piece_indices = [(i, j + int(self.num_columns / 2) - 1)
                                    for i, j in piece.indices]

        if all(self.array[i][j] == 0 for i, j in new_active_piece_indices):
            self.active_piece = piece
            self.update_array(new_active_piece_indices)
            piece.indices = new_active_piece_indices
            piece.center_of_rotation[1] += int(self.num_columns / 2) - 1
            piece.last_move_overlap = False
        else:
            piece.last_move_overlap = True

    def rotate_active_piece(self):
        # rotates active piece indices 90 degrees counter clockwise about it's
        # center of a rotation

        x, y = self.active_piece.center_of_rotation

        # this translates the active piece so that it's center is
        # the origin, then rotates each point in indices about the origin,
        # then translates the piece so that it's center is at it's
        # original position
        temp_active_piece_indices = [(int(-j + y + x), int(i - x + y))
                                     for i, j in self.active_piece.indices]

        if (self.in_bounds(temp_active_piece_indices)
                and self.no_overlap(temp_active_piece_indices)):
            self.update_array(temp_active_piece_indices)
            self.active_piece.indices = temp_active_piece_indices

    def translate_active_piece(self, direction):
        if direction == 'right':
            x, y = 0, 1
        elif direction == 'left':
            x, y = 0, -1
        elif direction == 'down':
            x, y = 1, 0

        temp_active_piece_indices = [(i + x, j + y)
                                     for i, j in self.active_piece.indices]
        if (self.in_bounds(temp_active_piece_indices)
                and self.no_overlap(temp_active_piece_indices)):
            self.update_array(temp_active_piece_indices)
            self.active_piece.indices = temp_active_piece_indices
            self.active_piece.center_of_rotation[0] += x
            self.active_piece.center_of_rotation[1] += y

            self.active_piece.last_move_overlap = False

        elif (self.in_bounds(temp_active_piece_indices)
              and not self.no_overlap(temp_active_piece_indices)):
            self.active_piece.last_move_overlap = True

        # this is necessary to tell when a piece hits the bottom of the
        # board
        elif not self.in_bounds(temp_active_piece_indices) and direction == 'down':
            self.active_piece.last_move_overlap = True

    def update_array(self, new_indices):
        for i, j in self.active_piece.indices:
            self.array[i][j] = 0
        for i, j in new_indices:
            self.array[i][j] = self.active_piece.color


class CursesWindow:
    def __init__(self, game):
        self.game = game
        self.window = None

    def update(self):
        pass

    def refresh(self):
        self.window.refresh()

    def addstr(self, y, x, string):
        self.window.addstr(y, x, string)


class BoardWindow(CursesWindow):
    def __init__(self, game):
        CursesWindow.__init__(self, game)

        # the window's border adds two extra rows and two extra columns
        self.num_rows = game.board.num_rows + 2
        self.num_columns = game.board.num_columns + 2

        self.window = curses.newwin(
            self.num_rows,
            self.num_columns
        )

        self.window.border('*', '*', '*', '*', '*', '*', '*', '*')
        self.update()

    def update(self):
        # only update the interior of the window
        for i in range(self.num_rows - 2):
            for j in range(self.num_columns - 2):
                if self.game.board.array[i][j] != 0:
                    self.window.addstr(
                        i + 1,
                        j + 1,
                        '1',
                        curses.color_pair(self.game.board.array[i][j])
                    )
                else:
                    self.window.addstr(i + 1, j + 1, '.')
        self.window.refresh()

    def keypad(self, flag):
        self.window.keypad(flag)

    def nodelay(self, flag):
        self.window.nodelay(flag)

    def getch(self):
        return self.window.getch()


class ScoreWindow(CursesWindow):
    def __init__(self, game, board_window):
        CursesWindow.__init__(self, game)
        self.num_items_to_display = 3

        # the window's border adds two extra rows
        self.num_rows = self.num_items_to_display + 2

        # 6 digits for the string 'score:' + max_num_score_digits + 2 for border
        self.num_columns = 6 + game.max_num_score_digits + 2

        self.window = curses.newwin(
            self.num_rows,
            self.num_columns,
            0,
            board_window.num_columns + 1
        )

        self.update()

    def update(self):
        self.window.erase()
        self.window.border('*', '*', '*', '*', '*', '*', '*', '*')
        self.window.addstr(1, 1, f'Score:{self.game.score}')
        self.window.addstr(2, 1, f'Lines:{self.game.lines_completed}')
        self.window.addstr(3, 1, f'Level:{self.game.level}')
        self.window.refresh()


class PiecePreviewWindow(CursesWindow):
    def __init__(self, game, board_window, score_window):
        CursesWindow.__init__(self, game)

        # the window's border adds two extra rows and two extra columns
        self.num_rows = game.max_piece_length + 2
        self.num_columns = game.max_piece_length + 2

        self.window = curses.newwin(
            self.num_rows,
            self.num_columns,
            score_window.num_rows,
            board_window.num_columns + 1
        )

        self.window.border('*', '*', '*', '*', '*', '*', '*', '*')
        self.update()

    def update(self):
        self.window.erase()

        # only update the interior of the window
        for i in range(self.num_rows - 2):
            for j in range(self.num_columns - 2):
                if (i, j) in self.game.next_piece.indices:
                    self.window.addstr(
                        i + 1,
                        j + 1,
                        '1',
                        curses.color_pair(self.game.next_piece.color)
                    )

        self.window.refresh()


class GUI:
    def __init__(self, game):
        self.game = game

        curses.initscr()
        curses.start_color()
        curses.noecho()
        curses.cbreak()
        curses.curs_set(0)

        curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
        curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
        curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
        curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK)
        curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
        curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK)
        curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK)

        self.board_window = BoardWindow(game)
        self.score_window = ScoreWindow(game, self.board_window)
        self.piece_preview_window = PiecePreviewWindow(game, self.board_window,
                                                       self.score_window)

        self.board_window.keypad(True)
        self.board_window.nodelay(True)


class Game:
    def __init__(self, board_num_rows, board_num_columns):
        self.board = TetrisBoard(board_num_rows, board_num_columns)

        self.score = 0
        self.max_num_score_digits = 8
        self.lines_completed = 0
        self.level = 0

        self.SPACE_KEY_VALUE = 32

        # approximate frame rate
        self.frame_rate = 60

        self.pieces = [
            TetrisPiece([(0, 1), (1, 1), (2, 1), (3, 1)], [1.5, 1.5], 1),  # I
            TetrisPiece([(0, 1), (1, 1), (2, 1), (2, 2)], [1, 1], 2),  # J
            TetrisPiece([(0, 1), (1, 1), (2, 1), (2, 0)], [1, 1], 3),  # L
            TetrisPiece([(0, 0), (0, 1), (1, 0), (1, 1)], [.5, .5], 4),  # O
            TetrisPiece([(1, 0), (1, 1), (0, 1), (0, 2)], [1, 1], 5),  # S
            TetrisPiece([(1, 0), (1, 1), (1, 2), (0, 1)], [1, 1], 6),  # T
            TetrisPiece([(0, 0), (0, 1), (1, 1), (1, 2)], [1, 1], 7)  # Z
        ]

        self.max_piece_length = 4
        
        self.next_piece = deepcopy(random.choice(self.pieces))

        self.GUI = GUI(self)

    def points(self, number_of_lines):
        coefficients = [0, 40, 100, 300, 1200]
        return coefficients[number_of_lines] * (self.level + 1)

    def main_loop(self):
        self.board.add_piece(self.next_piece)
        self.next_piece = deepcopy(random.choice(self.pieces))
        self.GUI.piece_preview_window.update()

        loop_count = 0
        while True:
            keyboard_input = self.GUI.board_window.getch()

            loop_count += 1

            force_move = (loop_count % max(self.frame_rate - self.level, 1) == 0)
            hard_drop = (keyboard_input == self.SPACE_KEY_VALUE)
            if force_move or hard_drop:
                if hard_drop:
                    while not self.board.active_piece.last_move_overlap:
                        self.board.translate_active_piece('down')

                    self.GUI.board_window.update()
                    time.sleep(.5)

                elif force_move:
                    self.board.translate_active_piece('down')

                if self.board.active_piece.last_move_overlap:
                    # try to clear lines one at a time starting from the top of
                    # the screen
                    line_count = 0
                    for row_number, row in enumerate(self.board.array):
                        if all(row):
                            # highlight row to be deleted
                            # add 1 to row_number because of board_window's border
                            self.GUI.board_window.addstr(
                                row_number + 1, 1, '=' * self.board.num_columns
                            )

                            self.GUI.board_window.refresh()
                            time.sleep(.5)

                            # delete row
                            del self.board.array[row_number]
                            self.board.array.insert(0, [0] * self.board.num_columns)

                            self.GUI.board_window.update()
                            time.sleep(.5)

                            line_count += 1

                    self.score += self.points(line_count)
                    self.lines_completed += line_count
                    self.level = self.lines_completed // 2

                    # Basically, reset the game to prevent the strings
                    # corresponding to the score, lines_completed, or level
                    # variables from exceeding the dimensions the score_window
                    if len(str(self.score)) > self.max_num_score_digits:
                        self.score = 0
                        self.level = 0
                        self.lines_completed = 0

                    self.GUI.score_window.update()

                    # try to add nextPiece to Board
                    self.board.add_piece(self.next_piece)

                    # if unsuccessful, gameover
                    if self.next_piece.last_move_overlap:
                        break

                    self.next_piece = deepcopy(random.choice(self.pieces))
                    self.GUI.piece_preview_window.update()

            else:
                if keyboard_input == ord('w'):
                    self.board.rotate_active_piece()
                if keyboard_input == ord('d'):
                    self.board.translate_active_piece('right')
                if keyboard_input == ord('s'):
                    self.board.translate_active_piece('down')
                if keyboard_input == ord('a'):
                    self.board.translate_active_piece('left')
                # exit game
                if keyboard_input == ord('e'):
                    break

            self.GUI.board_window.update()

            # delay after a rotation
            if keyboard_input == ord('w'):
                time.sleep(.25)

            time.sleep(1 / self.frame_rate)

        # Reset terminal window before exiting the game.
        curses.nocbreak()
        self.GUI.board_window.keypad(False)
        self.GUI.board_window.nodelay(False)
        curses.echo()
        curses.endwin()
        curses.curs_set(1)

        print('Game Over')
        exit()


# Run the game
game = Game(board_num_rows=16, board_num_columns=10)
game.main_loop()

Yanıtlar

5 user985366 Sep 14 2020 at 05:55

Genel olarak yapılandırılmış görünüyor, iyi isimler kullanıyor, işlevlere ayrılıyor vs. Bunlar iyi şeyler.

Bazı yorumlarım var ama bunların benim görüşlerim olduğunu unutmayın, bunun doğru ya da yanlış olduğunu söyleyemem ve herhangi bir standart ya da kod stiline atıfta bulunmayacağım.

1

def in_bounds(self, temp_active_piece_indices):
    return all(0 <= i < self.num_rows and 0 <= j < self.num_columns
               for i, j in temp_active_piece_indices)

Bu kod parçası çok kısa ve derli toplu. Birkaç satır ve çift liste anlama gibi Python kullanımı. Birkaç bayt kodla çok şey yapılır.

Yine de okumayı zor buluyorum ve böyle çok fazla kod yazarsanız bu projede sizinle işbirliği yapmak istemem, çünkü değiştirmeden önce bir şeyin ne yaptığını çözmek ve anlamaya çalışmak için çok fazla zaman harcamam gerekirdi. veya genişletme. Değiştirilmesi gerekiyorsa, tamamen yeniden yazılması gerekebilir.

Nasıl farklı yazılacağına dair bir öneri, anlamayı daha kolay buluyorum (yine, bu tabii ki özneldir).

def in_bounds(self, x, y):
    return x >= 0 and x < self.num_columns and y >= 0 and y < self.num_rows
        

Bu işlev yalnızca bir parçayı işler ve koordinatları doğrudan alır, bu koordinatların neyi temsil ettiğini "umursamaz" (bir parça, ancak artık başka herhangi bir şey için de kullanılabilir).

Bir tetris oyununda satırlar ve sütunlar koordinat olarak işlev gördüğü için xve kullanmanın ydoğal olduğunu düşünüyorum . Bu daha spesifiktir ive jbu genellikle zorunlu olarak veya doğal olarak koordinatlara ilgili olmayan bir iç döngü için yineleyicileri olarak kullanılır.

Ayrıca, her bir vakaya birer birer bakıyorum all, parçaları aynı anda kullanmaya ve "hatırlamaya" gerek yok . Bu, onu okunabilir ve parça parça anlaşılmasını kolaylaştıran en önemli değişikliktir.

İşlevi dışarıdan çağırırken, artık kullanmak mantıklı all.

if all([in_bounds(x,y) for (y,x) in pieces]):

2

def translate_active_piece(self, direction):
    if direction == 'right':
        x, y = 0, 1
    elif direction == 'left':
        x, y = 0, -1
    elif direction == 'down':
        x, y = 1, 0

Bu tam tersi. Hiç kimse bu kodu yanlış anlamayacak veya anlamak için birkaç saniyeden fazla bakmak zorunda kalmayacak. Bir şeyi gözden geçirirken böyle bir kod kullanmayı tercih ederim. Çok daha kısa olması için yeniden yazılabilir ve kıvrılabilir, ancak bu gerekli değildir ve çalışmayı kolaylaştırmaz.

Yine de daha fazla seçeneğiniz olsaydı, diyelim 4 veya daha fazla, tekrarını azaltmak için bir dikte kullanırdım if elifvex,y=

3

    curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
    curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
    curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
    curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK)
    curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
    curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK)
    curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK)

Burada çok fazla tekrar var. Bu geliştirilebilir.

colors = [curses.COLOR_RED, curses.COLOR_GREEN, curses.COLOR_YELLOW, ...]
for i, x in enumerate(colors, start=1):
    curses.init_pair(i, x, curses.COLOR_BLACK)