Python 3 Mengutuk Terminal Tetris
Saya telah belajar pemrograman sendiri selama beberapa tahun terutama menggunakan python, dan saya telah membuat permainan baris perintah mirip tetris menggunakan pustaka Kutukan Python. Permainan tampaknya berfungsi sebagaimana mestinya; tapi saya kurang pengalaman menulis kode berorientasi objek. Jadi, saya ingin umpan balik tentang struktur kode, dan menganggap strukturnya baik-baik saja, gaya kodenya.
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()
Jawaban
Secara umum terlihat terstruktur, menggunakan nama yang baik, dipecah menjadi beberapa fungsi, dll. Itu adalah hal yang baik.
Saya memiliki beberapa komentar tetapi ingat ini adalah pendapat saya, saya tidak dapat mengatakan bahwa itu benar atau salah dan saya tidak akan mengacu pada standar atau gaya kode apa pun.
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)
Potongan kode ini sangat pendek dan padat. Beberapa baris, dan menggunakan Python hal-hal seperti pemahaman daftar ganda. Itu banyak dilakukan dengan beberapa byte kode.
Saya merasa sulit untuk membaca dan saya tidak ingin bekerja sama dengan Anda dalam proyek ini jika Anda menulis banyak kode seperti ini, karena saya harus menghabiskan begitu banyak waktu untuk mengurai dan mencoba memahami apa yang dilakukan sesuatu, sebelum memodifikasi atau memperpanjangnya. Dan jika perlu dimodifikasi, mungkin perlu ditulis ulang sepenuhnya.
Sebuah saran tentang bagaimana menuliskannya secara berbeda, yang saya anggap lebih mudah untuk dipahami (sekali lagi, ini subjektif tentunya).
def in_bounds(self, x, y):
return x >= 0 and x < self.num_columns and y >= 0 and y < self.num_rows
Fungsi ini hanya menangani satu bagian dan mengambil koordinat secara langsung, tidak "peduli" apa yang diwakili oleh koordinat tersebut (satu bagian, tapi sekarang dapat digunakan untuk hal lain juga).
Saya pikir menggunakan xdan ywajar karena baris dan kolom berfungsi sebagai koordinat dalam permainan tetris. Ini lebih spesifik daripada idan jyang sering digunakan sebagai iterator untuk loop dalam apa pun, yang tidak selalu atau secara alami terkait dengan koordinat.
Juga, saya melihat setiap kasus satu per satu, tidak perlu menggunakan alldan "mengingat" bagiannya secara bersamaan. Ini adalah perubahan terpenting agar dapat dibaca dan sepotong demi sepotong mudah dipahami.
Saat memanggil fungsi dari luar, sekarang masuk akal untuk digunakan 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
Ini sebaliknya. Tidak ada yang akan salah memahami kode ini atau harus melihat lebih dari beberapa detik untuk memahaminya. Saya lebih suka kode seperti ini ketika saya mengulas sesuatu. Ini bisa ditulis ulang dan berbelit-belit menjadi jauh lebih pendek, tapi itu tidak perlu dan tidak akan membuatnya lebih mudah untuk dikerjakan.
Jika Anda memiliki lebih banyak opsi, katakanlah 4 atau lebih, saya akan menggunakan dict untuk mengurangi pengulangan if elifdanx,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)
Terlalu banyak pengulangan di sini. Ini bisa diperbaiki.
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)