[pygame] 슬라이드 퍼즐 - 2

서희찬·2021년 4월 11일
0
post-thumbnail

이제 함수들을 짜주자 !

IDLE 와 pygame 프로그램 종료하기

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

겨우 두개의 함수를 호출하면서 함수로 만들어 놓은것은 약간 심한 것처럼 보이긴 하지만 그래도 편의상 이게 낫다 !

특정 이벤트 검사하기, pygame의 이벤트 큐에 이벤트 포스팅하기

def checkForQuit():
 for event in pygame.event.get(QUIT): #QUIT 이벤트 가져온다.
     terminate()
 for event in pygame.event.get(KEYUP): 
     if event.key == K_ESCAPE:
         terminate() : # esc keyup event end program
     pygame.event.post(event) #다른 keyup 이벤트 객체는 다시 돌려준다. 

pygame은 내부적으로 Event 객체를 만들어서 추가하는 리스트 데이터 구조가 있다.
이 데이터 구조를 이벤트 큐 라고한다.

아무 파라미터 없이 pygame.event.get()을 호출하면 전체 리스트를 반환한다.
하지만 QUIT과 같은 상수를 넘겨주면 내부 이벤트에 QUIT 이벤트가 있을 때만 이 이벤트를 반환한다.
다른 이벤트들은 다음 호출 때까지 이벤트 큐에 머문다.

pygame의 이벤트 큐는 오직 127개 까지 이벤트 객체만을 저장할 수 있다.
만약 자주 함수를 호출하지 않으면 이벤트 큐가 전부 차버린다.
그리고 새로운 이벤트는 더 이상 이벤트 큐에 들어올 수 없다.

pygame.event.pos()함수는 파라미터로 전달받은 이벤트 객체를 파이게임이벤트 큐의 제일 뒤에 추가한다.
만약.. 이렇게 하지 않는다면 checkForQuit 함수가 키업 이벤트를 모두~ 가져와 처리할 키보드 이벤트가 없게 된다.

게임판 데이터 구조 만들기

def getStartingBoard():
    # 원래대로 정렬된 상태의 게임판 데이터 구조를 반환한다.
    # 예를 들어 BOARDWIDTH BOARDHEIGHT 가 모두 3이면
    # 이 함수는 [[1,4,7],[2,5,8],[3,6,0]] 반환
    counter = 1
    board = []
    for x in range(BOARDWIDTH):
        column = []
        for y in range(BOARDHEIGHT):
            column.append(counter)
            counter += BOARDWIDTH
        board.append(column)
        counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1
    
    board[BOARDWIDTH- 1][BOARDHEIGHT -1] = BLANK
    return board 

칼럼이 보드의 너비만큼 더해지므로 [1,4,7]다음은[2,5,8]이다.
Blank는 빈칸 !

빈칸 위치는 그때그때 찾아낸다.

def getBlankPosition(board):
 # 빈칸 위치의 개임판 x,y 좌표를 반환한다.
 for x in range(BOARDWIDTH):
     for y in range(BOARDHEIGHT):
         if board[x][y] == BLANK:
             return (x,y)

빈칸의 x,y좌표가 필요할 때를 대비해서 필요할때마다 찾을 수 있게 함수를 만들었다.

게임판 데이터 구조를 갱신해서 이동하기

def makeMove(board,move):
 # 이 함수는 이동이 가능한지 검사하지 않는다. 
 blankx,blanky=getBlankPosition(board)

 if move == UP:
     board[blankx][blanky], board[blankx][blanky+1] = board[blankx][blanky+1],board[blankx][blanky] 
     #빈칸을 위로 올리고 숫자였던 카드를 빈칸으로, 빈칸을 숫자로 바꾼다 .
 elif move = DOWN:
     board[blankx][blanky], board[blankx][blanky-1] = board[blankx][blanky-1],board[blankx][blanky]
 elif move == LEFT:
     board[blankx][blanky], board[blankx + 1][blanky] = board[blankx + 1][blanky], board[blankx][blanky]
 elif move == RIGHT:
     board[blankx][blanky], board[blankx - 1][blanky] = board[blankx - 1][blanky], board[blankx][blanky]

move 로 파라미터값에 방향에 따라 빈칸과 숫자가 있던 카드를 바꿔치기 한다 !
board 파리미터가 리스트 자체에 대한 레퍼런스를 가지기 때문에 makeMove()함수는 특별히 반환 값을 돌려줄 필요가 없다.

즉, 함수 안에서 board 값을 바꾸면 makeMove()에 전달한 list 값도 바뀐다.

왜 Assertion을 쓰면 안 될까?

def isValidMove(board,move):
    blankx,blanky = getBlankPosition(board)
    return (move == UP and blanky != len(board[0])-1) or \
           (move == DOWN and blanky != 0) or \
           (move == LEFT and blankx != len(board) - 1) or \
           (move == RIGHT and blankx != 0)

Move 가 가능하다면 True를 반환하고 아니면 False 를 반환한다.

"\" 를 사용하면 줄이 연속되어 있는 것으로 인식한다.

그래서..왜 쓰면 안되는데요..

무작위적이지 않은 움직임 얻기

def getRandomMove(board,lastMove=None):
    #가능한 모든 움직임으로부터 시작한다.
    validMoves =[UP,DOWN,LEFT,RIGHT]

    #움직일 수 없는 경우는 제외한다
    if lastMove == UP or not isValidMove(board,DOWN):
        validMoves.remove(DOWN)
    if lastMove == DOWN or not isValidMove(board, UP):
        validMoves.remove(UP)
    if lastMove == LEFT or not isValidMove(board, RIGHT):
        validMoves.remove(RIGHT)
    if lastMove == RIGHT or not isValidMove(board, LEFT):
        validMoves.remove(LEFT)
    return random.choice(validMoves)

validmove 로 임의로 방향을 선택하도록 한다.
그러나.. 몇가지 제약이 있는데 예를 들어 슬라이드 퍼즐에서 어떤 타일을 밀고 다시 전에 방향으로 밀면 아무 소용없게 된다.
그리고 빈칸이 게임판의 맨 오른쪽 아래에 있다면 타일을 왼쪽이나 위쪽으로 밀 수 없다.

getRandomMove()함수는 이러한 점을 고려하여 움직였던 방향의 반대 방향으로 움직이지 않도록 lastMove에 이전의 움직임을 저장한다.

if문을 통해 이동불가능한 방향은 모두 삭제한 후 이동 가능한 방향 중에서 random으로 choice한다.

타일 좌표를 픽셀 좌표로 변환하기

def getLeftTopOfTile(tileX,tileY):
 left =XMARGIN+(tileX*TILESIZE)+(tileX - 1)
 top = YMARGIN + (tileY*TILESIZE) + (tileY - 1)
 return(left,top) 

게임판 타일의 x,y좌표를 넘겨주면 함수는 픽셀 좌표를 계산하여 게임판 윈도우의 맨 왼쪽 상단을 0,0 으로 했을 때 얼마나 떨어져 있는지 알려준다.

픽셀 좌표를 게임판 좌표로 변환하기

def getSpotClicked(board,x,y):
    # x,y 픽셀 좌표를 게임판 좌표로 변환한다.
    for tileX in range(len(board)):
        for tileY in rnage(len(board[0])):
            left, top = getLeftTopOfTile(tileX,tileY)
            tileRect = pygame.Rect(left,top,TILESIZE,TILESIZE)
            if tileRect.collidepoint(x,y):
                return (tileX,tileY)
    return (None,None)

타일의 가장 왼쪽 상단의 좌표만 있다면 타일을 나타내는 Rect 객체를 만들 수 있다. Rect객체의 Colidepoint 메소드를 써서 해당하는 픽셀 좌표가 Rect객체 안에 들어있는지 확인 가능하다.
만약, 어느 타일 위에도 있지 않다면 None 을 반환한다.

타일 그리기

def drawTile(tilex,tiley,number,adjx=0,adjy=0):
 #게임판의 타일 좌표에 타일을 그린다
 #adjx adjy 의 값으로 타일을 그리는 좌표 조정 가능 
 left, top - getLeftTopOfTile(tilex,tiley)
 pygmae.draw.rect(DISPLAYSURF,TILECOLOR,(left+ajdx,top+adjy,TILESIZE,TILESIZE))
 textSurf = BASICFONT.render(str(numbrt),True ,TEXTCOLOR)
 textRect = textSurf.get_rect()
 textRect.center = left +int(TILESIZE/2) + adjx, top + int(TILESIZE/2)+adjy
 DISPLAYSURF.blit(textSurf,textRect)

숫자가 하나 쓰여 있는 타일을 그린다.
number 파라미터는 타일위이 그릴 숫자를 말한다.
adjx,adjy로 타일의 위치를 조정한다.
이러한 조정 값을 두면 타일을 미는 효과를 그릴 때 편하다.

화면상에 문자열 보여주기

def makeText(text,color,bgcolor,top,left):
    #surface 객체와 Rect 객체를 만들어서 텍스트를 보여준다.
    textSurf = BASICFONT.render(text,True,color,bgcolor)
    textRect = textSurf.get_rect()
    textRect.topleft = (top,left)
    return (textSurf,textRect)

게임판 그리기

def drawBoard(board,message):
    DISPLAYSURF.fill(BGCOLOR)
    if message:
        textSurf, textRect = makeText(message,MESSAGECOLOR,BGCOLOR,5,5)
        DISPLAYSURF.blit(textSurf,textRect)
        for tilex in range(len(board)):
            for tiley in range(len(board[0])):
                if board[tilex][tiley]:
                    drawTile(tilex,tiley, board[tilex][tiley])

게임판 테두리 그리기

    left, top = getLeftTopOfTile(0,0)
    width = BOARDWIDTH * TILESIZE
    height = BOARDHEIGHT * TILESIZE
    pygame.draw.rect(DISPLAYSURF,BORDERCOLOR,(left-5,top-5,width + 11, height+11),4)

게임판 죄표계에서 왼쪽,위쪽 5픽셀 떨어져서 시작한다.
테두리의 너비와 높이는 타일이 가로로 몇 개 있는지 세로로 몇 개 있는지 값을 가져와서 여기에 타일의 크기를 곱해서 결정한다.

사각형 테두리의 두께는 4이다.
따라서 시작점을 5픽셀만큼만 왼쪽과 위쪽으로 옮겨야 타일 위에 겹쳐서 테두리를 그리지 않는다.
그리고 너비와 높이에 11픽셀을 더했는데 이 중 5픽셀은 왼쪽과 위쪽으로 로 옮긴 것 때문에 보정한 값이다.

버튼 그리기

  DISPLAYSURF.blit(RESET_SURF,RESET_RECT)
  DISPLAYSURF.blit(NEW_SURF,NEW_RECT)
  DISPLAYSURF.blit(SOLVE_SURF,SOLVE_RECT)

버튼 위치와 글씨는 바뀌지 않기에 main에서 상수로 지정하였다.

타일 슬라이드 애니메이션

def slideAnimation(board,direction,message,animationSpeed):
   #주의 : 이 함수는 타일의 움직임이 유효한지 체크 x 

   blankx,blanky = getBlankPosition(board)
   if direction == up:
       movex = blankx
       movey = blanky + 1
   elif direction == DOWN :
       movex = blankx 
       movey = blanky - 1
   elif direction == LEFT:
       movex = blankx + 1
       movey = blanky
   elif direction == RIGHT :
       movex = blankx - 1
       movey = blanky 

copy() Surface 메소드

    # 기본 Surface 를 준비한다.
    drawBoard(board,message)
    baseSurf = DISPLAYSURF.copy()
    #baseSurf Surface의 움직이는 타일 위에 빈칸을 그린다.
    moveLeft, moveTop = getLeftTopOfTile(movex,movey)
    pygame.draw.rect(baseSurf,BGCOLOR,(moveLeft,moveTop,TILESIZE,TILESIZE))

copy 메소드는 새 Surface 객체를 반환하는데 이 위에 동일한 이미지를 그려서 반환한다.
하지만 이 둘은 서로다른 Surface 객체이기에 서로 영향을 주지않는다.
baseSurf Surface의 움직일 타일을 빈칸으로 한 번 칠해 주지 않으면 움직이고 있음에도 여전히 타일이 거기 있는것처럼 보일 것이다 !

    for i in range(0,TILESIZE,animationSpeed):
        #타일이 움직이는 것을 애니메이션으로 보여준다
        checkForQuit()
        DISPLAYSURF.blit(baseSurf, (0, 0))
        if direction == UP:
            drawTile(movex, movey, board[movex][movey], 0, -i)
        if direction == DOWN:
            drawTile(movex, movey, board[movex][movey], 0, i)
        if direction == LEFT:
            drawTile(movex, movey, board[movex][movey], -i, 0)
        if direction == RIGHT:
            drawTile(movex, movey, board[movex][movey], i, 0)

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

인접한 두 타일의 거리는 타일 하나의 길이인 TILESIZE와 동일하다.
따라서 for문은 거리 0에서 TILESIZE까지 루프를 돌게 된다.

일반적으로 타일이 움직이는 것을 그리려면 처음에는 타일이 0 픽셀 떨어져 ㅣㅇㅆ다가 그 다음 프레임에는 1픽셀 떨어지고 점점... 이동해야한다 TILESIZE를 80으로 설정해서 너무~ 느리다 ~(2.5초)
(FPS = 30)

그래서 for문에서 0부터 TILESIZE까지 한 프레임에 여러 픽셀씩 움직이도록 한다. 그러면 8씩 움직이므로 1/3초만에 끝난다 !

새 퍼즐 만들기

def generateNewPuzzle(numSlides):
 # 처음 시작의 설정 값을 가지고 , numSlide 값만큼 타일을 움직인다.
 sequence =[]
 board = getStartingBoard()
 drawBoard(board,'')
 pygame.display.update()
 pygame.time.wait(500)
 lastMove = None
 for i in ragne(numSlides):
     move = getRandomMove(board,lastMove)
     slideAnimation(board,move,"Generationg new Puzzle....",animationSpeed=int(TILESIZE/3))
     makeMove(board,move)
     sequence.append(move)
     lastMove = move
 return (board,sequence)

첫 부분은 게임판을 만들고 이를 섞는다 !

numslide 값 만큼 무작위로 타일을 옮긴다.
move에 따라 타일을 움직이는데 move를 미리 만들고 slideAnimation()함수에는 전달만 했기 때문에 에니메이션과정에서 게임판 데이터 구조가 바뀌지 않는다.
MakeMove()를 통해 게임판 데이터 구조에 move를 반영한다.

move를 기록하기위해 sequence에 기록해둔다 !
이유는 모르면 바보~

move를 lastMosve변수에 저장해서 다음번 for문 수행에서 쓰도록 getRandomMove()에게 넘겨준다.
이렇게해야 무작위로 움직일 때 그 이전 동작을 기억하도록 해서 한 동작을 취소하는 동작을 하지 않는다.

보드 리셋 에니메이션

def resetAnimation(board,allMoves):
 #allMoves 의 움직임을 거꾸로 수행
 revAllMoves = allMoves[:]
 revAllMoves.reverse()

 for move in revAllMoves:
     if move == UP:
         oppositeMove = DOWN
     elif move == DOWN:
         oppositeMove = UP
     elif move == RIGHT:
         oppositeMove = LEFT
     elif move == LEFT:
         oppositeMove = RIGHT
     slideAnimation(board,oppositeMove,'',animationSpeed=int(TILESIZE/2))
     makeMove(board,oppositeMove) 

allmoves에 들어있는걸 반대로 ! 해서 원래 보드판으로 돌아간다 !
그리고 이렇게 하는 에니메이션을 oppositeMove를 넘겨주면서 보여준다!

if __name__ == '__main__':
    main()

이제 끗 !! !다음편에 모아놓고 실행사진을 올리겠다!

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

0개의 댓글