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

서희찬·2021년 4월 19일
0

이제 전체 코드를 보여주겠다.

# Tetromino (a Tetris clone)
# By Al Sweigart al@inventwithpython.com
# http://inventwithpython.com/pygame
# Released under a "Simplified BSD" license

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

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

MOVESIDEWAYSFREQ = 0.15
MOVEDOWNFREQ = 0.1

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

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..',
                    '.....']]

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}

def main():
   global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
   pygame.init()
   FPSCLOCK = pygame.time.Clock()
   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.play(-1,0.0)
       runGame()
       pygame.mixer.music.stop()
       showTextScreen('Game Over')

def runGame():
   # setup variables for the start of the game
   board = getBlankBoard()
   lastMoveDownTime = time.time()
   lastMoveSidewaysTime = time.time()
   lastFallTime = time.time()
   movingDown = False # note: there is no movingUp variable
   movingLeft = False
   movingRight = False
   score = 0
   level, fallFreq = calculateLevelAndFallFreq(score)

   fallingPiece = getNewPiece()
   nextPiece = getNewPiece()

   while True: # game loop
       if fallingPiece == None:
           # No falling piece in play, so start a new piece at the top
           fallingPiece = nextPiece
           nextPiece = getNewPiece()
           lastFallTime = time.time() # reset lastFallTime

           if not isValidPosition(board, fallingPiece):
               return # can't fit a new piece on the board, so game over

       checkForQuit()
       for event in pygame.event.get(): # event handling loop
           if event.type == KEYUP:
               if (event.key == K_p):
                   # Pausing the game
                   DISPLAYSURF.fill(BGCOLOR)
                   pygame.mixer.music.stop()
                   showTextScreen('Paused') # pause until a key press
                   pygame.mixer.music.play(-1, 0.0)
                   lastFallTime = time.time()
                   lastMoveDownTime = time.time()
                   lastMoveSidewaysTime = time.time()
               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

           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()

               # rotating the piece (if there is room to rotate)
               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']])
               elif (event.key == K_q): # rotate the other direction
                   fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
                   if not isValidPosition(board, fallingPiece):
                       fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])

               # making the piece fall faster with the down key
               elif (event.key == K_DOWN or event.key == K_s):
                   movingDown = True
                   if isValidPosition(board, fallingPiece, adjY=1):
                       fallingPiece['y'] += 1
                   lastMoveDownTime = time.time()

               # move the current piece all the way down
               elif event.key == K_SPACE:
                   movingDown = False
                   movingLeft = False
                   movingRight = False
                   for i in range(1, BOARDHEIGHT):
                       if not isValidPosition(board, fallingPiece, adjY=i):
                           break
                   fallingPiece['y'] += i - 1

       # handle moving the piece because of user input
       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()

       # let the piece fall if it is time to fall
       if time.time() - lastFallTime > fallFreq:
           # see if the piece has landed
           if not isValidPosition(board, fallingPiece, adjY=1):
               # falling piece has landed, set it on the board
               addToBoard(board, fallingPiece)
               score += removeCompleteLines(board)
               level, fallFreq = calculateLevelAndFallFreq(score)
               fallingPiece = None
           else:
               # piece did not land, just move the piece down
               fallingPiece['y'] += 1
               lastFallTime = time.time()

       # drawing everything on the screen
       DISPLAYSURF.fill(BGCOLOR)
       drawBoard(board)
       drawStatus(score, level)
       drawNextPiece(nextPiece)
       if fallingPiece != None:
           drawPiece(fallingPiece)

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

def makeTextObjs(text,font,color):
   surf = font.render(text,True,color)
   return surf, surf.get_rect()

def terminate():
   pygame.quit()
   sys.exit()

def checkForKeyPress():
   # KeyUp ㅇㅣ벤트가 발생했는지 이벤트 큐를 찾는다.
   # 키다운 이벤트를 찾아서 이벤트 큐에서 제거한다.
   checkForQuit()

   for event in pygame.event.get([KEYDOWN,KEYUP]):
       if event.type == KEYDOWN:
           continue
       return event.key
   return None 

def showTextScreen(text):
   # 이 함수는 플레이어가 키를 누를때 까지 화면 중간에 커다란 글씨를 그려서 보여준다.

   # 텍스트의 그림자 효과를 그린다.
   titleSurf, titleRect = makeTextObjs(text,BIGFONT,TEXTSHADOWCOLOR)
   titleRect.center = (int(WINDOWWIDTH/2),int(WINDOWHEIGHT/2))
   DISPLAYSURF.blit(titleSurf,titleRect)

   # drawing text
   titleSurf,titleRect = makeTextObjs(text,BIGFONT,TEXTCOLOR)
   titleRect.center = (int(WINDOWWIDTH/2)-3,int(WINDOWHEIGHT/2)-3)
   DISPLAYSURF.blit(titleSurf,titleRect)

   # drawing "Press a Key to Play"
   pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play',BASICFONT,TEXTCOLOR)
   pressKeyRect.center = (int(WINDOWWIDTH/2),int(WINDOWHEIGHT)+100)
   DISPLAYSURF.blit(pressKeySurf,pressKeyRect)

   while checkForKeyPress() == None:
       pygame.display.update()
       FPSCLOCK.tick()

def checkForQuit():
   for event in pygame.event.get(QUIT):
       terminate()
   for event in pygame.event.get(KEYUP):
       if event.key == K_ESCAPE:
           terminate()
       pygame.event.post(event) 

def calculateLevelAndFallFreq(score):
   #점수에 따라 플레이어의 레벨을 계산해서 반환한다.
   #그리고 피스가 얼마나 빨리 떨어져야하는지 계산한다.
   level = int(score/3) + 1
   fallFreq = 0.27 - (level * 0.02)
   return level,fallFreq

def getNewPiece():
   # 무작위로 회전시키고 무작위 색깔을 가진 새 피스를 반환한다.
   shape = random.choice(list(PIECES.keys()))
   newPiece ={
       'shape':shape,
       'rotation':random.randint(0,len(PIECES[shape])-1),
       'x':int(BOARDWIDTH/2) - int(TEMPLATEWIDTH/2),
       'y':-2, # 보드의 위쪽에서 시작한다(좌표는 0 보다 작다)
       'color':random.randint(0,len(COLORS)-1)
   }
   return newPiece

def addToBoard(board,piece):
   # 피스의 위치, 형태, 회전 여부에 따라 보드를 채운다.
   for x in range(TEMPLATEWIDTH):
       for y in range(TEMPLATEHEIGHT):
           if PIECES[piece['shape']][piece['rotation']][y][x] != BLANK:
               board[x + piece['x']][y + piece['y']] = piece['color']

def getBlankBoard():
   #비어 있는 새 보드 데이터 구조를 만들어 반환한다.
   board = []
   for i in range(BOARDWIDTH):
       board.append([BLANK]*BOARDHEIGHT)
   return board 

def isOnBoard(x,y):
   return x>=0 and x< BOARDWIDTH and y<BOARDHEIGHT

def isValidPosition(board,piece,adjX=0,adjY=0):
   #피스가 보드에 있고 충동하지 않았으면 True 를 반환한다.
   for x in range(TEMPLATEWIDTH):
       for y in range(TEMPLATEHEIGHT):
           isAboveBoard = y + piece['y'] + adjY<0
           if isAboveBoard or PIECES[piece['shape']][piece['rotation']][y][x] == BLANK:
               continue
           if not isOnBoard(x+piece['x']+adjX,y+piece['y']+adjY):
               return False
           if board[x+piece['x']+adjX][y+piece['y']+adjY] != BLANK:
               return False
   return True 

def isCompleteLine(board,y):
   #상자로 채워진 줄에 빈칸이 없으면 True를 반환한다.
   for x in range(BOARDWIDTH):
       if board[x][y] == BLANK:
           return False 
   return True 

def removeCompleteLines(board):
   #보드에서 완전하게 완성된 줄을 제거한다. 그리고 다른 줄을 모두 아래로 내리고 몇 개의 행이 완성되었는지 반환한다.
   numLinesRemoved = 0
   y = BOARDHEIGHT - 1 #보드 맨 아랫부분의 y부터 시작한다.
   while y >=0:
       if isCompleteLine(board,y):
           #행을 한 줄 없애고 상자를 모두 한 줄 아래로 내린다.
           for pullDownY in range(y,0,-1):
               for x in range(BOARDWIDTH):
                   board[x][pullDownY] = board[x][pullDownY-1]
           #맨 위의 줄을 빈칸으로 설정한다.
           for x in range(BOARDWIDTH):
               board[x][0] = BLANK
           numLinesRemoved +=1
           #반복문에서 처음에 y는 그대로인데 한 칸 아래로 내린 줄 또한 완성된 줄이라면 그 줄도 없애야한다
       else :
           y -=1 #한 줄씩 위로 올라가면서 검사한다.
   return numLinesRemoved

def convertToPixelCoords(boxx,boxy):
   #주어진 보드 좌표계를 픽셀 좌표계로 변환한다.
   return(XMARGIN +(boxx*BOXSIZE)),(TOPMARGIN+(boxy*BOXSIZE))

def drawBox(boxx,boxy,color,pixelx=None,pixely=None):
   # 보드 좌표계 x,y 좌표에 상자 하나를 그린다 (테트로미노는 4개의 상자로 구성된다)
   #만약 pixelx,pixely 값을 주면 픽셀 좌표계에 상자를 그린다
   if color == BLANK:
       return
   if pixelx == None and pixely == None :
       pixelx,pixely = convertToPixelCoords(boxx,boxy)
   pygame.draw.rect(DISPLAYSURF,COLORS[color],(pixelx+1,pixely+1,BOXSIZE-1,BOXSIZE-1))
   pygame.draw.rect(DISPLAYSURF,LIGHTCOLORS[color],(pixelx+1,pixely+1,BOXSIZE - 4,BOXSIZE-4))

def drawBoard(board):
   #보드 주면에 테두리를 그린다.
   pygame.draw.rect(DISPLAYSURF,BORDERCOLOR,(XMARGIN-3,TOPMARGIN-7,(BOARDWIDTH*BOXSIZE)+8,(BOARDHEIGHT*BOXSIZE)+8),5)

   # 보드의 배경색을 채운다.
   pygame.draw.rect(DISPLAYSURF,BGCOLOR,(XMARGIN,TOPMARGIN,BOXSIZE*BOARDWIDTH,BOXSIZE*BOARDHEIGHT))
   #보드의 각 상자를 그린다.
   for x in range(BOARDWIDTH):
       for y in range(BOARDHEIGHT):
           drawBox(x, y, board[x][y])

def drawStatus(score,level):
   #스코어 텍스트를 그린다.
   scoreSurf = BASICFONT.render("Score : %s" %score,True,TEXTCOLOR)
   scoreRect = scoreSurf.get_rect()
   scoreRect.topleft = (WINDOWWIDTH - 150, 20)
   DISPLAYSURF.blit(scoreSurf,scoreRect)

   #draw level text 
   levelSurf = BASICFONT.render("Level : %s" %level,True,TEXTCOLOR)
   levelRect = levelSurf.get_rect()
   levelRect.topleft = (WINDOWWIDTH - 150,50)
   DISPLAYSURF.blit(levelSurf,levelRect)

def drawPiece(piece,pixelx=None,pixely=None):
   shapeToDraw = PIECES[piece['shape']][piece['rotation']]
   if pixelx == None and pixely == None :
       #특정값으로 지정되지 않았으면 피스 데이터 구조에 저장된 위치를 사용한다.
       pixelx,pixely = convertToPixelCoords(piece['x'],piece['y'])

   # 피스를 구성하는 각 상자들을 그린다.
   for x in range(TEMPLATEWIDTH):
       for y in range(TEMPLATEHEIGHT):
           if shapeToDraw[y][x] != BLANK:
               drawBox(None,None,piece['color'],pixelx+(x*BOXSIZE),pixely+(y*BOXSIZE))


def drawNextPiece(piece):
   nextSurf = BASICFONT.render("NEXT : ",True,TEXTCOLOR)
   nextRect = nextSurf.get_rect()
   nextRect.topleft = (WINDOWWIDTH-120,80)
   DISPLAYSURF.blit(nextSurf,nextRect)
   drawPiece(piece,pixelx=WINDOWWIDTH - 120, pixely=100)

if __name__ == '__main__':
   main()

후.. 길고 긴 테트리스 클론을 끝냈다.

시작 화면이다!

이렇게 플레이하면 된다.

게임 오버하면 이러한 화면이 뜬다!

플레이영상은 다음에 남기겠다!


생각보다.. 어렵다..

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

0개의 댓글