實(shí)例2:井字棋
井字棋是一種在3 * 3格子上進(jìn)行的連珠游戲,又稱井字游戲。井字棋的游戲有兩名玩家,其中一個玩家畫圈,另一個玩家畫叉,輪流在3 * 3格子上畫上自己的符號,最先在橫向、縱向、或斜線方向連成一條線的人為勝利方。如圖1所示為畫圈的一方為勝利者。
?
圖1 井字棋
本實(shí)例要求編寫程序,實(shí)現(xiàn)具有人機(jī)交互功能的井字棋。
實(shí)例目標(biāo)
- 理解面向?qū)ο蟮乃枷?/li>
- 能獨(dú)立設(shè)計類
- 掌握類的繼承和父類方法的重寫
實(shí)例分析
根據(jù)實(shí)例描述的井字棋游戲的規(guī)則,下面模擬一次游戲的流程如圖2所示。
?
圖2 井字棋游戲流程
圖2中的描述的游戲流程如下:
- 重置棋盤數(shù)據(jù),清理之前一輪的對局?jǐn)?shù)據(jù),為本輪對局做好準(zhǔn)備。
- 顯示棋盤上每個格子的編號,讓玩家熟悉落子位置。
- 根據(jù)系統(tǒng)隨機(jī)產(chǎn)生的結(jié)果確定先手玩家(先手使用X)。
- 當(dāng)前落子一方落子。
- 顯示落子后的棋盤。
- 判斷落子一方是否勝利?若落子一方取得勝利,修改玩家得分,本輪對局結(jié)束,跳轉(zhuǎn)至第(9)步。
- 判斷是否和棋?若出現(xiàn)和棋,本輪對局結(jié)束,跳轉(zhuǎn)至第(9)步。
- 交換落子方,跳轉(zhuǎn)至第(4)步,繼續(xù)本輪游戲。
- 顯示玩家當(dāng)前對局比分。
以上流程中,落子是游戲中的核心功能,如何落子則是體現(xiàn)電腦智能的關(guān)鍵步驟,實(shí)現(xiàn)智能落子有策略可循的。按照井字棋的游戲規(guī)則:當(dāng)玩家每次落子后,玩家的棋子在棋盤的水平、垂直或者對角線任一方向連成一條直線,則表示玩家獲勝。因此,我們可以將電腦的落子位置按照優(yōu)先級分成以下三種:
(1)必勝落子位置
我方在該位置落子會獲勝。一旦出現(xiàn)這種情況,顯然應(yīng)該毫不猶豫在這個位置落子。
(2)必救落子位置
對方在該位置落子會獲勝。如果我方暫時沒有必勝落子位置,那么應(yīng)該在必救落子位置落子,以阻止對方獲勝。
(3)評估子力價值
評估子力價值,就是如果在該位置落子獲勝的幾率越高,子力價值就越大;獲勝的幾率越低,子力價值就越小。
如果當(dāng)前的棋盤上,既沒有必勝落子位置,也沒有必救落子位置,那么就應(yīng)該針對棋盤上的每一個空白位置尋找子力價值最高的位置落子。
要編寫一個評估子力價值的程序,需要考慮諸多因素,這里我們選擇了一種簡單評估子力價值的方式——只考慮某個位置在空棋盤上的價值,而不考慮已有棋子以及落子之后的盤面變化。下面來看一下在空棋盤上不同位置落子的示意圖,如圖3所示。
?
圖3 棋盤落子示意圖
觀察圖3不難發(fā)現(xiàn),玩家在空棋盤上落子的位置可分為以下3種情況:
- 中心點(diǎn),這個位置共有4個方向可能和其它棋子連接成直線,獲勝的幾率最高。
- 4個角位,這4個位置各自有3個方向可能和其它棋子連接成直線,獲勝幾率中等。
- 4個邊位,這4個位置各自有2個方向可能和其它棋子連接成直線,獲勝幾率最低。
綜上所述,如果電腦在落子時,既沒有必勝落子位置,也沒有必救落子位置時,我們就可以讓電腦按照勝率的高低來選擇落子位置,也就是說,若棋盤的中心點(diǎn)沒有棋子,則選擇中心點(diǎn)作為落子位置;若中心點(diǎn)已有棋子,而角位沒有棋子,則隨機(jī)選擇一個沒有棋子的角位作為落子位置;若中心點(diǎn)和四個角位都有棋子,而邊位沒有棋子,則隨機(jī)選擇一個沒有棋子的邊位作為落子位置。
井字棋游戲一共需要設(shè)計4個類,不同的類創(chuàng)建的對象承擔(dān)不同的職責(zé),分別是:
(1)游戲類(Game):負(fù)責(zé)整個游戲流程的控制,是該游戲的入口。
(2)棋盤類(Board):負(fù)責(zé)顯示棋盤、記錄本輪對局?jǐn)?shù)據(jù)、以及判斷勝利等和對弈相關(guān)的處理工作。
(3)玩家類(Player):負(fù)責(zé)記錄玩家姓名、棋子類型和得分、以及實(shí)現(xiàn)玩家在棋盤上落子。
(4)電腦玩家類(AIPlayer):是玩家類的子類。在電腦玩家類類中重寫玩家類的落子方法,在重寫的方法中實(shí)現(xiàn)電腦智能選擇落子位置的功能。
設(shè)計后的類圖如圖4所示。
?
圖4 類結(jié)構(gòu)圖
本實(shí)例中涉及到多個類,為保證程序具有清晰的結(jié)構(gòu),可以將每個類的相關(guān)代碼分別放置到與其同名的py文件中。另外,由于Player和AIPlayer類具有繼承關(guān)系,可以將這兩個類的代碼放置到player.py文件中。
代碼實(shí)現(xiàn)
本實(shí)例的實(shí)現(xiàn)過程如下所示。
- 創(chuàng)建項目
使用PyCharm創(chuàng)建一個名為“井字棋V1.0”的文件夾,在該文件夾下分別創(chuàng)建3個py文件,分別為board.py、game.py與player.py,此時程序的目錄結(jié)構(gòu)如圖5所示。
?
圖5 井字棋文件目錄
由于棋盤類是井字棋游戲的重點(diǎn),因此我們先開發(fā)Board類。
- 設(shè)計Board類
(1)屬性
井字棋的棋盤上共有9個格子落子,落子也是有位置可循的,因此這里使用列表作為棋盤的數(shù)據(jù)結(jié)構(gòu),列表中的元素則是棋盤上的棋子,它有以下三種取值:
- " " 表示沒有落子,是初始值;
- "X" 表示玩家在該位置下了一個X的棋子;
- "O" 表示玩家在該位置下了一個O的棋子。
其中列表中的元素為" "的位置才允許玩家落子。為了讓玩家明確可落子的位置,需要增加可落子列表。根據(jù)圖4中設(shè)計的類圖,在board.py文件中定義Board類,并在該類的構(gòu)造方法中添加屬性board_data和movable_list,具體代碼如下。
class Board(object):
??? """棋盤類"""
??? def __init__(self):
??????? self.board_data = [" "] * 9? ????????# 棋盤數(shù)據(jù)
??????? self.movable_list = list(range(9))? # 可移動列表
(2)show_board()方法
show_board()方法實(shí)現(xiàn)創(chuàng)建一個九宮格棋盤的功能。游戲過程中顯示的棋盤分為兩種情況,一種是新一輪游戲開始前顯示的有索引的棋盤,讓玩家明確棋盤格子與序號的對應(yīng)關(guān)系;另一種是游戲中顯示當(dāng)前落子情況的棋盤,會在玩家每次落子后展示。在Board類中添加show_board()方法,并在該方法中傳遞一個參數(shù)show_index,用于設(shè)置是否在棋盤中顯示索引(默認(rèn)為False,表示不顯示索引),具體代碼如下。
def show_board(self, show_index=False):
??? """顯示棋盤
??? :param show_index: True 表示顯示索引 / False 表示顯示數(shù)據(jù)
??? """
??? for i in (0, 3, 6):
??????? print("?????? |?????? |")
??????? if show_index:
??????????? print("?? %d?? |?? %d?? |?? %d" % (i, i + 1, i + 2))
??????? else:
??????????? print("?? %s?? |?? %s?? |?? %s" % (self.board_data[i],
?????????????? ??????????????????????????????self.board_data[i + 1],
?????????????? ??????????????????????????????self.board_data[i + 2]))
??????? print("?????? |?????? |")
??????? if i != 6:
??????????? print("-" * 23)
(3)move_down ()方法
move_down ()方法實(shí)現(xiàn)在指定的位置落子的功能,該方法接收兩個參數(shù),分別是表示落子位置的index和表示落子類型(X或者O)的chess,接收的這些參數(shù)都是落子前需要考慮的必要要素,具體代碼如下。
def move_down(self, index, chess):
??? """在指定位置落子
??? :param index: 列表索引
??? :param chess: 棋子類型 X 或 O
??? """
??? # 1. 判斷 index 是否在可移動列表中
??? if index not in self.movable_list:
??????? print("%d 位置不允許落子" % index)
??????? return
??? # 2. 修改棋盤數(shù)據(jù)
??? self.board_data[index] = chess
??? # 3. 修改可移動列表
??? self.movable_list.remove(index)
以上代碼首先判斷落子位置是否可以落子,如果可以就將棋子添加到board_data列表的對應(yīng)位置,并從movable_list列表中刪除。
(4)is_draw ()方法
is_draw ()方法實(shí)現(xiàn)判斷游戲是否平局的功能,該方法會查看可落子索引列表中是否有值,若沒有值表示棋盤中的棋子已經(jīng)落滿了,說明游戲平局,具體代碼如下。
def is_draw(self):
??? """是否平局"""
??? return not self.movable_list
(5)is_win ()方法
is_draw ()方法實(shí)現(xiàn)判斷游戲是否勝利的功能,該方法會先定義方向列表,再遍歷方向列表判斷游戲是否勝利,勝利則返回True,否則返回False,具體代碼如下。
def is_win(self, chess, ai_index=-1):
?? ?"""是否勝利
??? :param chess: 玩家的棋子
??? :param ai_index: 預(yù)判索引,-1 直接判斷當(dāng)前棋盤數(shù)據(jù)
??? """
??? # 1. 定義檢查方向列表
??? check_dirs = [[0, 1, 2], [3, 4, 5], [6, 7, 8],
????????????????? ???[0, 3, 6], [1, 4, 7], [2, 5, 8],
????????????????? ???[0, 4, 8], [2, 4, 6]]
??? # 2. 定義局部變量記錄棋盤數(shù)據(jù)副本
??? data = self.board_data.copy()
??? # 判斷是否預(yù)判勝利
??? if ai_index > 0:
??????? data[ai_index] = chess
??? # 3. 遍歷檢查方向列表判斷是否勝利
??? for item in check_dirs:
??????? if (data[item[0]] == chess and
??????????? data[item[1]] == chess
????????????? ??and data[item[2]] == chess):
??????????? return True
??? return False
注意,is_win()方法的ai_index參數(shù)的默認(rèn)值為-1,表示無需進(jìn)行預(yù)判,即提示玩家最有利的落子位置;若該參數(shù)不為-1時,表示需要進(jìn)行預(yù)判。
(6)reset_board ()方法
reset_board ()方法實(shí)現(xiàn)清空棋盤的功能,該方法中會先清空movable_list,再將棋盤上的數(shù)據(jù)全部置為初始值,最后往movable_list中添加0~8的數(shù)字,具體代碼如下。
def reset_board(self):
??? """重置棋盤"""
??? # 1. 清空可移動列表數(shù)據(jù)
??? self.movable_list.clear()
??? # 2. 重置數(shù)據(jù)
??? for i in range(9):
??????? self.board_data[i] = " "
??????? self.movable_list.append(i)
- 設(shè)計Player類
根據(jù)圖4中設(shè)計的類圖,在player.py文件中定義Player類,分別在該類中添加屬性和方法,具體內(nèi)容如下。
(1)屬性
在Player類中添加name、score、chess屬性,具體代碼如下。
import board
import random
class Player(object):
??? """玩家類"""
??? def __init__(self, name):
??????? self.name = name? ???# 姓名
??????? self.score = 0? ?????# 成績
??????? self.chess = None? ?# 棋子
(2)move()方法
move()方法實(shí)現(xiàn)玩家在指定位置落子的功能,該方法中會先提示用戶棋盤上可落子的位置,之后使棋盤根據(jù)用戶選擇的位置重置棋盤數(shù)據(jù)后進(jìn)行顯示,具體代碼如下。
def move(self, chess_board):
??? """在棋盤上落子
??? :param chess_board:
??? """
??? # 1. 由用戶輸入要落子索引
??? index = -1
??? while index not in chess_board.movable_list:
??????? try:
?? ?????????index = int(input("請 “%s” 輸入落子位置 %s:" %
??????????? ????(self.name, chess_board.movable_list)))
??????? except ValueError:
??????????? pass
??? # 2. 在指定位置落子
??? chess_board.move_down(index, self.chess)
- 設(shè)計AIPlayer類
根據(jù)圖4中設(shè)計的類圖,在player.py文件中定義繼承自Player類的子類AIPlayer。AIPlayer類中重寫了父類的move()方法,在該方法中需要增加分析中的策略,使得計算機(jī)玩家變得更加聰明,具體代碼如下。
class AIPlayer(Player):
??? """智能玩家"""
??? def move(self, chess_board):
??????? """在棋盤上落子
??????? :param chess_board:
??????? """
??????? print("%s 正在思考落子位置..." % self.name)
??? ????# 1. 查找我方必勝落子位置
??????? for index in chess_board.movable_list:
??????????? if chess_board.is_win(self.chess, index):
??????????????? print("走在 %d 位置必勝?。?!" % index)
??????????????? chess_board.move_down(index, self.chess)
??????????????? return
??????? # 2. 查找地方必勝落子位置-我方必救位置
??????? other_chess = "O" if self.chess == "X" else "X"
??????? for index in chess_board.movable_list:
??????????? if chess_board.is_win(other_chess, index):
??????????????? print("敵人走在 %d 位置必輸,火速堵上!" % index)
?????????????? ?chess_board.move_down(index, self.chess)
??????????????? return
??????? # 3. 根據(jù)子力價值選擇落子位置
??????? index = -1
??????? # 沒有落子的角位置列表
??????? corners = list(set([0, 2, 6, 8]).intersection(
chess_board.movable_list))
??????? # 沒有落子的邊位置列表
??????? edges = list(set([1, 3, 5, 7]).intersection(
chess_board.movable_list))
??????? if 4 in chess_board.movable_list:
??????????? index = 4
??????? elif corners:
??????????? index = random.choice(corners)
??????? elif edges:
??????????? index = random.choice(edges)
??????? # 在指定位置落子
??????? chess_board.move_down(index, self.chess)
- 設(shè)計Game類
根據(jù)圖4中設(shè)計的類圖,在game.py文件中定義Game類,分別在該類中添加屬性和方法,具體內(nèi)容如下。
(1)屬性
在Game類中添加chess_board、human、computer屬性,具體代碼如下。
import random
import board
import player
class Game(object):
??? """游戲類"""
??? def __init__(self):
??????? self.chess_board = board.Board()? ????????# 棋盤對象
??????? self.human = player.Player("玩家")? ??????# 人類玩家對象
??????? self.computer = player.AIPlayer("電腦")? # 電腦玩家對象
(2)random_player()方法
random_player()方法實(shí)現(xiàn)隨機(jī)生成先手玩家的功能,該方法中會先隨機(jī)生成0和1兩個數(shù),選到數(shù)字1的玩家為先手玩家,然后再為兩個玩家設(shè)置棋子類型,即先手玩家為“X”,對手玩家為“O”,具體代碼如下。
def random_player(self):
??? """隨機(jī)先手玩家
??? :return: 落子先后順序的玩家元組
??? """
??? # 隨機(jī)到 1 表示玩家先手
??? if random.randint(0, 1) == 1:
??????? players = (self.human, self.computer)
??? else:
??????? players = (self.computer, self.human)
??? # 設(shè)置玩家棋子
??? players[0].chess = "X"
??? players[1].chess = "O"
??? print("根據(jù)隨機(jī)抽取結(jié)果 %s 先行" % players[0].name)
??? return players
(3)play_round ()方法
play_round ()方法實(shí)現(xiàn)一輪完整對局的功能,該方法的邏輯可按照實(shí)例分析的一次流程完成,具體代碼如下。
def play_round(self):
?? ?"""一輪完整對局"""
??? # 1. 顯示棋盤落子位置
??? self.chess_board.show_board(True)
??? # 2. 隨機(jī)決定先手
??? current_player, next_player = self.random_player()
??? # 3. 兩個玩家輪流落子
??? while True:
??????? # 下子方落子
??????? current_player.move(self.chess_board)
??????? # 顯示落子結(jié)果
? ??????self.chess_board.show_board()
??????? # 是否勝利?
??????? if self.chess_board.is_win(current_player.chess):
??????????? print("%s 戰(zhàn)勝 %s" % (current_player.name, next_player.name))
??????????? current_player.score += 1
??????????? break
??????? # 是否平局
?? ?????if self.chess_board.is_draw():
??????????? print("%s 和 %s 戰(zhàn)成平局" % (current_player.name,
next_player.name))
??????????? break
??????? # 交換落子方
??????? current_player, next_player = next_player, current_player
??? # 4. 顯示比分
??? print("[%s] 對戰(zhàn) [%s] 比分是 %d : %d" % (self.human.name,
??????????????????????????????????????? self.computer.name,
??????????????????????????????????????? self.human.score,
??????????????????????????????????????? self.computer.score))
從上述代碼可以看出,大部分的功能都是通過游戲中各個對象訪問屬性或調(diào)用方法實(shí)現(xiàn)的,這正好體現(xiàn)了類的封裝性的特點(diǎn),即每個類分工完成各自的任務(wù)。
(4)start ()方法
start ()方法實(shí)現(xiàn)循環(huán)對局的功能,該方法中會在每輪對局結(jié)束之后詢問玩家是否再來一局,若玩家選擇是,則重置棋盤數(shù)據(jù)后開始新一輪對局;若玩家選擇否,則會退出游戲,具體代碼如下。
def start(self):
??? """循環(huán)開始對局"""
??? while True:
??????? # 一輪完整對局
??????? self.play_round()
??????? # 詢問是否繼續(xù)
??????? is_continue = input("是否再來一盤(Y/N)?").upper()
??????? # 判斷玩家輸入
??????? if is_continue != "Y":
??????????? break
??????? # 重置棋盤數(shù)據(jù)
??????? self.chess_board.reset_board()
最后在game.py文件中通過Game類對象調(diào)用start()方法啟動井字棋游戲,具體代碼如下。
if __name__ == '__main__':
??? Game().start()
代碼測試
運(yùn)行程序,對戰(zhàn)一局游戲的結(jié)果如下所示:
?????? |?????? |
?? 0?? |?? 1?? |?? 2
?????? |?????? |
-----------------------
?????? |?????? |
?? 3?? |?? 4?? |?? 5
?????? |?????? |
-----------------------
?????? |?????? |
?? 6?? |?? 7?? |?? 8
?????? |?????? |
根據(jù)隨機(jī)抽取結(jié)果 電腦 先行
電腦 正在思考落子位置...
?????? |?????? |
?????? |?????? |???
?????? |?????? |
-----------------------
?????? |?????? |
?????? |?? X?? |???
?????? |?????? |
-----------------------
?????? |?????? |
?????? |?????? |???
?????? |?????? |
請 “玩家” 輸入落子位置 [0, 1, 2, 3, 5, 6, 7, 8]:0
?????? |?????? |
?? O?? |?????? |???
?????? |?????? |
-----------------------
?????? |?????? |
?????? |?? X?? |???
?????? |?????? |
-----------------------
?????? |?????? |
?????? |?????? |???
?????? |?????? |
電腦 正在思考落子位置...
?????? |?????? |
?? O?? |?????? |???
?????? |?????? |
-----------------------
?????? |?????? |
?????? |?? X?? |???
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?????? |???
?????? |?????? |
請 “玩家” 輸入落子位置 [1, 2, 3, 5, 7, 8]:2
?????? |?????? |
?? O?? |?????? |?? O
?????? |?????? |
-----------------------
?????? |?????? |
?????? |?? X?? |???
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?????? |???
?????? |?????? |
電腦 正在思考落子位置...
敵人走在 1 位置必輸,火速堵上!
?????? |?????? |
?? O?? |?? X?? |?? O
???? ??|?????? |
-----------------------
?????? |?????? |
?????? |?? X?? |???
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?????? |???
?????? |?????? |
請 “玩家” 輸入落子位置 [3, 5, 7, 8]:7
?????? |?????? |
?? O?? |?? X?? |?? O
?????? |?????? |
-----------------------
?????? |?????? |
?????? |?? X?? |???
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?? O?? |???
?????? |?????? |
電腦 正在思考落子位置...
?????? |?????? |
?? O?? |?? X?? |?? O
?????? |?????? |
-----------------------
????? ?|?????? |
?????? |?? X?? |???
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?? O?? |?? X
?????? |?????? |
請 “玩家” 輸入落子位置 [3, 5]:5
?????? |?????? |
?? O?? |?? X?? |?? O
?????? |?????? |
-----------------------
?????? |?????? |
?????? | ??X?? |?? O
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?? O?? |?? X
?????? |?????? |
電腦 正在思考落子位置...
?????? |?????? |
?? O?? |?? X?? |?? O
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?? X?? |?? O
?????? |?????? |
-----------------------
?????? |?????? |
?? X?? |?? O?? |?? X
?????? |?????? |
電腦 和 玩家 戰(zhàn)成平局
[玩家] 對戰(zhàn) [電腦] 比分是 0 : 0文章來源:http://www.zghlxwxcb.cn/news/detail-758156.html
是否再來一盤(Y/N)?n文章來源地址http://www.zghlxwxcb.cn/news/detail-758156.html
到了這里,關(guān)于井字棋--課后程序(Python程序開發(fā)案例教程-黑馬程序員編著-第7章-課后作業(yè))的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!