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