Python 3 สาปเทอร์มินัล Tetris
ฉันเรียนรู้การเขียนโปรแกรมด้วยตัวเองมาสองสามปีแล้วโดยส่วนใหญ่ใช้ python และฉันได้สร้างเกมบรรทัดคำสั่งที่เหมือน tetris โดยใช้ไลบรารี Curses ของ Python ดูเหมือนว่าเกมจะทำงานได้ตามที่ตั้งใจไว้ แต่ฉันไม่มีประสบการณ์ในการเขียนโค้ดเชิงวัตถุ ดังนั้นฉันต้องการความคิดเห็นเกี่ยวกับโครงสร้างของโค้ดและสมมติว่าโครงสร้างนั้นใช้ได้รูปแบบของโค้ด
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()
คำตอบ
โดยทั่วไปจะมีลักษณะโครงสร้างใช้ชื่อที่ดีแยกเป็นหน้าที่ ฯลฯ สิ่งเหล่านี้เป็นสิ่งที่ดี
ฉันมีความคิดเห็นบางส่วน แต่จำไว้ว่านี่เป็นความคิดเห็นของฉันฉันไม่สามารถพูดได้ว่าถูกหรือผิดและฉันจะไม่อ้างถึงมาตรฐานหรือรูปแบบโค้ดใด ๆ
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)
โค้ดชิ้นนี้สั้นและกะทัดรัดมาก ไม่กี่บรรทัดและใช้ Python เช่น double list comp understandion ได้รับจำนวนมากโดยใช้รหัสไม่กี่ไบต์
ฉันคิดว่ามันยากที่จะอ่านและฉันไม่อยากร่วมมือกับคุณในโครงการนี้หากคุณเขียนโค้ดแบบนี้เป็นจำนวนมากเพราะฉันจะต้องใช้เวลามากในการแก้ปัญหาและพยายามทำความเข้าใจว่ามีอะไรทำบ้างก่อนที่จะแก้ไข หรือขยายออกไป และหากจำเป็นต้องแก้ไขก็อาจต้องเขียนใหม่ทั้งหมด
ข้อเสนอแนะสำหรับวิธีการเขียนที่แตกต่างออกไปซึ่งฉันเข้าใจได้ง่ายขึ้น (อีกครั้งนี่เป็นเรื่องส่วนตัว)
def in_bounds(self, x, y):
return x >= 0 and x < self.num_columns and y >= 0 and y < self.num_rows
ฟังก์ชั่นนี้จัดการเพียงชิ้นเดียวและรับพิกัดโดยตรงโดยไม่ "สนใจ" ว่าพิกัดเหล่านั้นหมายถึงอะไร (ชิ้นส่วน แต่ตอนนี้สามารถใช้กับสิ่งอื่นได้ด้วย)
ฉันคิดว่าการใช้xและyเป็นธรรมชาติเนื่องจากแถวและคอลัมน์ทำงานเป็นพิกัดในเกมเตตริส สิ่งนี้มีความเฉพาะเจาะจงมากกว่าiและjมักใช้เป็นตัววนซ้ำสำหรับวงในซึ่งไม่จำเป็นหรือเกี่ยวข้องกับพิกัดตามธรรมชาติ
นอกจากนี้ฉันกำลังดูทีละกรณีไม่จำเป็นต้องใช้allและ "จำ" ชิ้นส่วนไปพร้อม ๆ กัน นี่คือการเปลี่ยนแปลงที่สำคัญที่สุดในการทำให้สามารถอ่านได้และเข้าใจง่ายทีละชิ้น
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
นี่เป็นสิ่งที่ตรงกันข้าม ไม่มีใครเข้าใจรหัสนี้ผิดหรือต้องมองมากกว่าสองสามวินาทีเพื่อทำความเข้าใจ ฉันชอบรหัสแบบนี้เมื่อฉันตรวจสอบบางสิ่ง สามารถเขียนใหม่และเรียบเรียงให้สั้นลงได้มาก แต่ไม่จำเป็นและจะไม่ทำให้ง่ายต่อการทำงานด้วย
ถ้าคุณมีตัวเลือกมากกว่าสมมุติว่า 4 ตัวขึ้นไปฉันจะใช้คำสั่งเพื่อลดการซ้ำของif elifและx,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)
การทำซ้ำมากเกินไปที่นี่ สิ่งนี้สามารถปรับปรุงได้
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)