728x90
반응형

참고영상: https://www.youtube.com/watch?v=SfPWCKTHzE4

 

소스코드 미리보기

더보기
import pygame
import random


colors = [
    (0,0,0),
    (0,255,255), #I - Cyan
    (0,0,255), #Reverse L - Blue
    (255,127,0), #L - Orange
    (255,255,0), #Block - Yellow
    (0,255,0), #S - Green
    (128,0,128), #T - Purple
    (255,0,0), #Reverse S - Red
]

class Figure:
    x = 0
    y = 0

    Figures = [
        [[1,5,9,13], [4,5,6,7]], #I
        [[0,1,4,8], [0,4,5,6], [1,5,9,8], [4,5,6,10]], #Reverse L
        [[0,1,5,9], [4,5,6,8], [0,4,8,9], [2,4,5,6]], #L
        [[1,2,5,6]], #Block
        [[6,7,9,10], [1,5,6,10]], #S
        [[1,4,5,6], [1,4,5,9], [4,5,6,9], [1,5,6,9]], #T
        [[4,5,9,10], [2,6,5,9]], #Reverse S
    ]

    def __init__(self, x_coord, y_coord):
        self.x = x_coord
        self.y = y_coord
        self.type = random.randint(0, len(self.Figures)-1)
        self.color = colors[self.type+1]
        self.rotation = 0

    def image(self):
        return self.Figures[self.type][self.rotation]

    def rotate(self):
        self.rotation = (self.rotation + 1) % len(self.Figures[self.type])

class Tetris:
    height = 0
    width = 0
    field = []
    score = 0
    state = "start"
    Figure = None
    Next = None

    def __init__(self, _height, _width):
        self.height = _height
        self.width = _width
        self.field = []
        self.score = 0
        self.state = "start"
        for i in range(_height):
            new_line = []
            for j in range(_width):
                new_line.append(0)
            self.field.append(new_line)

        self.new_next()
        self.new_figure()

    def new_figure(self):
        self.Figure = Figure(6,0)
        self.Figure.type = self.Next.type
        self.Figure.color = colors[self.Figure.type+1]
        self.new_next()

    def new_next(self):
        self.Next = Figure(30,0)
        #self.Next.type = random.randint(0, len(self.Next.Figures)-1)

    def go_down(self):
        self.Figure.y += 1
        if self.intersects():
            self.Figure.y -= 1
            self.freeze()

    def side(self, dx):
        old_x = self.Figure.x
        edge = False
        for i in range(4):
            for j in range(4):
                p = i*4 + j
                if p in self.Figure.image():
                    if j + self.Figure.x + dx > self.width -1 or \
                        j + self.Figure.x + dx < 0:
                        edge = True
        if not edge:
            self.Figure.x += dx
        if self.intersects():
            self.Figure.x = old_x
    
    def left(self):
        self.side(-1)
    
    def right(self):
        self.side(1)

    def down(self):
        while not self.intersects():
            self.Figure.y += 1
        self.Figure.y -= 1
        self.freeze()

    def rotate(self):
        old_rotation = self.Figure.rotation
        self.Figure.rotate()
        if self.intersects():
            self.Figure.rotation = old_rotation
    
    def intersects(self):
        intersection = False
        try:
            for i in range(4):
                for j in range(4):
                    p = i*4 + j
                    if p in self.Figure.image():
                        if i+self.Figure.y > self.height-1 or \
                            i+self.Figure.y < 0 or \
                            self.field[i+self.Figure.y][j+self.Figure.x] > 0:
                            intersection = True
        except:
            intersection = True
        return intersection

    def freeze(self):
        for i in range(4):
            for j in range(4):
                 p = i*4 + j
                 if p in self.Figure.image():
                     self.field[i+self.Figure.y][j+self.Figure.x] = self.Figure.type+1

        self.break_lines()
        self.new_figure()
        if self.intersects():
            self.state = "gameover"

    def break_lines(self):
        lines = 0
        for i in range(1, self.height):
            zeros = 0
            for j in range(self.width):
                if self.field[i][j] == 0:
                    zeros += 1
            if zeros == 0:
                lines += 1
                for i2 in range(i, 1, -1):
                    for j in range(self.width):
                        self.field[i2][j] = self.field[i2-1][j]

        self.score += lines ** 2

def paused():
    pause = True

    while pause:
        text_pause = gameover_font.render("Paused", True, (255,215,0))
        screen.blit(text_pause, [90,250])
        pygame.display.flip()
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    pause = False


pygame.init()
screen = pygame.display.set_mode((380,670))
pygame.display.set_caption("Tetris")

done = False
pause = False
fps = 5
clock = pygame.time.Clock()
zoom = 20
downTick = 0
downCooldown = 0

game = Tetris(30,15)
pressing_down = False
pressing_left = False
pressing_right = False

BLACK = (0,0,0)
WHITE = (255,255,255)
GRAY = (128,128,128)

while not done:
    if game.state == "start":
        downTick += clock.get_time()
        if downTick > 500:
            downTick = 0
        if downTick == 0:
            game.go_down()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                game.rotate()
            if event.key == pygame.K_DOWN:
                game.down()
            if event.key == pygame.K_LEFT:
                game.left()
                pressing_left = True
            if event.key == pygame.K_RIGHT:
                game.right()
                pressing_right = True
            if event.key == pygame.K_ESCAPE:
                if game.state == "gameover":
                    done = True
                else:
                    paused()


        if event.type == pygame.KEYUP:
            if event.key == pygame.K_DOWN:
                pressing_down = False
            if event.key == pygame.K_LEFT:
                pressing_left = False
            if event.key == pygame.K_RIGHT:
                pressing_right = False

    if pressing_left:
        game.left()
    if pressing_right:
        game.right()

    screen.fill(color=BLACK)
    for i in range(game.height):
        for j in range(game.width):
            if game.field[i][j] == 0:
                color = GRAY
                border = 1
            else:
                color = colors[game.field[i][j]]
                border = 0
            pygame.draw.rect(screen, color, [30+j*zoom, 30+i*zoom, zoom, zoom], border)
            pygame.draw.rect(screen, GRAY, [30+j*zoom, 30+i*zoom, zoom, zoom], 1)

    if game.Figure is not None:
        for i in range(4):
            for j in range(4):
                p = i*4 + j
                #pygame.draw.rect(screen, WHITE, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)
                if p in game.Figure.image():
                    pygame.draw.rect(screen, game.Figure.color, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom])
                    #pygame.draw.rect(screen, GRAY, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)
    if game.Next is not None:
        for i in range(4):
            for j in range(4):
                p = i*4 + j
                if p in game.Next.image():
                    pygame.draw.rect(screen, game.Next.color, [30+(j+game.Next.x)*zoom/2, 30+(i+game.Next.y)*zoom/2, zoom/2, zoom/2])

    gameover_font = pygame.font.SysFont("Clibri", 65, True, False)
    text_gameover = gameover_font.render("Game Over!", True, (255,215,0))
    text_pressEsc = gameover_font.render("Press Esc", True, (255,215,0))

    if game.state == "gameover":
        screen.blit(text_gameover, [30,250])
        screen.blit(text_pressEsc, [60,315])

    score_font = pygame.font.SysFont("Clibri", 25, True, False)
    text_score = score_font.render("Score: " + str(game.score), True, WHITE)
    screen.blit(text_score, [0,0])

    pygame.display.flip()
    clock.tick(fps)


pygame.quit()

 

 

서론

 

Pygame 공부하기 세 번째 Tetris 만들기

 

참고한 영상에서의 Tetris 구현방식을 요약하면 다음과 같다.

- 블록의 데이터를 4x4 행렬과 같이 표시

- 블록을 그리는 것은 정확한 좌표가 아닌 Grid를 채우는 방식

- Grid의 칸에 색 데이터를 저장 ex) 0=빈칸, 1=하늘색...

- 충돌판정은 Grid의 색 데이터가 0이 아니면 블록이 있는 것으로 판정

- 줄을 없앨 때는 위쪽 줄의 데이터를 아래쪽으로 내림

 

코드 설명

Array: colors

더보기
colors = [
    (0,0,0),
    (0,255,255), #I - Cyan
    (0,0,255), #Reverse L - Blue
    (255,127,0), #L - Orange
    (255,255,0), #Block - Yellow
    (0,255,0), #S - Green
    (128,0,128), #T - Purple
    (255,0,0), #Reverse S - Red
]

Figure에 들어갈 색깔을 저장한다.

 

Class: Figure

더보기
class Figure:
    x = 0
    y = 0

    Figures = [
        [[1,5,9,13], [4,5,6,7]], #I
        [[0,1,4,8], [0,4,5,6], [1,5,9,8], [4,5,6,10]], #Reverse L
        [[0,1,5,9], [4,5,6,8], [0,4,8,9], [2,4,5,6]], #L
        [[1,2,5,6]], #Block
        [[6,7,9,10], [1,5,6,10]], #S
        [[1,4,5,6], [1,4,5,9], [4,5,6,9], [1,5,6,9]], #T
        [[4,5,9,10], [2,6,5,9]], #Reverse S
    ]

    def __init__(self, x_coord, y_coord):
        self.x = x_coord
        self.y = y_coord
        self.type = random.randint(0, len(self.Figures)-1)
        self.color = colors[self.type+1]
        self.rotation = 0

    def image(self):
        return self.Figures[self.type][self.rotation]

    def rotate(self):
        self.rotation = (self.rotation + 1) % len(self.Figures[self.type])

 

블록의 모양에 대한 Class

Array: Figures

각 블록의 회전된 모양에 대한 데이터를 갖고 있는 배열

아래와 같은 4x4 행렬의 모습으로 생각할 수 있음

Figures[type][rotation]과 같은 모양

0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15

 

Func: __init__

Class의 init 함수

초기 x, y 정보를 받고 (x,y는 Grid에서의 좌표)

type을 임의로 설정

color를 colors의 type+1번째로 설정 (colors의 첫번째 element는 (0,0,0); 빈칸)

rotation을 기본(0)으로 설정

 

Func: image

Figure의 모양에 대한 배열을 반환

 

Func: rotate

rotation을 바꿈

나머지 연산을 이용해서 각 type에 따라 rotation을 변경할 수 있도록 함

 

Class: Tetris

더보기
class Tetris:
    height = 0
    width = 0
    field = []
    score = 0
    state = "start"
    Figure = None
    Next = None

    def __init__(self, _height, _width):
        self.height = _height
        self.width = _width
        self.field = []
        self.score = 0
        self.state = "start"
        for i in range(_height):
            new_line = []
            for j in range(_width):
                new_line.append(0)
            self.field.append(new_line)

        self.new_next()
        self.new_figure()

    def new_figure(self):
        self.Figure = Figure(6,0)
        self.Figure.type = self.Next.type
        self.Figure.color = colors[self.Figure.type+1]
        self.new_next()

    def new_next(self):
        self.Next = Figure(30,0)
        #self.Next.type = random.randint(0, len(self.Next.Figures)-1)

    def go_down(self):
        self.Figure.y += 1
        if self.intersects():
            self.Figure.y -= 1
            self.freeze()

    def side(self, dx):
        old_x = self.Figure.x
        edge = False
        for i in range(4):
            for j in range(4):
                p = i*4 + j
                if p in self.Figure.image():
                    if j + self.Figure.x + dx > self.width -1 or \
                        j + self.Figure.x + dx < 0:
                        edge = True
        if not edge:
            self.Figure.x += dx
        if self.intersects():
            self.Figure.x = old_x
    
    def left(self):
        self.side(-1)
    
    def right(self):
        self.side(1)

    def down(self):
        while not self.intersects():
            self.Figure.y += 1
        self.Figure.y -= 1
        self.freeze()

    def rotate(self):
        old_rotation = self.Figure.rotation
        self.Figure.rotate()
        if self.intersects():
            self.Figure.rotation = old_rotation
    
    def intersects(self):
        intersection = False
        try:
            for i in range(4):
                for j in range(4):
                    p = i*4 + j
                    if p in self.Figure.image():
                        if i+self.Figure.y > self.height-1 or \
                            i+self.Figure.y < 0 or \
                            self.field[i+self.Figure.y][j+self.Figure.x] > 0:
                            intersection = True
        except:
            intersection = True
        return intersection

    def freeze(self):
        for i in range(4):
            for j in range(4):
                 p = i*4 + j
                 if p in self.Figure.image():
                     self.field[i+self.Figure.y][j+self.Figure.x] = self.Figure.type+1

        self.break_lines()
        self.new_figure()
        if self.intersects():
            self.state = "gameover"

    def break_lines(self):
        lines = 0
        for i in range(1, self.height):
            zeros = 0
            for j in range(self.width):
                if self.field[i][j] == 0:
                    zeros += 1
            if zeros == 0:
                lines += 1
                for i2 in range(i, 1, -1):
                    for j in range(self.width):
                        self.field[i2][j] = self.field[i2-1][j]

        self.score += lines ** 2

사실상 본체

Variables

height: 테트리스 칸의 행 갯수

width: 테트리스 칸의 열 갯수

field: 테트리스 칸 정보를 저장하는 배열 (Grid)

score: 점수

state: 현재 게임 상태 (start, gameover)

Figure: 현재 조작하는 블록

Next: 다음에 나올 블록

 

Func: __init__

각 변수를 초기화하고

반복문으로 field를 모두 0으로 만든다. (모두 빈칸)

현재 블록과 다음 블록을 생성한다.

 

Func: new_figure

새로운 블록을 중간쯤에 생성하는 함수; 영상과는 조금 다르다

type은 처음에 생성했던 다음 블록의 type을 복사하고 이에 따른 color를 지정한다.

이후 다시 한번 다음 블록을 생성(new_next)

 

Func: new_next

새로운 다음 블록을 생성하는 함수; 영상에는 없는 새로운 함수

 

Func: go_down

프레임마다 실행되는 블록을 한칸 내리는 함수

intersects가 True이면 블록이 겹쳤다는 것이니 한 칸 올리고 블록을 고정한다(freeze).

 

Func: side

현재 블록을 좌우로 옮기는 함수

옮길 수 없을 때를 위해 old_x를 저장

현재 블록의 모든 사각형에 대하여 움직였을 때의 x좌표가 field를 벗어난다면 edge = True로 움직이지 않게 한다.

또한 intersects로 다른 블록과의 충돌 판정으로 움직일지 말지 결정한다.

 

Func: left, right

Input이 감지되었을 때 실행되는 함수

 

Func: down

Hard Drop의 일종을 구현하는 함수

intersects(충돌판정)가 True가 되기 전까지 블록을 내린다.

intersects가 True이면 블록이 겹쳤다는 것이니 한 칸 올리고 블록을 고정한다(freeze).

 

Func: rotate

블록을 회전시키는 함수

side와 마찬가지로 회전할 수 없을 때를 위해 old_rotation을 저장

Class Figure의 rotate 함수를 실행하고 intersects=True라면 원래 회전상태로 돌아온다.

 

Func: intersects

블록 간 충돌 판정을 반환하는 함수

현재 블록의 모든 사각형에 대하여 y좌표가 height-1보다 크거나 (바닥)

y좌표가 0보다 작거나 (천장)

해당 field의 색 0이 아니면 (블록이 있음)

intersection=True로 한다.

 

Func: freeze

블록을 고정하고 각 데이터를 맞는 field의 x,y에 저장하는 함수

이때 줄을 없앨지 확인하고

새로운 블록을 생성한다.

만약 intersects=True이면 (블록을 놓을 곳이 없어서 생성된 블록이 충돌하는 경우)

gamestate=gameover로 만든다.

 

Func: break_lines

줄을 없애고 점수를 추가하는 함수

한 줄의 모든 field의 값이 0이 아니면 field 전체를 한칸 내린다. (field[i][j] = field[i-1][j])

점수는 없앤 줄의 제곱만큼 추가하도록 했다. (변경가능)

 

Func: pause

더보기
def paused():
    pause = True

    while pause:
        text_pause = gameover_font.render("Paused", True, (255,215,0))
        screen.blit(text_pause, [90,250])
        pygame.display.flip()
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    pause = False

영상에는 없는 새로운 함수; 일시정지하는 함수

Esc를 누르면 pause=True로 하고 Paused Text를 띄운다.

다시 Esc를 누르면 pause=False로 한다.

 

Program Initialize

pygame.init() - pygame 시작

screen = pygame.display.set_mode((380,670)) - screen의 크기를 지정

pygame.display.set_caption - 창의 제목

 

done - Program Loop 변수

pause - 일시정지 변수

fps - 초당 프레임

clock = pygame.time.Clock() - 시간 관련

zoom - 테트리스 칸의 크기

downTick - 이만큼의 ms가 지나면 블록이 한칸 내려감

 

game = Tetris(30,15) - 인스턴스 생성

 

키를 계속 누르고 있음을 표현

pressing_down

pressing_left

pressing_right

 

BLACK, GRAY, WHITE - 미리 지정해둔 color 배열

 

Program Loop

done=True일 때까지 반복한다.

 

블록 한칸 내리기

game의 state가 start일 때 downTick에 time을 더하고 500이 넘으면 초기화한 후 블록을 한칸 내림

    if game.state == "start":
        downTick += clock.get_time()
        if downTick > 500:
            downTick = 0
        if downTick == 0:
            game.go_down()

 

Keyboard Input

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                game.rotate()
            if event.key == pygame.K_DOWN:
                game.down()
            if event.key == pygame.K_LEFT:
                game.left()
                pressing_left = True
            if event.key == pygame.K_RIGHT:
                game.right()
                pressing_right = True
            if event.key == pygame.K_ESCAPE:
                if game.state == "gameover":
                    done = True
                else:
                    paused()


        if event.type == pygame.KEYUP:
            if event.key == pygame.K_DOWN:
                pressing_down = False
            if event.key == pygame.K_LEFT:
                pressing_left = False
            if event.key == pygame.K_RIGHT:
                pressing_right = False

    if pressing_left:
        game.left()
    if pressing_right:
        game.right()

KEYDOWN이면 pressing=True로 하여 계속 누르고 있을 때 행동을 지속

KEYUP 이면 pressing=False로 하여 행동을 멈춤

 

Grid 그리기

field에 저장되어 있는 데이터에 따라 rect를 그려 표현

    screen.fill(color=BLACK)
    for i in range(game.height):
        for j in range(game.width):
            if game.field[i][j] == 0:
                color = GRAY
                border = 1
            else:
                color = colors[game.field[i][j]]
                border = 0
            pygame.draw.rect(screen, color, [30+j*zoom, 30+i*zoom, zoom, zoom], border)
            pygame.draw.rect(screen, GRAY, [30+j*zoom, 30+i*zoom, zoom, zoom], 1)

현재 블록 그리기

    if game.Figure is not None:
        for i in range(4):
            for j in range(4):
                p = i*4 + j
                #pygame.draw.rect(screen, WHITE, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)
                if p in game.Figure.image():
                    pygame.draw.rect(screen, game.Figure.color, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom])
                    #pygame.draw.rect(screen, GRAY, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)

다음 블록 그리기

    if game.Next is not None:
        for i in range(4):
            for j in range(4):
                p = i*4 + j
                if p in game.Next.image():
                    pygame.draw.rect(screen, game.Next.color, [30+(j+game.Next.x)*zoom/2, 30+(i+game.Next.y)*zoom/2, zoom/2, zoom/2])

Gameover Text

    gameover_font = pygame.font.SysFont("Clibri", 65, True, False)
    text_gameover = gameover_font.render("Game Over!", True, (255,215,0))
    text_pressEsc = gameover_font.render("Press Esc", True, (255,215,0))

    if game.state == "gameover":
        screen.blit(text_gameover, [30,250])
        screen.blit(text_pressEsc, [60,315])

Score Text

    score_font = pygame.font.SysFont("Clibri", 25, True, False)
    text_score = score_font.render("Score: " + str(game.score), True, WHITE)
    screen.blit(text_score, [0,0])

 

Screen update by fps

    pygame.display.flip()
    clock.tick(fps)

 

'작업일지 > Pygame' 카테고리의 다른 글

Pygame #2 - Space Invader  (0) 2022.05.05
Pygame #1 - Installation  (0) 2022.05.02