# 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}]"}