이번 편의 목표: 스페이스로 블록을 바닥까지 한 번에 떨어뜨리고, 떨어질 자리를 테두리만 있는 블록으로 미리 보여 줍니다.
하드 드롭
hard_drop은 move(0, 1)이 실패할 때까지 반복해서 블록을 내리고, 내려간 칸 수만큼 2점씩 더한 뒤 바로 lock을 부릅니다. move가 성공 여부를 돌려주게 만들어 둔 덕분에 while 한 줄로 끝납니다.
고스트
하드 드롭을 쓰려면 블록이 어디에 떨어질지 알아야 합니다. ghost_offset은 지금 블록을 몇 칸 내려도 들어가는지 세어 돌려줍니다. 실제로 옮기지 않고 positions(0, dy + 1)로 계산만 해 보기 때문에 지금 블록은 그대로입니다. hard_drop과 같은 원리를 계산으로만 하는 것입니다.
draw_cell에는 outline 인자를 더했습니다. True이면 칸을 채우지 않고 두께 2픽셀 테두리만 그립니다. draw는 지금 블록을 그리기 전에 ghost_offset만큼 내린 자리에 테두리 블록을 먼저 그립니다. 고스트를 먼저 그려야 블록이 바닥 가까이 내려와 둘이 겹칠 때 진짜 블록이 위에 보입니다.
전체 코드
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
BACKGROUND = (18, 18, 24)
GRID = (40, 40, 52)
TEXT = (235, 235, 240)
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),
}
LINE_SCORES = {1: 100, 2: 300, 3: 500, 4: 800}
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]
class Bag:
def __init__(self):
self.items = []
def next(self):
if not self.items:
self.items = list(SHAPES)
random.shuffle(self.items)
return self.items.pop()
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.bag = Bag()
self.piece = Piece(self.bag.next())
self.next_piece = Piece(self.bag.next())
self.score = 0
self.lines = 0
self.level = 1
self.drop_timer = 0
def interval(self):
return max(100, 800 - (self.level - 1) * 70)
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 self.move(0, 1):
self.score += 1
else:
self.lock()
self.drop_timer = 0
def hard_drop(self):
distance = 0
while self.move(0, 1):
distance += 1
self.score += distance * 2
self.lock()
def ghost_offset(self):
dy = 0
while fits(self.board, self.piece.positions(0, dy + 1)):
dy += 1
return dy
def lock(self):
color = COLORS[self.piece.kind]
for x, y in self.piece.positions():
self.board[y][x] = color
cleared = clear_lines(self.board)
if cleared:
self.score += LINE_SCORES[cleared] * self.level
self.lines += cleared
self.level = self.lines // 10 + 1
self.piece = self.next_piece
self.next_piece = Piece(self.bag.next())
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 >= self.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()
elif key == pygame.K_SPACE:
game.hard_drop()
def draw_cell(screen, x, y, color, outline=False):
rect = pygame.Rect(x * CELL, y * CELL, CELL, CELL)
if outline:
pygame.draw.rect(screen, color, rect.inflate(-4, -4), 2)
else:
pygame.draw.rect(screen, color, rect.inflate(-2, -2))
def draw_text(screen, font, text, x, y):
screen.blit(font.render(text, True, TEXT), (x, y))
def draw(screen, game, font):
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(0, game.ghost_offset()):
draw_cell(screen, x, y, color, outline=True)
for x, y in game.piece.positions():
draw_cell(screen, x, y, color)
left = BOARD_WIDTH + 20
draw_text(screen, font, "NEXT", left, 20)
for cx, cy in game.next_piece.cells:
rect = pygame.Rect(left + cx * CELL, 60 + cy * CELL, CELL, CELL)
pygame.draw.rect(screen, COLORS[game.next_piece.kind], rect.inflate(-2, -2))
draw_text(screen, font, f"SCORE {game.score}", left, 200)
draw_text(screen, font, f"LINES {game.lines}", left, 240)
draw_text(screen, font, f"LEVEL {game.level}", left, 280)
def main():
pygame.init()
pygame.display.set_caption("Tetris")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
font = pygame.font.Font(None, 34)
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, font)
pygame.display.flip()
if __name__ == "__main__":
main()블록 아래쪽 떨어질 자리에 같은 색 테두리가 보이고, 스페이스를 누르면 블록이 그 자리로 바로 떨어지면 성공입니다.
직접 해 보기
빈 판에서 맨 위의 세로 I를 하드 드롭하면 몇 점을 얻는지 계산해 보세요. 세로 I는 4×4 상자의 2번 열을 차지하고, 상자는 y = 0에서 시작합니다.
해설
세로 I의 칸은 y = 0부터 3까지입니다. 바닥 줄이 19이므로 맨 아래 칸이 19에 닿을 때까지 16칸 내려갑니다. 16 × 2 = 32점입니다. 사실 블록이 나오자마자 위 방향키로 세우면 그동안 중력이 한두 칸 내렸을 수 있으니, 실제 점수는 그만큼 적게 나올 수 있습니다.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
댓글은 구글이나 카카오로 시작한 뒤에 남길 수 있습니다.
등록된 댓글이 없습니다.