---
# System prepended metadata

title: Pygame自製遊戲 - Tic Tac Toe
tags: [電算社講義, 電算社講義-python]

---

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

```
:::