[pygame] 테트로미노 - 테트리스 클론 게임 - 1

서희찬·2021년 4월 18일
1
post-thumbnail

자, 클론할 게임 중 제일 인기있고 프로그래밍 게임개발의 기본이라고 할 수 있는 테트리스를 클론해볼 시간이다 .

따로 테트리스에 대해서 설명해주지 않아도 다들 알지 않는가!

용어 정리

코드가 길어질예정이니 미리 용어를 정리하고 시작하자

  • Board : 10*20 공간으로 되어있고 블록이 떨어져서 채우는 공간이다.
  • Box : 보드의 정사각형 하나의 단위를 말한다.
  • Piece : 화면 위에서 떨어지며 플레이어가 이 피스를 회전시키거나 위치를 잡을 수 있다.
    각 피스는 형태가달고 4개의 상자로 구성된다.
  • shape : T,S,Z,J,L,I,O 형태가 있다.
  • template : 형태 데이터 구조의 리스트이며, 형태를 회전 시켰을 때 만들어질 수 있는 모든 모양을 나타낸다.
    S_SHAPE_TEMPLATE 같은 변수에 저장한다.
  • Landed : 착지~ 하면 다음 피스가 내려온다.

시작하즈앙...

셋업코드

import random, time, pygame, sys
from pygame.locals import *

FPS = 25
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
BOXSIZE = 20
BOARDWIDTH = 10
BOARDHEIGHT = 20
BLANK = '.'

BLANK 상수는 보드 데이터 구조에서 빈칸을 나타낼 떄 사용한다.

키를 누르고 있는 동안의 타이밍 설정 상수

MOVESIDEWAYSFREQ = 0.15
MOVEDOWNFREQ = 0.1

플레이어가 한번씩 키를 누르는것이 아니라 계속해서 키를 누르는경우 MOVESIDEWAYSFREQ 값을 설정하여 왼쪽 화살표 키나 오른쪽 화살표키를 계속 누르고 있는 경우 한 번씩 키를 누른 것처럼 한 칸씩 옆으로 바로 이동하도록 한다.
하나는 옆 하나는 아래이당~

셋업 코드 추가

XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2)
TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5

코드그대로당..

이렇당

#               R    G    B
WHITE       = (255, 255, 255)
GRAY        = (185, 185, 185)
BLACK       = (  0,   0,   0)
RED         = (155,   0,   0)
LIGHTRED    = (175,  20,  20)
GREEN       = (  0, 155,   0)
LIGHTGREEN  = ( 20, 175,  20)
BLUE        = (  0,   0, 155)
LIGHTBLUE   = ( 20,  20, 175)
YELLOW      = (155, 155,   0)
LIGHTYELLOW = (175, 175,  20)

BORDERCOLOR = BLUE
BGCOLOR = BLACK
TEXTCOLOR = WHITE
TEXTSHADOWCOLOR = GRAY
COLORS      = (     BLUE,      GREEN,      RED,      YELLOW)
LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW)
assert len(COLORS) == len(LIGHTCOLORS) # each color must have light color

각 피스의 색과 하이라이트도 정의 해주었다.
4가지 색은 COLORS 튜플에 저장하고 밝은 색은 LIGHTCOLORS 에 저장한다.

이벤트 루프 처리

TEMPLATEWIDTH = 5
TEMPLATEHEIGHT = 5

S_SHAPE_TEMPLATE = [['.....',
                    '.....',
                    '..OO.',
                    '.OO..',
                    '.....'],
                   ['.....',
                    '..O..',
                    '..OO.',
                    '...O.',
                    '.....']]

Z_SHAPE_TEMPLATE = [['.....',
                    '.....',
                    '.OO..',
                    '..OO.',
                    '.....'],
                   ['.....',
                    '..O..',
                    '.OO..',
                    '.O...',
                    '.....']]

I_SHAPE_TEMPLATE = [['..O..',
                    '..O..',
                    '..O..',
                    '..O..',
                    '.....'],
                   ['.....',
                    '.....',
                    'OOOO.',
                    '.....',
                    '.....']]

O_SHAPE_TEMPLATE = [['.....',
                    '.....',
                    '.OO..',
                    '.OO..',
                    '.....']]

J_SHAPE_TEMPLATE = [['.....',
                    '.O...',
                    '.OOO.',
                    '.....',
                    '.....'],
                   ['.....',
                    '..OO.',
                    '..O..',
                    '..O..',
                    '.....'],
                   ['.....',
                    '.....',
                    '.OOO.',
                    '...O.',
                    '.....'],
                   ['.....',
                    '..O..',
                    '..O..',
                    '.OO..',
                    '.....']]

L_SHAPE_TEMPLATE = [['.....',
                    '...O.',
                    '.OOO.',
                    '.....',
                    '.....'],
                   ['.....',
                    '..O..',
                    '..O..',
                    '..OO.',
                    '.....'],
                   ['.....',
                    '.....',
                    '.OOO.',
                    '.O...',
                    '.....'],
                   ['.....',
                    '.OO..',
                    '..O..',
                    '..O..',
                    '.....']]

T_SHAPE_TEMPLATE = [['.....',
                    '..O..',
                    '.OOO.',
                    '.....',
                    '.....'],
                   ['.....',
                    '..O..',
                    '..OO.',
                    '..O..',
                    '.....'],
                   ['.....',
                    '.....',
                    '.OOO.',
                    '..O..',
                    '.....'],
                   ['.....',
                    '..O..',
                    '.OO..',
                    '..O..',
                    '.....']]

.은 빈칸을 나타내고 o는 상자를 나태낸다.


PIECES = {'S': S_SHAPE_TEMPLATE,
          'Z': Z_SHAPE_TEMPLATE,
          'J': J_SHAPE_TEMPLATE,
          'L': L_SHAPE_TEMPLATE,
          'I': I_SHAPE_TEMPLATE,
          'O': O_SHAPE_TEMPLATE,
          'T': T_SHAPE_TEMPLATE}

PIECES 변수는 모든 템플리트를 저장하는 딕셔너리이다.
각 템플리트는 하나의 형태가 회전하면서 가질 수 있는 모든 모양을 가지고 있다!
그러므로 PIECES 변수는 테트로미노가 사용할 수 있는 모든 형태와 회전한 모양을 가지게 된다.

따라서 이 게임이 가지는 모든 형태에 대한 데이터 구조가된다.

main()

def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH,WINDOWHEIGHT))
    BASICFONT = pygame.font.Font('freesansbold.ttf',18)
    BIGFONT = pygame.font.Font('freesansbold.ttf',100)
    pygame.display.set_caption("Chans Tetromino")

    showTextScreen('Tetromino')
    

기본적인 설정을 해준다.

    while True: #game loop
        if random.randint(0,1) == 0:
            pygame.mixer.music.load('tetrisb.mid')
        else:
            pygame.mixer.music.load('tetrisc.mid')
        pygame.mixer.music.load(-1,0.0)
        runGame()
        pygame.mixer.music.stop()
        showTextScreen('Game Over')

실제 게임을 수행하는 코드는 runGame()에 있다.
main() 함수는 무작위로 노래 틀어주고 게임이 끝나면 게임오버~ 뜨게해준당

새 게임 시작


def runGame():
    #게임 시작 부분에서 변수 설정
    board = getBlankBoard()
    lastMoveDownTime = time.time()
    lastMoveSidewaysTime = time.time()
    lastFallTime = time.time()
    movingDown = False 
    movingLeft = False
    movingRight = False 
    score = 0 
    level, fallFreq = calculateLevelAndFallFreq(score)

    fallingPiece = getNewPiece()
    nextPiece = getNewPiece()

시작전에 모두 초기화한다.
FallingPiece 는 현재 떨어지고 있는 플레이어가 회전시킬 수 있는 피스로 설정한다.
NextPiece 는 다음번에 떨어 뜨릴 피스이다.

게임 루프

while True: #game loop
        if fallingPiece == None:
            #떨어지고 있는 피스가 없으면 새 피스가 위에서 떨어지도록 한다.
            fallingPiece = nextPiece
            nextPiece = getNewPiece()
            lastFallTime = time.time #reset lastFallTime 

            if not isValidPosition(board,fallingPiece):
                return # end game 
        
        checkForQuit()

이벤트 처리 루프

        for event in pygame.event.get(): #Event loop
            if event.tpye == KEYUP:

이벤트 처리 루프는 플레이어가 떨어지는 피스를 회전시키거나 피스를 이동하거나 잠시 게임을 멈출 때 발생하는 이벤트를 처리한다.

게임 잠시 멈추기

                if(event.key == K_p):
                    #게임을 잠시 멈춘다. 
                    DISPLAYSURF.fill(BGCOLOR)
                    pygame.mixer.music.stop()
                    showTextScreen('Paused') # 키를 누를 때까지 멈춘다.
                    pygame.mixer.music.play(-1,0.0)
                    lastFallTime = time.time()
                    lastMoveDownTime = time.time()
                    lastMoveSidewaysTime = time.time()

p를 누르면 게임을 잠깐 멈출 수 있다.
이때 보드를 가려야한다!
그렇지 않으면 플레이어가 생각을 할 수 있기 때문이다!! DISPLAYSURF.fill 을 통해 화면을 가리고 음악을 멈춘다.

플레이어가 키를 누르면 showTextScreen은 반환되어 게임을 재개한다.

사용자 입력을 처리하기 위해 움직임 관련 변수 사용하기

                elif (event.key == K_LEFT or event.key == K_a):
                    movingLeft = False
                elif (event.key == K_RIGHT or event.key == K_d):
                    movingRight = False
                elif (event.key == K_DOWN or event.key == K_s):
                    movingDown = False

키에서 손을 땠을때 False 값으로 설정한다!
이 의미는 플레이어가 더 이상 그 방향으로 움직이려고 하지 않음을 의미한다.

밀거나 회전했을 때 유효한지 확인하기

            elif event.type == KEYDOWN:
                # moving the piece sideways 
                if(event.key == K_LEFT or event.key == K_a) and isValidPosition(board,fallingPiece,adjX=-1):
                    fallingPiece['x'] -= 1
                    movingLeft = True
                    movingRight = False 
                    lastMoveSidewaysTime = time.time()
            elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1):
                    fallingPiece['x'] += 1
                    movingRight = True
                    movingLeft = False
                    lastMoveSidewaysTime = time.time()

adjx 에 -1값을 주면 왼쪽으로 한 칸 이동한 위치에서 피스의 위치가 적절한지를 검사한다.
+1이면 오른쪽 y는 위아래에 대해 !

                #회전
                elif(event.key == K_UP or event.key == K_w):
                    fallingPiece['rotation'] = (fallingPiece['rotation']+1) % len(PIECES[fallingPiece['shape']])

회전시킨당

if not isValidPosition(board,fallingPiece):
                        fallingPiece['rotation'] = (fallingPiece['rotation']-1 % len(PIECES[fallingPiece['shape']]))

만약 회전을 못하는 상황이라면 -1을 해서 원래의 모양으로 돌려놔야한다.

elif(event.key == K_q): #반대방향 회전 
                    fallingPiece['rotation'] = (fallingPiece['rotation']-1)%len(PIECES[fallingPiece['shape']])
                    if not isValidPosition(board,fallingPiece):
                        fallingPiece['rotation'] = (fallingPiece['rotation']+1)%len(PIECES[fallingPiece['shape']])

q를 누르면 반대 방향으로 돌린다.

                #피스를 빨리 떨어뜨린다
                elif(event.key == K_DOWN or event.key == K_s):
                    movingDown = True
                    if isValidPosition(board,fallingPiece,adjY=1):
                        fallingPiece['y']+=1
                    lastMoveDownTime = time.time()

아래로 누르면 빨리 내려간다~

바닥 찾아내기

                # 현재 피스를 바닥으로 떨어뜨린다.
                elif event.key = K_SPACE:
                    movingDown = False
                    movingRight = False
                    movingLeft = False
                    for i in range(1,BOARDHEIGHT):
                        if not isValidPosition(board,fallingPiece,adjY=1):
                            break
                    fallingPiece['y']+=i-1

플레이어가 스페이스 키를 치면 현재 피스는 바로 아래로 떨어진다.
우선 프로그램은 아래로 착지하기위해 얼마나 많은 공간이 남았는지 확인해야한다.

isValidPosition 이 False를 반환하면 더 이상 아래로 내려갈수 없고 True를 반환하면 피스가 한 칸 아래로 이동할 수 있음을 의미한다.

키를 누르고 있는 동안 움직이기

        # 사용자의 입력에 따라 피스 움직이기
        if(movingLeft or movingRight) and time.time()-lastMoveSidewaysTime > MOVESIDEWAYSFREQ:
            if movingLeft and isValidPosition(board,fallingPiece,adjX=-1):
                fallingPiece['x'] -= 1
            elif movingRight and isValidPosition(board,fallingPiece,adjX=1):
                fallingPiece['x'] += 1
            lastMoveSidewaysTime = time.time()
            
      	     if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1):
            fallingPiece['y'] += 1
            lastMoveDownTime = time.time()

피스가 "자연스럽게" 떨어지도록 만들기

        #떨어질 시간이 되면 피스를 떨어뜨린다.
        if time.time() -lastFallTime>fallFreq:
            #피스가 착지했는지 검사한다.
            if not isValidPosition(board,fallingPiece,adjY=1):
                #떨어지는 피스가 칙지했으면 보드에 둔다.
                addToBoard(board,fallingPiece)
                score += removeCompleteLines(board)
                level, fallFreq = calculateLevelAndFallFreq(score)
                fallingPiece = None 
            else :
                #피스가 아직 착지하지 않았으면 피스를 아래로 움직인다.
                fallingPiece['y'] += 1
                lastFallTime = time.time()

스크린에 모두 그리기

        #모두 그리쟈
        DISPLAYSURF.fill(BGCOLOR)
        drawBoard(board)
        drawStatus(score,level)
        drawNextPiece(nextPiece)
        if fallingPiece != None :
            drawPiece(fallingPiece)

        pygame.display.update()
        FPSCLOCK.tick(FPS)

모두 그린다.

이제 남은 함수들은 다음 포스트에서 정의하자 ...

profile
부족한 실력을 엉덩이 힘으로 채워나가는 개발자 서희찬입니다 :)

0개의 댓글