We are Architect

30. 파이썬으로 만드는 TIC-TAC-TOE 게임 본문

Programing/Python

30. 파이썬으로 만드는 TIC-TAC-TOE 게임

the best infra 2024. 12. 4. 18:24

 

* 취지 : 

  • 파이썬 연습을 하고 싶어서 무슨 프로그램을 만들면 좋을까 싶어서 쉬운 프로그램을 하나 택해서 해보려고 했는데 간단해 보이면서도 쉬워 보이고 심지어 패키지 도 pygame이라는 패키지 하나 밖에 안 써서 해당 프로그램을 만들었다.
  • 그러나 보기 보다는 쉽지 않았고 UI에 맞춘 공간 설정 및 여러 설정 값들이 많이 들어가야 하며 게임 환경을 만들기 위한 변숫값 설정과 공식이 은근히 들어가서 어려웠던 거 같다... 수준이 많이 낮은 걸 인지하고 최대한 여러 도구와 자료들을 이용해서 프로그램을 만들어 보았다. 

 

* 도구 

  • pygame : 파이썬으로 게임을 만들기 위한 전용 패키지 이다. 안에 UI 제공 및 게임을 위한 함수들이 들어가 있다. 

 

* 프로그램 설계 단계

  • 게임 안에 필요한 전역 변수 설정
    1. # UI에 사용될 글자 설정
    2. # 스크린에 들어갈 값 설정
    3. UI에 사용되는 RGB색상 값
    4. 턴 넘기기를 위한 턴 설정 값
    5. global 변수로  done, turn, grid
  • 게임 안에 필요한 함수
    • runGame : 게임을 실행하는 가장 큰 함수
      is_valid_position : 유효한 값(그리드 값안에 ' ')
      is_winner : 승리 조건
      is_grid_full : 그리드가 꽉 찰 경우
       
  • 전체 소스 코드 
# Python에서 2D 게임 개발을 쉽게 할 수 있도록 도와주는 라이브러리. C 기반의 SDL 라이브러리를 사용하여 그래픽과 멀티미디어 기능을 제공.
import pygame

# 내부 그래픽, 사운드 등을 초기화하는 함수로 반드시 실행.
pygame.init()

# 게임 UI에 사용되는 RGB색상 값.
white = (255,255,255)
black = (0,0,0)
yellow = (255,255,0)
red = (255,0,0)

# UI에 사용될 글자 설정
large_font = pygame.font.SysFont(None, 72)
small_font = pygame.font.SysFont(None, 36)

# 스크린에 들어갈 값 설정
size = [600,600]
screen = pygame.display.set_mode(size)

# 턴 넘기기를 위한 턴 설정값
turn = 0 
grid = [' ',' ',' ',' ',' ',' ',' ',' ',' ']

done = False
clock = pygame.time.Clock()

# 게임 무한 루프
def runGame():
    #게임 활용 변수
    CELL_SIZE = 60
    COLUMN_COUNT = 3
    ROW_COUNT = 3
    X_WIN = 1
    O_WIN = 2
    DRAW = 3
    game_over = 0

    # 전역 변수를 가져와서 사용.
    global done, turn, grid

    # 게임 실행 
    while not done:

        # 프레임 설정
        clock.tick(30)

        # 스크린 화면 색깔 검은색
        screen.fill(black)

        # 키 입력, 마우스 클릭, 창 닫기 등의 사용자 이벤트를 큐(queue)에 저장
        for event in pygame.event.get():

            # 이벤트 타입이 QUIT(창 닫기)면, done(종료 코드)을 참으로.
            if event.type == pygame.QUIT:
                done=True

            # 이벤트 타입이 마우스 클릭이면 적용.
            elif event.type == pygame.MOUSEBUTTONDOWN:

                # 만약 턴 값이 0이면 플레이어 X의 턴. 
                if turn == 0:

                    # event.pos(x좌표 or y좌표)함수는 마우스 클릭 좌표.
                    # x 좌표
                    column_index = event.pos[0] // CELL_SIZE

                    # y 좌표
                    row_index = event.pos[1] // CELL_SIZE

                    # position(위치) = 열 번호 + (행번호 * 그리드 갯수)
                    position = column_index + 3 * row_index

                    print("Hello world") # 오류 체킹 용

                    # 플레이어 X의 설정
                    if is_valid_position(grid, position):
                        # 그리드 안에 X
                        grid[position] = 'X'

                        # 만약 그리드안에 위치들에 X들이 배치 되면
                        if is_winner(grid, 'X'):

                            # X의 승리
                            print('X 가 이겼습니다.')

                            # game_over 안에 X_WIN 값 넣기
                            game_over = X_WIN 
                            #break

                        # full=true값이면
                        elif is_grid_full(grid):
                            print('무승부 입니다.')

                            # game_over안에 DRAW
                            game_over = DRAW 
                            #break
                        
                        # 턴 넘기기
                        turn += 1
                        turn = turn % 2
                
                # 플레이어 O 설정
                else:       

                    # X좌표 
                    column_index = event.pos[0] // CELL_SIZE

                    # Y좌표
                    row_index = event.pos[1] // CELL_SIZE

                    # position(두 좌표의 값을 1차원적 값으로 표현) = x좌표 + 3 * y좌표 
                    position = column_index + 3 * row_index

                    # position에 특정 값이 채워졌을때
                    if is_valid_position(grid, position):
                        grid[position] = 'O' 

                        # O 플레이어가 O를 채워서 이기는 경우  
                        if is_winner(grid, 'O'):
                            print('O 가 이겼습니다.')
                            game_over = O_WIN 
                            #break
                        
                        # O 플레이어가 무승부인 경우
                        elif is_grid_full(grid):
                            print('무승부 입니다.')
                            game_over = DRAW 
                            #break
                        
                        # 턴 넘기기
                        turn += 1
                        turn = turn % 2


            # 화면 그리기 
            # y축 갯수 만큼 반복
            for column_index in range(COLUMN_COUNT):

                # x축 갯수 만큼 반복
                for row_index in range(ROW_COUNT):

                    # rect(사각형) 튜플 구조 = (x, y, width, height) 
                    rect = (CELL_SIZE * column_index, CELL_SIZE * row_index, CELL_SIZE, CELL_SIZE)

                    # Pygame에서 사각형을 그릴 때 사용하는 함수
                    pygame.draw.rect(screen, white, rect, 1)
            
            # y축 갯수 만큼 반복
            for column_index in range(COLUMN_COUNT):

                # x축 갯수 만큼 반복
                for row_index in range(ROW_COUNT):

                    # position(두 좌표의 값을 1차원적 값으로 표현) = x좌표 + 3 * y좌표 
                    position = column_index + 3 * row_index

                    # 그리드안에 position에 특정 값이 채워졌을때 mark변수에 넣기
                    mark = grid[position]

                    # mark가 X일 경우
                    if mark == 'X':

                        # X를 노란색으로 랜더링 후에 표시
                        X_image = small_font.render('{}'.format('X'), True, yellow)

                        # screen.blit(어떤 이미지를 가져올건지, 이미지 위치) > Pygame에서 이미지를 화면에 표시할 때 사용하는 함수
                        screen.blit(X_image, (CELL_SIZE * column_index + 10, CELL_SIZE * row_index + 10)) 
                    
                    # mark가 O일 경우
                    elif mark == 'O':

                        # O를 노란색으로 랜더링 후에 표시
                        O_image = small_font.render('{}'.format('O'), True, white)

                        # screen.blit(어떤 이미지를 가져올건지, 이미지 위치) > Pygame에서 이미지를 화면에 표시할 때 사용하는 함수
                        screen.blit(O_image, (CELL_SIZE * column_index + 10, CELL_SIZE * row_index + 10)) 

            # game_over인지 아닌지
            if not game_over: 

                # 게임이 안 끝났으면
                pass

            # game_over이 WIN상태인지 아님 draw 상태인지
            else:

                # X_WIN 일 때 렌더링
                if game_over == X_WIN:
                    game_over_image = large_font.render('X wins', True, red)

                # O_WIN 일 때 렌더링
                elif game_over == O_WIN:
                    game_over_image = large_font.render('O wins', True, red)
                
                # Draw 일 때 렌더링
                else:
                    game_over_image = large_font.render('Draw', True, red)
                
                # game_over_image 상태를 UI 위치에 띄우기
                screen.blit(game_over_image, (600 // 2 - game_over_image.get_width() // 2, 600 // 2 - game_over_image.get_height() // 2))

            pygame.display.update() #모든 화면 그리기 업데이트

# 유효한 값이 들어갔는지 판단.
def is_valid_position(grid, position):
    if grid[position] == ' ':
        return True
    else:
        return False


# 승리 조건 만들기
def is_winner(grid, mark):
    if(grid[0] == mark and grid[1] == mark and grid[2] == mark) or \
      (grid[3] == mark and grid[4] == mark and grid[5] == mark) or \
      (grid[6] == mark and grid[7] == mark and grid[8] == mark) or \
      (grid[0] == mark and grid[3] == mark and grid[6] == mark) or \
      (grid[1] == mark and grid[4] == mark and grid[7] == mark) or \
      (grid[2] == mark and grid[5] == mark and grid[8] == mark) or \
      (grid[0] == mark and grid[4] == mark and grid[8] == mark) or \
      (grid[2] == mark and grid[4] == mark and grid[6] == mark):
        return True
    else:
        return False

# 그리드가 꽉 찰 경우의 함수
def is_grid_full(grid):
    full = True
    for mark in grid:
        if mark == ' ':
            full = False
            break

    return full

# runGame함수 시작
runGame()

# UI창 종료
pygame.quit()

 

 

* 실행 결과 

 

*참고 자료 : ai-creator님 > https://ai-creator.tistory.com/528