이번 편의 목표: 일곱 가지 블록을 한 묶음으로 섞어 차례로 꺼내고, 다음에 나올 블록을 오른쪽에 미리 보여 줍니다.
완전한 무작위의 문제
지금까지는 random.choice로 매번 일곱 개 중 하나를 골랐습니다. 이렇게 하면 같은 블록이 서너 번 연달아 나오거나, I가 스무 번 넘게 안 나오는 일이 실제로 생깁니다. 계산해 보면 특정 블록이 14번 연속 안 나올 확률이 약 12%입니다. 실력과 상관없이 판이 망가지는 경험은 재미가 없습니다.
7-bag
요즘 테트리스는 일곱 가지를 하나씩 담은 주머니를 섞어서 차례로 꺼내고, 주머니가 비면 다시 일곱 개를 채워 섞습니다. 이것을 7-bag이라고 부릅니다. 이렇게 하면 일곱 번 안에 모든 모양이 꼭 한 번씩 나오고, 같은 블록 사이의 간격은 가장 길어도 12개입니다.
Bag 클래스는 items 리스트가 비었을 때만 list(SHAPES)로 일곱 개를 채우고 random.shuffle로 섞습니다. next는 맨 뒤에서 하나를 꺼내 돌려줍니다. pop()은 리스트 끝에서 꺼내기 때문에 앞에서 꺼내는 pop(0)보다 빠르지만, 일곱 개짜리 리스트에서는 차이가 없으니 읽기 쉬운 쪽을 골라도 됩니다.
다음 블록 미리 보기
Game은 이제 지금 블록(piece)과 다음 블록(next_piece)을 함께 가집니다. lock에서 새 블록을 만들 때 next_piece를 지금 블록으로 올리고, 주머니에서 하나를 더 꺼내 다음 블록으로 둡니다.
draw는 오른쪽 위에 NEXT 글자를 쓰고, 다음 블록의 상자 안 좌표를 판 대신 오른쪽 영역 기준으로 그립니다. 판 위의 칸이 아니라서 draw_cell 대신 픽셀 위치를 직접 계산합니다.
전체 코드
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 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()
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, "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()오른쪽 위에 다음 블록이 보이고, 지금 블록이 굳으면 그 블록이 위에서 나오면 성공입니다. 일곱 개씩 세어 보면 같은 모양이 한 묶음 안에서 겹치지 않습니다.
직접 해 보기
같은 블록 사이의 간격이 가장 길 때 12개라는 말을 확인해 보세요. 어떤 순서로 두 주머니가 나올 때 그렇게 될까요?
해설
첫 주머니에서 I가 맨 처음에 나오고, 다음 주머니에서 I가 맨 마지막에 나오면 됩니다. 첫 주머니의 나머지 6개와 다음 주머니의 앞 6개, 합쳐서 12개가 그 사이에 나옵니다. 더 벌어질 수는 없습니다. 한 주머니 안에 I가 반드시 한 번 있기 때문입니다.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
댓글은 구글이나 카카오로 시작한 뒤에 남길 수 있습니다.
등록된 댓글이 없습니다.