이번 편의 목표: 블록이 굳을 때마다 가득 찬 줄을 지우고, 그 위의 줄들을 아래로 내립니다.
남길 줄만 모으기
줄 하나가 가득 찼다는 것은 그 줄에 None이 하나도 없다는 뜻입니다. 그래서 None in row가 True인 줄, 즉 빈 칸이 하나라도 있는 줄만 골라 kept에 모읍니다. 가득 찬 줄은 이 과정에서 저절로 빠집니다.
지운 줄 수는 원래 줄 수 20에서 kept의 길이를 빼면 나옵니다. 그만큼 빈 줄을 kept의 맨 앞, 즉 판의 맨 위에 끼워 넣으면 줄 수가 다시 20이 되고, 지운 줄 위에 있던 줄들은 그만큼 아래로 내려온 셈이 됩니다.
board[:] = kept는 board라는 이름이 가리키는 리스트의 내용물을 통째로 바꿉니다. board = kept라고 쓰면 함수 안의 이름만 새 리스트를 가리키고, game.board는 그대로 남습니다. 함수에 넘겨받은 리스트를 고칠 때 자주 만나는 함정입니다.
clear_lines는 지운 줄 수를 돌려줍니다. 이번 편에서는 그 값을 쓰지 않지만, 다음 편에서 점수를 계산할 때 씁니다.
Game.lock은 블록을 판에 적은 직후에 clear_lines를 부릅니다. 새 블록을 만들기 전에 지워야 새 블록이 나올 자리를 제대로 확인할 수 있습니다.
전체 코드
import random
import sys
import pygame
COLS, ROWS = 10, 20
CELL = 30
BOARD_WIDTH = COLS * CELL
SIDE_WIDTH = 180
WIDTH, HEIGHT = BOARD_WIDTH + SIDE_WIDTH, ROWS * CELL
FPS = 60
DROP_INTERVAL = 800
BACKGROUND = (18, 18, 24)
GRID = (40, 40, 52)
SHAPES = {
"I": [(0, 1), (1, 1), (2, 1), (3, 1)],
"O": [(0, 0), (1, 0), (0, 1), (1, 1)],
"T": [(1, 0), (0, 1), (1, 1), (2, 1)],
"S": [(1, 0), (2, 0), (0, 1), (1, 1)],
"Z": [(0, 0), (1, 0), (1, 1), (2, 1)],
"J": [(0, 0), (0, 1), (1, 1), (2, 1)],
"L": [(2, 0), (0, 1), (1, 1), (2, 1)],
}
SIZES = {"I": 4, "O": 2, "T": 3, "S": 3, "Z": 3, "J": 3, "L": 3}
COLORS = {
"I": (0, 200, 220),
"O": (230, 200, 0),
"T": (160, 80, 200),
"S": (80, 200, 80),
"Z": (220, 60, 60),
"J": (60, 100, 220),
"L": (240, 140, 30),
}
KICKS = (0, -1, 1, -2, 2)
class Piece:
def __init__(self, kind):
self.kind = kind
self.cells = list(SHAPES[kind])
self.size = SIZES[kind]
self.x = (COLS - self.size) // 2
self.y = 0
def positions(self, dx=0, dy=0, cells=None):
if cells is None:
cells = self.cells
return [(self.x + cx + dx, self.y + cy + dy) for cx, cy in cells]
def rotated(self):
n = self.size
return [(n - 1 - cy, cx) for cx, cy in self.cells]
def new_board():
return [[None] * COLS for _ in range(ROWS)]
def fits(board, positions):
for x, y in positions:
if x < 0 or x >= COLS or y < 0 or y >= ROWS:
return False
if board[y][x] is not None:
return False
return True
def clear_lines(board):
kept = [row for row in board if None in row]
cleared = ROWS - len(kept)
for _ in range(cleared):
kept.insert(0, [None] * COLS)
board[:] = kept
return cleared
class Game:
def __init__(self):
self.board = new_board()
self.piece = Piece(random.choice(list(SHAPES)))
self.drop_timer = 0
def move(self, dx, dy):
if fits(self.board, self.piece.positions(dx, dy)):
self.piece.x += dx
self.piece.y += dy
return True
return False
def rotate(self):
cells = self.piece.rotated()
for kick in KICKS:
if fits(self.board, self.piece.positions(kick, 0, cells)):
self.piece.cells = cells
self.piece.x += kick
return
def soft_drop(self):
if not self.move(0, 1):
self.lock()
self.drop_timer = 0
def lock(self):
color = COLORS[self.piece.kind]
for x, y in self.piece.positions():
self.board[y][x] = color
clear_lines(self.board)
self.piece = Piece(random.choice(list(SHAPES)))
self.drop_timer = 0
if not fits(self.board, self.piece.positions()):
self.board = new_board()
def update(self, dt):
self.drop_timer += dt
if self.drop_timer >= DROP_INTERVAL:
self.drop_timer = 0
if not self.move(0, 1):
self.lock()
def handle_key(game, key):
if key == pygame.K_LEFT:
game.move(-1, 0)
elif key == pygame.K_RIGHT:
game.move(1, 0)
elif key == pygame.K_DOWN:
game.soft_drop()
elif key in (pygame.K_UP, pygame.K_x):
game.rotate()
def draw_cell(screen, x, y, color):
rect = pygame.Rect(x * CELL, y * CELL, CELL, CELL)
pygame.draw.rect(screen, color, rect.inflate(-2, -2))
def draw(screen, game):
screen.fill(BACKGROUND)
for x in range(COLS + 1):
pygame.draw.line(screen, GRID, (x * CELL, 0), (x * CELL, HEIGHT))
for y in range(ROWS + 1):
pygame.draw.line(screen, GRID, (0, y * CELL), (BOARD_WIDTH, y * CELL))
for y, row in enumerate(game.board):
for x, color in enumerate(row):
if color is not None:
draw_cell(screen, x, y, color)
color = COLORS[game.piece.kind]
for x, y in game.piece.positions():
draw_cell(screen, x, y, color)
def main():
pygame.init()
pygame.display.set_caption("Tetris")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
pygame.key.set_repeat(170, 50)
game = Game()
while True:
dt = clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
handle_key(game, event.key)
game.update(dt)
draw(screen, game)
pygame.display.flip()
if __name__ == "__main__":
main()한 줄을 가득 채우면 그 줄이 사라지고 위의 블록이 한 칸 내려오면 성공입니다. 세로로 세운 I로 네 줄을 한 번에 지워 보세요.
직접 해 보기
가득 찬 줄을 for 문으로 돌면서 del board[y]로 바로 지우면 어떤 문제가 생길지 생각해 보세요.
해설
for y in range(ROWS)로 돌면서 18번 줄을 지우면, 19번 줄이 18번 자리로 당겨집니다. 다음 반복은 19번을 보는데, 그 자리에는 이제 아무것도 없어서 IndexError가 나거나, 반대로 위에서 아래로 돌면 당겨진 줄 하나를 건너뜁니다. 연속한 두 줄이 동시에 찼을 때 하나만 지워지는 버그가 이렇게 생깁니다. 지울 것을 고르는 대신 남길 것을 새로 모으면 이 문제가 없습니다.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
댓글은 구글이나 카카오로 시작한 뒤에 남길 수 있습니다.
등록된 댓글이 없습니다.