이번 편의 목표: 지운 줄 수에 따라 점수를 주고, 열 줄을 지울 때마다 레벨을 올려 블록이 더 빨리 떨어지게 합니다. 오른쪽 빈자리에 점수, 줄 수, 레벨을 표시합니다.
점수 규칙
한 번에 지운 줄이 1줄이면 100점, 2줄이면 300점, 3줄이면 500점, 4줄이면 800점에 지금 레벨을 곱합니다. 공식 테트리스 가이드라인의 기본 점수표와 같습니다. 네 줄을 한 번에 지우면 한 줄씩 네 번 지울 때(400점)의 두 배를 주므로, 기다렸다가 I로 한꺼번에 지울 이유가 생깁니다. 이 표를 LINE_SCORES 딕셔너리에 두었습니다.
아래 방향키로 한 칸 내릴 때마다 1점을 더합니다. 빨리 내릴수록 조금씩 이득이 됩니다.
레벨과 속도
레벨은 지운 줄 수를 10으로 나눈 몫에 1을 더한 값입니다. 0~9줄은 레벨 1, 10~19줄은 레벨 2입니다.
낙하 간격은 interval 메서드가 정합니다. 레벨 1에서 800밀리초이고, 레벨이 하나 오를 때마다 70밀리초씩 줄어 레벨 11부터는 100밀리초에서 멈춥니다. max(100, ...)가 그 아래로 내려가지 않게 막습니다. 상수였던 DROP_INTERVAL은 이 메서드로 바뀌어서 지웠습니다.
글자 쓰기
pygame.font.Font(None, 34)는 pygame에 들어 있는 기본 글꼴을 34 크기로 엽니다. font.render는 글자를 그린 그림(Surface)을 만들고, screen.blit이 그 그림을 화면의 정해진 위치에 붙입니다. draw_text가 이 두 단계를 묶습니다.
기본 글꼴에는 한글이 없어서 한글을 쓰면 네모로 나옵니다. 그래서 화면 글자는 SCORE, LINES, LEVEL처럼 영어로 적었습니다. 한글을 쓰려면 Windows에서는 pygame.font.SysFont("malgungothic", 28)처럼 설치된 글꼴 이름을 주거나, 나눔고딕 같은 글꼴 파일을 프로젝트 폴더에 두고 pygame.font.Font("NanumGothic.ttf", 28)로 엽니다.
글꼴은 main에서 한 번만 열어 draw에 넘깁니다. 매 프레임 글꼴 파일을 다시 열면 눈에 띄게 느려집니다.
전체 코드
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]
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.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 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 = 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 >= 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()
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_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():
draw_cell(screen, x, y, color)
left = BOARD_WIDTH + 20
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()오른쪽에 SCORE, LINES, LEVEL이 보이고, 줄을 지우면 숫자가 오르면 성공입니다.
직접 해 보기
레벨 3에서 네 줄을 한 번에 지우면 몇 점을 얻는지 계산해 보세요. 그리고 레벨 5의 낙하 간격을 구해 보세요.
해설
네 줄은 800점이고 레벨 3을 곱해 2400점입니다. 레벨 5의 낙하 간격은 800 - (5 - 1) × 70 = 520밀리초입니다. 점수는 줄을 지운 순간의 레벨로 계산합니다. lock에서 점수를 먼저 더하고 레벨을 나중에 올리기 때문에, 9줄에서 네 줄을 지워 13줄이 될 때도 그 네 줄은 레벨 1 점수로 계산됩니다.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
댓글은 구글이나 카카오로 시작한 뒤에 남길 수 있습니다.
등록된 댓글이 없습니다.