이번 편의 목표: 시간이 지나면 블록이 저절로 내려오고, 더 내려갈 수 없으면 판에 굳은 뒤 새 블록이 나오게 합니다.
시간 재기
clock.tick(FPS)는 지난번 호출 뒤로 흐른 시간을 밀리초로 돌려줍니다. 지금까지는 이 값을 버렸는데, 이제 dt라는 이름으로 받아 game.update(dt)에 넘깁니다.
Game.update는 dt를 drop_timer에 계속 더하다가 DROP_INTERVAL(800밀리초)을 넘으면 0으로 되돌리고 블록을 한 칸 내립니다. 프레임 수를 세지 않고 실제 흐른 시간을 쓰기 때문에, 컴퓨터가 느려서 프레임이 덜 나와도 블록이 내려오는 속도는 같습니다.
굳히기
move(0, 1)이 False를 돌려주면 블록 바로 아래가 바닥이거나 다른 블록이라는 뜻입니다. 그때 lock을 부릅니다. lock은 블록의 네 칸 좌표에 그 블록의 색을 판에 적고, 새 블록을 만들고, 타이머를 0으로 돌립니다. 판에 색이 적힌 칸은 02편에서 만든 draw가 알아서 그려 줍니다.
새 블록이 나올 자리가 이미 막혀 있으면 원래는 게임 오버입니다. 게임 오버 화면은 11편에서 만들기로 하고, 이번 편에서는 판을 새로 비워 계속 놀 수 있게 했습니다.
아래 방향키는 이제 soft_drop을 부릅니다. 한 칸 내려가지 못하면 그 자리에서 바로 굳히고, 내려갔든 굳혔든 타이머를 0으로 돌립니다. 타이머를 돌리지 않으면 키로 내린 직후에 중력이 또 한 칸을 내려서 두 칸이 내려간 것처럼 보일 때가 있습니다.
블록이 저절로 쌓이므로 스페이스로 새 블록을 꺼내던 시험 기능은 지웠습니다.
전체 코드
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
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
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()블록이 0.8초마다 한 칸씩 내려오고, 바닥이나 다른 블록 위에 쌓이면 성공입니다. 아직 줄은 지워지지 않으니 가득 찬 줄도 그대로 남습니다.
직접 해 보기
DROP_INTERVAL을 200으로 바꿔서 빠르기를 느껴 보고, 다시 800으로 되돌리세요. 그 다음, 아래 방향키를 누르고 있을 때 블록이 바닥에 닿자마자 굳는 이유를 코드에서 찾아보세요.
해설
soft_drop이 move(0, 1)에 실패하면 곧바로 lock을 부르기 때문입니다. 공식 테트리스에는 바닥에 닿은 뒤 잠깐 기다려 주는 잠금 지연(lock delay)이 있어서, 그 사이에 옆으로 밀어 넣을 수 있습니다. 이 연재에서는 중력으로 내려올 때 한 번의 낙하 간격만큼 기다리는 효과만 있고, 키로 내릴 때는 바로 굳습니다.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
댓글은 구글이나 카카오로 시작한 뒤에 남길 수 있습니다.
등록된 댓글이 없습니다.