# Python實作課程 ## 第三堂課 : Tic Tac Toe ## 2022 / 4 / 28 ### Tony --- ## 遊戲介紹 ---- Tic Tac Toe 又叫井字遊戲 兩位玩家會在一個3*3的棋盤內輪流放置X和O佔領格子 當有連續的3個相同符號佔領一個<font color='red'>橫排、直排、或對角線</font>時,則該符號的玩家勝利。若所有格子都被佔領但雙方皆為滿足獲勝條件,則平局。 --- ## 開始寫Code ---- [要模板點我](https://github.com/Tony041010/2022_CRC_Python_Project_Class_Template/tree/main/TicTacToe) 這次會用到的技巧 * `Class` 的各項工具:繼承, @staticmethod, ... * `Try and Except` 函式的運用 * `all()` 函式的運用 * `list` 的操作 * `in enumerate(list)`的使用 建議開一個資料夾叫做"TicTacToe" 這次會需要兩個.py的檔案 ---- 一、設定Player class 1. 創建一個`player.py`, 寫兩個玩家的class,都繼承Player class, 一個代表電腦,一個代表真人玩家 letter 代表這個玩家會用什麼符號玩遊戲('X','O') get_move是玩家決定下哪一步時執行的函式 ```python= class Player: def __init__(self, letter): # letter is x or o self.letter = letter # we want all players to get their next move def get_move(self, game): pass ``` ---- ```python= class RandomComputerPlayer(Player): def __init__(self, letter): super().__init__(letter) def get_move(self, game): pass class HumanPlayer(Player): def __init__(self, letter): super().__init__(letter) def get_move(self, game): pass ``` ---- 二、遊戲初步設定 1. 創建一個 `game.py`,寫一個TicTacToe的class並宣告一個list作為遊戲的棋盤, 再宣告current_winner追蹤遊戲進行時隨時可能出現的贏家 ```python= class TicTacToe: def __init__(self): self.board = [' ' for _ in range(9)] self.current_winner = None ``` ---- 2. print_board函式: 執行後當前棋盤的模樣就會被印出來 ```python= def print_board(self): # get the rows # for row in [[0,1,2], [3,4,5], [6,7,8]] for row in [self.board[i*3:(i+1)*3] for i in range(3)]: print('|' + ' | '.join(row) + '|') ``` <font color='red'>**重要:**</font>`i*3:(i+1)*3 for i in range(3)` 這個概念要很清楚,之後還會用到! ---- 3. print_board_nums函式: 印出各個位置代表的座標方便使用者放符號 這邊就需要 @staticmethod ```python= @staticmethod def print_board_nums(): # it's going to print 0 | 1 | 2 , etc # (tell us what number corresponds to what position) number_board = [ [str(i) for i in range(j*3, (j+1)*3)] for j in range(3)] # [ [0,1,2],[3,4,5],[6,7,8] ] for row in number_board: print('|' + ' | '.join(row) + '|') ``` ---- 4. available_moves函式: 告訴使用者現在哪邊還可以下 in enumerate(list) : 把list裡面每一項跟它的index用tuple(一種不能改變資料的陣列)包起來並分開 ```python= def available_moves(self): # it'll return [] moves = [] for (i,spot) in enumerate(self.board): #i代表第幾格,spot代表那一格的東西 # ['x', 'x', 'o'] -> [(0, 'x'), (1, 'x'), (2, 'o')] if spot ==' ': moves.append(i) return moves # You can also use this: # return [i for i, spot in enumerate(self.board) if spot == ' '] ``` ---- 5. 回到`player.py`,完成兩種玩家的get_move函式 ```python= import random #記得在程式一開始寫上它 # RandomComputerPlayer def get_move(self, game): # get a random valid move for its next move square = random.choice(game.available_moves()) return square ``` ---- **在寫HumanPlayer之前你要先知道...** try and except:程式跑一跑遇到錯誤就會全盤中止,但<font color='FFF300'>我們希望碰到錯誤時不用終止而是通知使用者他做錯了</font>,可以用try and except的方式處理 ```python= try: #他如果不是輸入數字這邊就會跳出ValueError a = int(input('輸入一個數字(1~10)')) if a > 10 or a < 1: raise ValueError #如果不在範圍就強制給他ValueError return True except ValueError: #如果有ValueError就會進來 print("你不是輸入1~10的數字喔,再試一次") print('離開try and except') #就算有ValueError他也會跑到這裡來,但其他種error就不會 ``` ---- HumanPlayer的get_move函式 ```python= #HumanPlayer def get_move(self, game): valid_square = False val = None #玩家指定的棋盤上的某格 while not valid_square: square = input(self.letter + '\'s turn. Input move(0-8): ') try: val = int(square) if val not in game.available_moves(): raise ValueError valid_square = True # If these are successful, then hurray! except ValueError: print('Invalid square. Try again.') # 當玩家選的格子是現在可以下的格子,就會到這裡 return val ``` 恭喜完成`player.py`裡面所有東西了 ---- 6. 寫 `play`函式 回到`game.py`,`在TicTacToe class`外面創一個`play`函式,執行他就可以開始遊戲 * game : TicTacToe class 的一個物件,可以想像成"遊戲" * x_player : 代表X的玩家 * o_player : 代表O的玩家 * print_game : 決定要不要印出數字1~9的盤面 ```python= def play(game, x_player, o_player, print_game=True): if print_game: print_board_nums() letter = 'x' #starting letter #以下開始遊戲 ``` ---- 遊戲流程: 1. 要放棋子的玩家決定下哪一步 2. 如果盤面還有空格,把符號放進棋盤裡面並印出現在盤面 3. 如果現在盤面產生贏家,遊戲結束並宣布贏家 如果沒有,換玩家下並繼續遊戲 重複 1 ~ 3直到贏家產生 OR 沒空格 因為2,我們需要判斷盤面有無空格的function以及放棋子進盤面的function ---- `empty_square(self)` : 回傳一個步林值代表現在盤面還有沒有空格,寫在`TicTacToe class` 裡面 ```python= def empty_square(self): return ' ' in self.board ``` ---- `make_move(self)` : 把現在玩家要放的符號放在他選擇的位置,如果操作可行,回傳True,否則就回傳False ```python= def make_move(self, square, letter): if self.board[square] == ' ': self.boaard[square] = letter # 等等還要塞別的,但先這樣 return True return False #理論上不會走到這裡,But just in case ``` ---- 兩個函式寫完就可以回來寫`play`函式 ```python= def play(game, x_player, o_player, print_game=True): if print_game: TicTacToe.print_board_nums() letter = 'X' #starting letter #從這邊繼續 while game.empty_square(): # get move from the current player if letter == 'X': square = x_player.get_move(game) else: square = o_player.get_move(game) #If make move,inform us if game.make_move(square, letter): print(letter + f'makes a move to square {square}.') game.print_board() print('') #換行 #等等還要塞別的 #switch player if letter == 'X': letter = 'O' else: letter = 'X' time.sleep(0.8) #延遲一下,增加遊戲體驗 #如果到這邊來表示棋盤沒空格,也沒有贏家 -> 平局 if print_game: print('It\'s a tie !') ``` ---- 此時整個遊戲快要完成了 只差最後一步:判定贏家 判定的時機應該是在<font color = 'red'>每一次</font>放棋子之後 所以我們可以在`if game.make_move(square, letter)`裡面塞入以下額外程式碼 ```python= if game.current_winner != None: #一開始設定為None,不是None就代表有贏家 if print_game: print(letter + 'wins!') return letter #遊戲結束 ``` ---- 因為要在<font color = 'red'>每一次</font>放棋子之後判斷,我們在`make_move(self, square, letter)`函式裡面呼叫偵測贏家的函式`winner(self, square, letter)` 在`make_move(self, square, letter)`裡面補上以下程式碼 ```python= if self.board[square] == ' ': self.board[square] = letter #補上這個 if self.winner(square, winner): self.current_winner = letter return True return False ``` ---- `self.winner(square, letter)`函式:判斷贏家 我們判斷三個地方: 1. 該square所在橫列 2. 該square所在直行 3. 該square所在對角線 (如果有) 把這個函式寫在`TicTacToe class`裡面 ```python= def winner(self, square, letter): # 1. Row # 2. Column # 3. Diagonal ``` ---- 1. 該square所在橫列 如果整列都同一個字母,則他為贏家 `all()`函式:該陣列裡面每一個元素是否都不為0或空格 ```python= row_ind = square // 3 #該square所在列數 row = self.board[row_ind*3: (row_ind_1)*3] if all([spot == letter for spot in row]): # if letter = 'X', ['O', ' ', 'X'] -> [0, 0, 1] return True ``` ---- 2. 該square所在直行 如果整行都同一個字母,則他為贏家 ```python= col_ind = square % 3 #該square所在行數 column = [self.board[col_ind + i*3] for i in range(3)] #col_ind + i*3 : 每加3就會換到同行的下一個位置 if all(spot == letter for spot in column): return True ``` ---- 3. 該square所在對角線 (如果有) \ 左上右下 : 第 0, 4, 8 格 / 右上左下 : 第 2, 4, 6 格 如果全部判斷完都沒有一個符合,則回傳False,表示現在還沒有獲勝的局面 ```python= if square % 2 == 0: diagonal1 = [self.board[i] for i in [0, 4, 8]] if all([spot == letter for spot in diagonal1]): return True diagonal2 = [self.board[i] for i in [2, 4, 6]] if all([spot == letter for spot in diagonal2]): return True return False ``` 恭喜完成Play函式了! ---- 7. main函式處理 有兩種版本可以玩: * 真人 V.S.電腦 * 真人 V.S 真人 * (也可以電腦V.S.電腦,你爽就好) ```python= if __name__ == '__main__': x_player = HumanPlayer('X') o_player = RandomComputerPlayer('O') T = TicTacToe() play(x_player, o_player, T, print_game=True) ``` 恭喜各位完成最基礎的TicTacToe!
{"metaMigratedAt":"2023-06-16T23:58:06.061Z","metaMigratedFrom":"YAML","title":"Python實作三:TicTacToe","breaks":true,"slideOptions":"{\"transition\":\"slide\",\"theme\":null}","contributors":"[{\"id\":\"4f731eff-9d88-41f4-af56-2e3e02f20cfc\",\"add\":8248,\"del\":213}]"}
    1517 views