# Pygame自製遊戲 - Tic Tac Toe
Tic Tac Toe的中文名稱是井字棋,就是在3x3的格子裡畫O和X的那個遊戲
他有很多變體,但我們今天做最基本的就好
:::info
不一定要跟我一樣
:::
# 遊戲主體
## 初始化
首先,我們要先決定一些數值,包含:
視窗長寬、棋盤格子大小、顏色等
接著創建螢幕
:::spoiler 程式碼
```python=
import pygame
import sys
WIDTH = 680
HEIGHT = 680
LINE_WIDTH = 20 # 邊框寬
BOARD_SIZE = 3 # 一排的格數
GRID_SIZE = 200 # 一格的大小
BACKGROUND_COLOR = (63, 63, 63) # 背景的顏色
GRID_COLOR = (255, 255, 255) # 格子內的顏色
pygame.init()
pygame.display.set_caption("Tic Tac Toe")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
```
:::
## 遊戲迴圈
再來,我們要來做主迴圈。
主迴圈是負責跑每一幀,常常會用函式分出去讓他比較簡潔。
而主迴圈中最重要的是結束判定和顯示結果
:::spoiler 程式碼
```python=
running = True # 幫助控制迴圈
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT: # 收到退出信號
running = False
# 結束
pygame.quit()
sys.exit()
```
:::
## 事件判定
既然有了主迴圈,我們就可以開始接收訊息,並且做出對應的回應
我們這個遊戲是用滑鼠來決定下棋的位置
然後看滑鼠的位置是不是在格子上,來決定要做什麼
:::spoiler 程式碼
```python=
elif event.type == pygame.MOUSEBUTTONDOWN:
mouseX, mouseY = event.pos
```
:::
## 基本棋盤
創造棋盤的基底,接著在這上面畫出九個格子
:::spoiler 程式碼
```python=
def clear_board(): # 清空畫布
screen.fill(BACKGROUND_COLOR)
def draw_board(): #畫上格子
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
rect = pygame.Rect(i * (GRID_SIZE+LINE_WIDTH)+LINE_WIDTH,
j * (GRID_SIZE+LINE_WIDTH)+LINE_WIDTH,
GRID_SIZE,
GRID_SIZE)
pygame.draw.rect(screen, GRID_COLOR, rect)
```
:::
## 文字系統
可以在畫面上顯示文字的話,就可以簡化很多事情
:::spoiler 程式碼
```python=
# 初始化文字、顏色
TEXT_COLOR = (0, 0, 0)
FONT = pygame.font.SysFont(None, 80)
# 寫出文字
def draw_text(text, center_pos):
label = FONT.render(text, True, TEXT_COLOR)
rect = label.get_rect(center = center_pos)
screen.blit(label, rect)
```
:::
## 下棋
接著要來真的下棋。
要下棋,首先要有棋盤狀態的陣列
並且賦予玩家編號,以方便記錄
:::spoiler 程式碼
```python=
# 在外面的全域變數
curr_player = 1 # 當前玩家編號
# 格子上下左右界
board = [[0 for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
gridX1 = [i * (GRID_SIZE+LINE_WIDTH) + LINE_WIDTH for i in range(BOARD_SIZE)]
gridX2 = [(i+1) * (GRID_SIZE+LINE_WIDTH) for i in range(BOARD_SIZE)]
gridY1 = [j * (GRID_SIZE+LINE_WIDTH) + LINE_WIDTH for j in range(BOARD_SIZE)]
gridY2 = [(j+1) * (GRID_SIZE+LINE_WIDTH) for j in range(BOARD_SIZE)]
```
:::
接著在主迴圈中,看我們點下滑鼠時,有沒有在任何格子裡
:::spoiler 程式碼
```python=
# 在主迴圈內
elif event.type == pygame.MOUSEBUTTONDOWN: # 按下滑鼠時
mouseX, mouseY = event.pos # 得到滑鼠的位置
for i in range(BOARD_SIZE):
if mouseX>gridX1[i] and mouseX<gridX2[i]: # 看有沒有在任何一行的x範圍中
for j in range(BOARD_SIZE):
if mouseY>gridY1[j] and mouseY<gridY2[j]: # 看有沒有在任何一列的y範圍中
if board[i][j] == 0: # 不能覆蓋棋子
board[i][j] = curr_player
curr_player = curr_player % 2 + 1 # 換人
```
:::
最後我們要把棋子畫出來
:::spoiler 程式碼
```python=
# 加在draw_board中
if board[i][j] == 1:
draw_text("X", rect.center)
elif board[i][j] == 2:
draw_text("O", rect.center)
```
:::
## 結束判定
現在我們可以下棋之後,就要看有沒有人贏,或是棋盤填滿了就要結束
1. 有沒有人贏
看每個行、列、兩條對角線,有沒有整排一樣的
:::spoiler 程式碼
```python=
def check_win():
for i in range(BOARD_SIZE):
if board[i][0] == board[i][1] == board[i][2] != 0:
return True
for j in range(BOARD_SIZE):
if board[0][j] == board[1][j] == board[2][j] != 0:
return True
if board[0][0] == board[1][1] == board[2][2] != 0:
return True
if board[0][2] == board[1][1] == board[2][0] != 0:
return True
return False
```
:::
2. 棋盤有沒有滿
可以記錄下了幾次,第九次還沒有人贏的話就是下滿了,於是就平手了
:::spoiler 程式碼
```python=
# 改變換人的部分,左邊空格省略
if check_win():
running = False
else:
curr_player = curr_player % 2 + 1
round = round + 1
if round == 9:
running = False
```
:::
## 回合顯示
我們可以在遊戲中顯示現在是哪個玩家的回合,更不會搞錯
:::spoiler
```python=
# 拉長視窗,才有空間
HEIGHT = 780
# 主迴圈內
if curr_player == 1:
draw_text("PLAYER: X", (340, 730))
else:
draw_text("PLAYER: O", (340, 730))
```
:::
# 額外功能
## 按鈕系統
我們可以加入一些按鈕來做簡單的互動介面
按鈕可以用長方形代表,如果在它上面按滑鼠就會觸發按鈕
:::spoiler 程式碼
```python=
# 按鈕顏色
BUTTON_COLOR = (127, 255, 63)
# 按鈕的範圍
start_button = pygame.Rect(70, 450, 500, 150)
restart_button = pygame.Rect(75, 460, 300, 100)
quit_button = pygame.Rect(460, 460, 150, 100)
# 畫按鈕
def draw_button(rect, text):
pygame.draw.rect(screen, BUTTON_COLOR, rect)
draw_text(text, rect.center)
```
:::
## 開始界面、再玩一次
我們可以把遊戲分成三個階段:開始前、遊戲中、結束後
在這三個階段下顯示不同的東西
:::spoiler 程式碼
```python=
# 初始化狀態
BEFORE_START = 0
PLAYING = 1
GAME_OVER = 2
game_state = BEFORE_START
winner = 0
# 再玩一次
def reset_game():
global board, curr_player, winner, game_state, round
board = [[0 for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
round = 0
curr_player = 1
winner = 0
game_state = PLAYING
```
:::
三種狀態分開處理
:::spoiler 程式碼
```python=
# 主迴圈內,區分狀態
clear_board()
if game_state == BEFORE_START:
draw_text("Tic Tac Toe", (340, 160))
draw_button(start_button, "Begin")
elif game_state == PLAYING:
draw_board()
if curr_player == 1:
draw_text("PLAYER: X", (340, 730))
else:
draw_text("PLAYER: O", (340, 730))
elif game_state == GAME_OVER:
draw_button(restart_button, "Restart")
draw_button(quit_button, "Quit")
if winner == 1:
draw_text("Player X wins!", (340, 150))
elif winner == 2:
draw_text("Player O wins!", (340, 150))
else:
draw_text("DRAW", (340, 150))
```
```python=
# 按下滑鼠後
elif event.type == pygame.MOUSEBUTTONDOWN:
mouseX, mouseY = event.pos
if game_state == BEFORE_START:
if start_button.collidepoint(mouseX, mouseY):
reset_game()
elif game_state == PLAYING:
for i in range(BOARD_SIZE):
if mouseX>gridX1[i] and mouseX<gridX2[i]:
for j in range(BOARD_SIZE):
if mouseY>gridY1[j] and mouseY<gridY2[j]:
if board[i][j] == 0:
board[i][j] = curr_player
if check_win():
game_state = GAME_OVER
winner = curr_player
else:
curr_player = curr_player % 2 + 1
round = round + 1
if round == 9:
game_state = GAME_OVER
elif game_state == GAME_OVER:
if restart_button.collidepoint(mouseX, mouseY):
reset_game()
elif quit_button.collidepoint(mouseX, mouseY):
running = False
```
:::
## 數字鍵下棋
我們可以讓玩家用按數字鍵就能在對應的位置下棋。
:::info
要用英文輸入才能用
:::
:::spoiler 程式碼
```python=
# 各個數字鍵
NUMPAD_KEYS = [ pygame.K_KP7, pygame.K_KP8, pygame.K_KP9,
pygame.K_KP4, pygame.K_KP5, pygame.K_KP6,
pygame.K_KP1, pygame.K_KP2, pygame.K_KP3 ]
# 把函數拉出來,方便重複使用
def next_round():
global game_state, winner, curr_player, round
if check_win():
game_state = GAME_OVER
winner = curr_player
else:
curr_player = curr_player % 2 + 1
round = round + 1
if round == 9:
game_state = GAME_OVER
# 在迴圈最後面,和其他 event.type 同層
elif event.type == pygame.KEYDOWN:
if event.key in NUMPAD_KEYS and game_staate == PLAYING:
idx = NUMPAD_KEYS.index(event.key)
i, j = idx%3, 2 - idx//3
if board[i][j] == 0:
board[i][j] = curr_player
next_round()
# 按鍵加入、退出
elif event.key == pygame.K_KP0:
if game_state == BEFORE_START:
reset_game()
elif game_state == GAME_OVER:
reset_game()
elif event.key == pygame.K_KP_PERIOD and game_state == GAME_OVER:
running = False
```
:::
## 完整範例程式
:::spoiler 程式碼
```python=0
import pygame
import sys
WIDTH = 680
HEIGHT = 780
LINE_WIDTH = 20
BOARD_SIZE = 3
GRID_SIZE = 200
pygame.init()
pygame.display.set_caption("Tic Tac Toe")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
BACKGROUND_COLOR = (63, 63, 63)
GRID_COLOR = (255, 255, 255)
TEXT_COLOR = (0, 0, 0)
BUTTON_COLOR = (127, 255, 63)
FONT = pygame.font.SysFont(None, 80)
start_button = pygame.Rect(70, 450, 500, 150)
restart_button = pygame.Rect(75, 460, 300, 100)
quit_button = pygame.Rect(460, 460, 150, 100)
BEFORE_START = 0
PLAYING = 1
GAME_OVER = 2
game_state = BEFORE_START
winner = 0
curr_player = 1
board = [[0 for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
gridX1 = [i * (GRID_SIZE+LINE_WIDTH) + LINE_WIDTH for i in range(BOARD_SIZE)]
gridX2 = [(i+1) * (GRID_SIZE+LINE_WIDTH) for i in range(BOARD_SIZE)]
gridY1 = [j * (GRID_SIZE+LINE_WIDTH) + LINE_WIDTH for j in range(BOARD_SIZE)]
gridY2 = [(j+1) * (GRID_SIZE+LINE_WIDTH) for j in range(BOARD_SIZE)]
NUMPAD_KEYS = [ pygame.K_KP7, pygame.K_KP8, pygame.K_KP9,
pygame.K_KP4, pygame.K_KP5, pygame.K_KP6,
pygame.K_KP1, pygame.K_KP2, pygame.K_KP3 ]
def draw_text(text, center_pos):
label = FONT.render(text, True, TEXT_COLOR)
rect = label.get_rect(center = center_pos)
screen.blit(label, rect)
def clear_board():
screen.fill(BACKGROUND_COLOR)
def draw_board():
if curr_player == 1:
draw_text("PLAYER: X", (340, 730))
else:
draw_text("PLAYER: O", (340, 730))
for i in range(BOARD_SIZE):
for j in range(BOARD_SIZE):
rect = pygame.Rect(i * (GRID_SIZE+LINE_WIDTH)+LINE_WIDTH,
j * (GRID_SIZE+LINE_WIDTH)+LINE_WIDTH,
GRID_SIZE,
GRID_SIZE)
pygame.draw.rect(screen, GRID_COLOR, rect)
if board[i][j] == 1:
draw_text("X", rect.center)
elif board[i][j] == 2:
draw_text("O", rect.center)
def draw_button(rect, text):
pygame.draw.rect(screen, BUTTON_COLOR, rect)
draw_text(text, rect.center)
def check_win():
for i in range(BOARD_SIZE):
if board[i][0] == board[i][1] == board[i][2] != 0:
return True
for j in range(BOARD_SIZE):
if board[0][j] == board[1][j] == board[2][j] != 0:
return True
if board[0][0] == board[1][1] == board[2][2] != 0:
return True
if board[0][2] == board[1][1] == board[2][0] != 0:
return True
return False
def reset_game():
global board, curr_player, winner, game_state, round
board = [[0 for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
round = 0
curr_player = 1
winner = 0
game_state = PLAYING
def check_move():
global game_state, winner, round, curr_player
if board[i][j] == 0:
board[i][j] = curr_player
if check_win():
game_state = GAME_OVER
winner = curr_player
else:
curr_player = curr_player % 2 + 1
round = round + 1
if round == 9:
game_state = GAME_OVER
running = True
round = 0
while running:
clear_board()
if game_state == BEFORE_START:
draw_text("Tic Tac Toe", (340, 160))
draw_button(start_button, "Begin")
elif game_state == PLAYING:
draw_board()
if curr_player == 1:
draw_text("PLAYER: X", (340, 730))
else:
draw_text("PLAYER: O", (340, 730))
elif game_state == GAME_OVER:
draw_button(restart_button, "Restart")
draw_button(quit_button, "Quit")
if winner == 0:
draw_text("DRAW", (340, 150))
elif winner == 1:
draw_text("Player X wins!", (340, 150))
else:
draw_text("Player O wins!", (340, 150))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
mouseX, mouseY = event.pos
if game_state == BEFORE_START:
if start_button.collidepoint(mouseX, mouseY):
reset_game()
elif game_state == PLAYING:
for i in range(BOARD_SIZE):
if mouseX>gridX1[i] and mouseX<gridX2[i]:
for j in range(BOARD_SIZE):
if mouseY>gridY1[j] and mouseY<gridY2[j]:
check_move()
elif game_state == GAME_OVER:
if restart_button.collidepoint(mouseX, mouseY):
reset_game()
elif quit_button.collidepoint(mouseX, mouseY):
running = False
elif event.type == pygame.KEYDOWN:
if event.key in NUMPAD_KEYS and game_state == PLAYING:
idx = NUMPAD_KEYS.index(event.key)
i, j = idx%3, idx//3
check_move()
elif event.key == pygame.K_KP0:
if game_state == BEFORE_START:
reset_game()
elif game_state == GAME_OVER:
reset_game()
elif event.key == pygame.K_KP_PERIOD and game_state == GAME_OVER:
running = False
pygame.display.update()
pygame.quit()
sys.exit()
```
:::