이번 편의 목표: 방향키로 블록을 옮기되, 판 밖으로 나가거나 굳은 블록과 겹치는 이동은 막습니다.
먼저 따져 보고 움직이기
블록을 옮긴 다음에 문제가 있으면 되돌리는 방법도 있지만, 옮길 자리를 먼저 계산해서 괜찮을 때만 옮기는 편이 단순합니다. 지난 편에 만든 positions(dx, dy)가 옮긴 뒤의 좌표를 돌려주므로, 그 좌표들이 모두 들어갈 수 있는지만 확인하면 됩니다.
fits는 좌표 목록을 받아 하나라도 판 밖이거나 이미 채워진 칸이면 False를 돌려줍니다. 판 밖인지 먼저 보고, 그 다음에 board[y][x]를 읽습니다. 순서가 바뀌면 x가 10일 때 IndexError가 나고, x가 -1일 때는 Python이 음수 인덱스를 뒤에서부터 세기 때문에 오류 없이 엉뚱한 칸을 읽습니다.
Game.move는 fits가 True일 때만 블록의 x, y를 바꾸고 True를 돌려줍니다. 옮기지 못하면 False를 돌려주는데, 이 값은 06편에서 블록이 바닥에 닿았는지 아는 데 씁니다.
키 반복
pygame.key.set_repeat(170, 50)은 키를 누르고 있으면 170밀리초 뒤부터 50밀리초마다 KEYDOWN 이벤트를 다시 만들어 줍니다. 방향키를 꾹 누르면 블록이 미끄러지듯 움직입니다. 이 줄을 빼면 한 번 누를 때 한 칸만 움직입니다.
스페이스로 새 블록을 꺼내는 시험 기능은 아직 남겨 두었습니다. 블록이 바닥에 닿은 뒤 다른 모양을 시험할 때 씁니다.
전체 코드
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)
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),
}
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 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)))
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 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.move(0, 1)
elif key == pygame.K_SPACE:
game.piece = Piece(random.choice(list(SHAPES)))
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:
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)
draw(screen, game)
pygame.display.flip()
if __name__ == "__main__":
main()왼쪽, 오른쪽, 아래 방향키로 블록이 움직이고, 벽과 바닥에서 더 나가지 않으면 성공입니다.
직접 해 보기
fits 안의 두 if 문 순서를 바꾸고, 블록을 왼쪽 벽에 붙인 채 왼쪽 키를 눌러 보세요. 무슨 일이 생기는지 확인한 뒤 원래대로 돌려 놓으세요.
해설
x가 -1인 좌표를 검사할 때 board[y][-1], 즉 그 줄의 맨 오른쪽 칸을 읽습니다. 그 칸이 비어 있으면 검사를 통과하고, 바로 다음 범위 검사에서 False가 나와 결과는 같아 보입니다. 하지만 오른쪽 벽 쪽으로 x가 10이 되는 순간 board[y][10]에서 IndexError가 나며 프로그램이 멈춥니다. 범위 검사를 먼저 하는 것은 오류를 막는 순서입니다.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
댓글은 구글이나 카카오로 시작한 뒤에 남길 수 있습니다.
등록된 댓글이 없습니다.