[파이썬 간단한 게임 만들기] 3. 틱택토
유치한 게임에 오신 것을 환영합니다.
이번에는 틱택토 게임을 만들어 보겠습니다.
아래와 같은 순서로 배워보겠습니다.
1. 목표
이번 장에서는 틱택토 게임을 만들어 보도록 하겠습니다.
이번에 만들 게임은 틱택토(Tic Tac Toe) 게임입니다. 틱택토 게임의 규칙은 다음과 같습니다.
틱택토(tic-tac-toe)는 두 명이 번갈아가며 O와 X를 3×3 판에 써서 같은 글자를 가로, 세로, 혹은 대각선 상에 놓이도록 하는 놀이이다. m,n,k-게임으로, (3,3,3)-게임이다.[출처 - 위키백과]
쉽게 말해 3X3 화면에서 자기의 도형(O or X)을 번갈아 놓게 되며, 같은 모양의 도형이 3개가 연속해서 놓이게 되면 이기는 게임입니다.
이번 장에서는 이 틱택토 게임을 구현하고, 마우스를 활용한 상태변화와 이벤트 처리에 대해 배워보도록 하겠습니다.
2. 사전 준비
1) 파이썬 문법 이해
and 와 or 연산자를 이해하고, if문과 함께 사용해 봅니다. |
2) 게임은 어떤 고려사항이 있을까?
2-1) 게임 승리 조건
2-2) '수'를 놓을 수 있는 조건
2-3) 마우스 이벤트 처리 방법
2-4) 글자 출력 방법
참고) 설계를 해보자!
해당 고려사항들을 생각하고 바로 코딩을 하기 보다는 기능명세서를 작성해보세요.
기능명세서의 핵심은 1. 무엇을 2. 어떻게 만들면 되는지 기능단위 명세입니다.
전통적인 소프트웨어공학에서 다루고 있는 내용들이지만, 해야 할일이 많은 현업에서는 잘 하기도 하고/잘 안하기도 하는 부분들이기도 합니다.
현업을 떠나
코딩을 할때 많은 분들이 토로하시는 고충 중에 '내용은 이해하였지만 어디서부터 어떻게 시작해야할지 모르겠다'이 있습니다. 이런 분들께 추천 드리는 방법입니다.
또는 빠르게 구현하고 싶어, 구현으로 바로 들어갔지만 구현 시간이 오래 걸리는 분들도 이런 설계단계를 뛰어 넘는 경우가 많습니다.
순서는 항상 기획 > 요건 파악 > 설계 > 구현 > 유지보수 단계를 거칩니다.
요건파악이 완료되었다면, 설계를 한 후 구현하는 습관을 들여보세요.
예시1) 틱택토 기능명세서
예시2) 틱택토 기능명세서
3) 게임판 구상
import pygame # 1. pygame 선언
pygame.init() # 2. pygame 초기화
# 3. pygame에 사용되는 전역변수 선언
WHITE = (255,255,255)
BLACK = (0, 0, 0)
YELLOW = (255, 255, 0)
RED = (255, 0, 0)
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()
# 4. pygame 무한루프
def runGame():
global done
while not done:
clock.tick(10)
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == pygame.QUIT: # [X] 종료키가 누르면, 게임 종료
done=True
pygame.display.update() #모든 화면 그리기 업데이트
runGame()
pygame.quit()
게임을 진행할 화면을 다음과 같이 설정하도록 하겠습니다.
8번 라인(화면 색) : black(RGB - 0,0,0)
9번 라인(X 도형 색) : yellow(RGB - 255,255,0)
10번 라인(게임 종료 폰트 색) : red(RGB - 255,0,0)
11번 라인(large_font) : 게임 종료 메세지를 표시할 폰트 선언
12번 라인(small_font) : O, X 도형을 표시할 폰트 선언
13번 라인(화면 크기) : 넓이(width) - 600, 높이(height) - 600
16번 라인(trun) : 게임 순서(턴)를 확인 할 변수
13번 라인(grid) : 게임을 진행하기 위한 3X3 내부 게임판
3. 소스 코드 (전체)
파일명 :tictactoe.py
drive.google.com/drive/folders/1P7-ibtlJKUEet1lbqkSAg7N8RyxOza_o?usp=sharing
4. 사전 지식
1) 틱택토 게임에 필요한 요소 정의
틱택토는 간단하게 3X3의 게임판과 O,X 도형만 있으면 됩니다.
이번 장에서는 3X3의 게임판은 grid라는 리스트 안에 9개의 요소를 배치하여 사용을 할 것이고, 그 안에 각각 O, X 도형을 마크하여 게임을 진행하도록 하겠습니다.
다음으로 중요한 것은 게임의 승리 요소 입니다.
틱택토 게임에서 승리하기 위해서는 내가 사용하는 도형이 연속해서 3개가 이어져야 하며 이는 직선과 대각선 상관없이 이어져야 합니다. 따라서 승리에 대한 조건을 함수를 통해 구현해보도록 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
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
|
cs |
위의 코드에서 grid라는 게임판에서 mark(선택된 도형)이 연속해서 3개가 이어지는 경우를 모두 판단하여 게임의 승리요소를 확인 하고 있습니다. 만일 grid의 0,1,2의 좌표에서 인자로 들어오는 'O'라는 마크가 모두 동일하게 찍혀 있으면, 게임의 승리로 확정 짓게 됩니다.
2~4라인은 '가로'
5~7라인은 '세로'
8~9라인은 '대각선'
으로 게임승리를 확인 합니다.
2) PyGame 글자 쓰기
[그림1] 과 같이 게임 종료 시, O와 X중 승리자에 대한 결과 발표를 pygame 게임판 안에 출력해보겠습니다.
1
2
3
4
|
large_font = pygame.font.SysFont(None, 72)
small_font = pygame.font.SysFont(None, 36)
game_over_image = large_font.render('Draw', True, RED)
screen.blit(game_over_image, (600 // 2 - game_over_image.get_width() // 2, 600 // 2 - game_over_image.get_height() // 2))
|
cs |
1~2라인 : 앞서 사전준비에서 2개의 폰트를 설정 하였습니다.
large_font, small_font 두 가지로 변수를 선언하였는데, pygame.font.SysFont() 함수를 사용했습니다.
SysFont() 함수에서 첫번째 인자는 '폰트'를 의미하며 첫번째 인자에는 None 으로 설정해 기본 폰트를 지정하였고, 두번째 인자로 폰트 크기를 넣어 각각 72, 36으로 폰트 크기를 설정하였습니다.
3~4라인 : 해당 폰트 설정을 기반으로 게임판에 출력하기 위해서는 두 가지 작업이 필요합니다.
먼저, render() 함수를 이용해 작성할 텍스트와 폰트 색을 설정해줍니다.
다음으로는 screen.blit() 함수를 이용해 게임판에 텍스트가 출력되도록 합니다.
3라인에서 render() 함수를 통해 'Draw'라는 텍스트를 앞서 설정한 RED 색으로 출력하도록 하였고,
4라인에서 해당 텍스트를 게임판(600X600)의 정중앙에 출력하도록 설정하였습니다.
[2.사전준비]에 적절한 위치에 3~4라인을 추가하여, DRAW라는 글자를 출력해보세요.
35~36라인에 추가하여, 이와 같은 결과가 출력되었습니다.
import pygame # 1. pygame 선언
pygame.init() # 2. pygame 초기화
# 3. pygame에 사용되는 전역변수 선언
WHITE = (255,255,255)
BLACK = (0, 0, 0)
YELLOW = (255, 255, 0)
RED = (255, 0, 0)
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()
# 4. pygame 무한루프
def runGame():
global done
while not done:
clock.tick(10)
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == pygame.QUIT: # [X] 종료키가 누르면, 게임 종료
done=True
game_over_image = large_font.render('Draw', True, RED)
screen.blit(game_over_image, (600 // 2 - game_over_image.get_width() // 2, 600 // 2 - game_over_image.get_height() // 2))
pygame.display.update() #모든 화면 그리기 업데이트
runGame()
pygame.quit()
pygame에서 게임 텍스트를 출력하는 방법을 배웠으니 이제 게임을 만들어보도록 하겠습니다.
5. 구현 순서
구현 순서는 다음과 같습니다.
Step1 | 화면에 도형 찍기(마우스를 통한 게임 진행) |
Step2 | 화면 그리기 |
Step3 | 게임 종료 |
Step1) 화면에 도형 찍기(마우스를 통한 게임 진행)
이번에 사용할 이벤트는 '마우스'입니다.
마우스 클릭을 통해 3X3 게임판에 도형을 찍도록 하겠습니다.
앞서 배운 것 처럼 pygame의 이벤트를 먼저 받아 마우스 클릭을 if문을 통해 검증을 합니다.
마우스 클릭 이벤트는 pygame.MOUSEBUTTONDOWN 으로 표시되며, event.pos를 통해 해당 이벤트가 발생한 좌표를 확인 할 수 있습니다.
event.pos는 list 형태로 2가지 값을 가지는데 각각 x와 y축의 좌표를 나타냅니다.
event.pos[0] -> x좌표
event.pos[1] -> 좌표
이를 기반으로 코드를 살펴보도록 하겠습니다.
먼저 틱택토 게임에 사용될 변수들을 게임루프 내부에 선언하도록 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
|
cs |
3~9번 라인까지 각각 변수값은 다음과 같습니다.
CELL_SIZE - 3X3 내부 게임판의 각 셀 크기
COLUMN_COUNT - 내부 게임판의 세로행 갯수
ROW_COUNT - 내부 게임판의 가로열 갯수
game_over, X_WIN, O_WIN, DRAW - 각각 게임 종료 플래그 설정
다음은, 마우스 클릭 이벤트 처리입니다.
마우스를 클릭하면서, '너' 한번~ '나' 한번 게임을 하게 되죠.그러니 turn 이라는 변수로 O와 X의 각 순서를 검증하며 이벤트를 처리합니다. 이렇게 게임을 진행할 때 O, X를 둘 수 있는 상황인지를 생각해 봐야 합니다. 만약, 이미 O가 있는자리에 X를 둘 수 없자나요~ 그러므로, 마우스 클릭 이벤트가 발생 했을 때, 총 세 가지를 검증해야합니다.
1) 해당 셀이 비었는지? 즉, 도형을 그릴 수 있는 자리인지 검증을 합니다.
2) 해당 셀에 도형을 놓았을 때, 앞서 선언한 is_winner() 함수를 통해 승리 조건이 부합하는지 검증을 합니다.
3) 모든 셀이 가득 찼는지 검증합니다. 승자 없이 모든 셀이 가득찼기 떄문에 게임이 무승부로 끝나도록 설정합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
## 상단 게임 설정 선언 생략
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
turn = 0
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)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
elif event.type == pygame.MOUSEBUTTONDOWN:
if turn == 0:
column_index = event.pos[0] // CELL_SIZE
row_index = event.pos[1] // CELL_SIZE
position = column_index + 3 * row_index
if is_valid_position(grid, position):
grid[position] = 'X'
if is_winner(grid, 'X'):
print('X 가 이겼습니다.')
game_over = X_WIN
#break
elif is_grid_full(grid):
print('무승부 입니다.')
game_over = DRAW
#break
turn += 1
turn = turn % 2
else:
column_index = event.pos[0] // CELL_SIZE
row_index = event.pos[1] // CELL_SIZE
position = column_index + 3 * row_index
if is_valid_position(grid, position):
grid[position] = 'O'
if is_winner(grid, 'O'):
print('O 가 이겼습니다.')
game_over = O_WIN
#break
elif is_grid_full(grid):
print('무승부 입니다.')
game_over = DRAW
#break
turn += 1
turn = turn % 2
|
cs |
52번 라인은 턴을 검증하여 각각 0과 1일 때 X와 O 도형의 순서를 검증합니다. 69~70번 라인에서 각 순서가 끝나게 되면 turn이 1일때는 0으로 수정되며, turn이 0일 때는 1로 수정됩니다.
53,54번 라인은 앞서 설명드린 event.pos를 통해 마우스 클릭 이벤트가 발생한 x, y좌표를 구하며 이를 SELL_SIZE로 나누어 내부 게임판의 어떤 위치에 마우스가 찍혔는지 판단합니다.(0,1,2 값으로 인덱스 표현)
55번 라인은 위에서 구한 셀 위치를 기반으로 grid에 어떤 position에 해당하는지 구해주는 식입니다. grid는 9개의 리스트로 되어 있기 떄문에 column_index에 row_index에 3을 곱한 값을 더해줍니다. 예를 들어 row와column이 각각 (2,1)이라고 했을 때, 이를 1열의 9개 칸이라고 가정하면 1+3 * 2 을 통해 7번째 인덱스에 찍히게 됩니다.
57번 라인은 위에서 구한 postion 값을 통해 위에서 선언한 is_vaild_position() 함수를 통해 해당 위치에 다른 도형이 찍혀있지 않은지 판단을 합니다. 만약 vailid_postion이라면 58번 라인에서 해당 position을 O 혹은 X 도형으로 변경해줍니다.
60번 라인은 도형을 찍었을 때, 만약 승리 조건에 해당하게 된다면 game_over 값을 X_WIN으로 변경하여 각 승리를 판단할 수 있도록 합니다.
64번 라인은 모든 셀이 가득 찼지만, 승부가 나지 않았을 경우에 해당하며 무승부 처리를 하도록 합니다.
Step2) 화면 그리기
이번에는 앞서 작성한 코드를 기반으로 해당 게임을 화면에 그려보도록 하겠습니다.
먼저 두 가지를 화면에 그려야 합니다.
- 내부 게임판(grid - 3X3)
- O, X 도형(mark)
일단 게임판을 그리는 로직에 대해 설명 드리도록 하겠습니다.
1
2
3
4
|
for column_index in range(COLUMN_COUNT):
for row_index in range(ROW_COUNT):
rect = (CELL_SIZE * column_index, CELL_SIZE * row_index, CELL_SIZE, CELL_SIZE)
pygame.draw.rect(screen, WHITE, rect, 1)
|
cs |
3X3 게임판을 그려야 하기 때문에 이중 for문을 이용하여 각각 COLUMN_COUNT(3), ROW_COUNT(3)의 루프문을 돌며 9개의 도형을 그려주도록 합니다.
각 도형은 CELL_SIZE를 기반으로 위치와 크기를 넣어 rect로 생성이 되며,
이전 장에서 배운 pygame.draw.rect() 함수를 통해 사각형을 총 9개 그려 3X3의 게임판을 그려줍니다.
다음으로 앞서 작성한 도형을 화면에 찍어 주도록 하겠습니다.
로직은 위와 동일합니다. 각각 COLUMN_COUNT(3), ROW_COUNT(3)의 루프문을 돌며 9개의 각 rect안에 grid를 기반으로 도형을 찍게 됩니다.
1
2
3
4
5
6
7
8
9
10
|
for column_index in range(COLUMN_COUNT):
for row_index in range(ROW_COUNT):
position = column_index + 3 * row_index
mark = grid[position]
if mark == 'X':
X_image = small_font.render('{}'.format('X'), True, YELLOW)
screen.blit(X_image, (CELL_SIZE * column_index + 10, CELL_SIZE * row_index + 10))
elif mark == 'O':
O_image = small_font.render('{}'.format('O'), True, WHITE)
screen.blit(O_image, (CELL_SIZE * column_index + 10, CELL_SIZE * row_index + 10))
|
cs |
3번 라인은 앞서 구한바와 동일하게 postion을 구해 3X3을 1열의 9개의 칸으로 변경해줍니다.
4번 라인은 grid에서 해당 position에 찍혀있는 도형을 구해줍니다. ( ' ', O, X)
5~10번 라인은 각각 도형을 검증하여 해당 도형을 찍어주도록 하며, 앞서 설명드린 텍스트를 화면에 그린 방식을 적용하여 screen.blit() 함수를 이용하여 해당하는 cell 위치에 도형을 그려주도록 합니다.
Step3) 게임 종료
앞서 각 도형을 찍을 때, 게임승리 혹은 무승부를 검증하였습니다.
해당 라인에서는 게임 종료를 바로 하지 않았습니다. 일단 게임판에 도형을 그리고 난 후에 게임 종료를 해야하기 때문이죠.
따라서 게임 종료 처리는 가장 마지막에 추가해주도록 합니다.
먼저 게임 종료는 앞서 추가한 game_over 값이 X_WIN 인지 O_WIN 인지 판별하게 되고 해당 상태를 기반으로
화면에 게임판 종료 메세지를 출력하는 방식입니다.
1
2
3
4
5
6
7
8
9
10
|
if not game_over:
pass
else:
if game_over == X_WIN:
game_over_image = large_font.render('X wins', True, RED)
elif game_over == O_WIN:
game_over_image = large_font.render('O wins', True, RED)
else:
game_over_image = large_font.render('Draw', True, RED)
screen.blit(game_over_image, (600 // 2 - game_over_image.get_width() // 2, 600 // 2 - game_over_image.get_height() // 2))
|
cs |
1번 라인은 game_over 값이 변경 되지 않았다면 게임루프를 계속 진행하도록 합니다.
4번 라인은 앞서 설정한 게임 승리 조건에 해당되어 game_over 값이 X_WIN 이라면 X 가 승리했다는 메세지를 화면에 출력하도록합니다.
5번 라인은 large_font를 사용하여 'X wins'라는 메세지를 RED 값으로 출력하도록 설정합니다.
10번 라인은 5번 라인에서 설정한 game_over_image를 기반으로 화면의 정중앙에 메세지를 표시하도록 합니다. width와 height가 각각 600으로 지정되어 있으며, 해당 값에 game_over_image의 width와 height 값을 빼서 위치를 설정합니다.
6. 정리
이번 장에서는 앞서 배운 pygame의 기본 구조와 게임 루프, 이벤트 처리를 기반으로 마우스 클릭 이벤트를 더해 틱택토 게임을 구현해보았습니다.
먼저, 게임에 필요한 요소 정의를 하고 텍스트를 화면에 출력할 수 있도록 사전학습을 한 뒤 진행을 했으며,
Step 1) 마우스 클릭을 기반으로 화면에 도형 찍기 -> event.pos를 이용한 좌표 추적
Step 2) 화면 그리기 -> grid라는 게임판을 기반으로 3X3 내부 게임판 생성, 도형 찍기(text)
Step 3) 게임 종료 -> 플래그를 이용한 게임종료 처리, 화면 정중앙에 텍스트 출력하기
를 배웠습니다.
이번 장에는 pygame과 마우스 좌표 설정 텍스트 출력을 사용하며 여러 상황에 맞는 이벤트 처리를 주로 다뤘습니다. 이를 통해 조금 더 색다른 게임을 만들어 보세요.
7. 확장하기
3x3 게임에서 4x4로 게임판의 크기를 늘려보세요.
ㅁ 참고
도움이 되셨다면, 좋아요 / 구독 버튼 눌러주세요~
저작물의 저작권은 작성자에게 있습니다.
공유는 자유롭게 하시되 댓글 남겨주세요~
상업적 용도로는 무단 사용을 금지합니다.
끝까지 읽어주셔서 감사합니다^^