python3 井字棋 GUI - 人機對戰、機器對戰
功能
- GUI界面
- 人機對戰(可選擇機器先走)
- 機器對戰(50局)
流程圖
內核
棋盤
[0][1][2]
[3][4][5]
[6][7][8]
最佳下棋順序:
best_way = [4,0,2,6,8,1,3,5,7]
估價函數(以X為對象)
- 可以贏的行數 +1
- 可以贏的行數上有自己的棋子 +2
- 可導致自己贏 +2
- 可導致對手贏 -2
判斷贏局
win_chess = [[0,4,8],[2,4,6],[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8]]
人機對戰流程
人(X)點擊某個格子,觸發綁定事件
- 判斷該格子是否有子,無子繼續
- 設置該格子為
1
- 判斷X是否贏了,沒有繼續,有X贏
- 判斷棋盤是否無子可下,有繼續,沒有平局
- 輪到機器下棋
- 判斷O是否差一子便可贏,有則對應格子,無則繼續
- 判斷O是否處於危險狀態,即對方只差一子贏棋,有則選擇該格子,無則繼續
- 機器選擇最佳格子
- 判斷輸贏並判斷是有無子可下,循環
機器對戰流程
- 隨機產生先下棋者
- 第一顆棋子隨機下
- 各自判斷最佳走法
- 判斷輸贏即棋盤是否無子可下
- 循環
總結
1. 學習了python threading庫的用法
線程的使用:
id = 1
th = []
for i in range(50):
id = id * -1
try:
th.append(threading.Thread(target=run,args=(i,id)))
th[i].start()
except Exception as e:
print(e)
i = i - 1
2. 學習了python tkinter庫的用法
tkinter的mianloop做為主線程盡量避免被阻塞,以免界面卡死
創建窗口:
top = tk.Tk()#創建窗口
top.title('井字棋 -> Fighting')#標題
top.geometry("300x300")#大小
top.resizable()#可改變大小
創建Frame:
frame_top = tk.Frame(top)#top是上層
創建按鈕:
tk.Button(frame_top,text='人機對決',command=but1).pack(side=tk.LEFT)
創建labe:
label1 = tk.Label(frame_cont,justify=tk.CENTER,textvariable=show_str,font=("幼圓",30))
顯示可刷新變量:
tips = tk.StringVar(top) #提示信息
tips.set("")#設置顯示內容
label_bottom = tk.Label(frame_bot,justify=tk.CENTER,textvariable=tips,font=("幼圓",20),padx=0)#設置顯示的值為tips
綁定事件及解綁:
l0.bind("<Button-1>", touch_l0)#綁定
l0.unbind("<Button-1>")#解綁
布局:
l0.pack(side=tk.LEFT)
frame_top.pack()
開啟消息循環:
top.mainloop()
代碼
運行截圖
待修復問題
若產生平局會導致該線程卡死 , 即count_z無法計算,並造成卡頓- 程序優化不夠,代碼較為雜亂
上一個問題導致通過GUI關閉程序會有進程仍在跑,需要用任務管理器關閉- 先走角落易贏,機器走法單一,即下面情況
X _ O
O O _
X X X
補充:之前之所以會卡,是因為在計算下一步時,如果只剩下3或2個格子時,無法返回下一步的值,導致棋沒有下,外層循環又是
while True
,便導致死循環。針對這個問題我添加了具體的改進,已修復。
運行
gui.py
即可
chess.py
內核部分
#coding=utf-8
"""
[0,1,2]
[3,4,5]
[6,7,8]
"""
#勝利的走法
win_chess = [[0,4,8],[2,4,6],[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8]]
#最佳下棋順序
best_way = [4,0,2,6,8,1,3,5,7]
#棋盤
chess = [0,0,0,0,0,0,0,0,0]
def is_win(now_chess,who):
"""
判斷游戲方(who)是否贏局
"""
temp = now_chess[:]
for w_c in win_chess:
if temp[w_c[0]] == who and temp[w_c[1]] == who and temp[w_c[2]] == who :
return who
return 0
def count_zero(now_chess):
"""
統計剩余格子
返回個數
"""
temp = now_chess[:]
count = 0
for te in temp:
if te == 0:
count = count + 1
return count
def evaluation(now_chess):
"""
估價函數(以X為對象)
可以贏的行數 +1
可以贏的行數上有自己的棋子 +2
可導致自己贏 +2
可導致對手贏 -2
"""
temp = now_chess[:]
count = 0
for w_c in win_chess:
if temp[w_c[0]] >= 0 and temp[w_c[1]] >= 0 and temp[w_c[2]] >= 0 :
if temp[w_c[0]] == 1 or temp[w_c[1]] == 1 or temp[w_c[2]] == 1 :
count += 1
count += 1
if is_win(temp,1) == 1:
count = count + 2
if is_win(temp,-1) == -1:
count = count - 2
return count
def all_go(now_chess,who):
"""
遍歷所有走法
"""
temp = now_chess[:]
tempp = []
for i in best_way:
if temp[i] == 0:
temppp = temp[:]
temppp[i]=who
tempp.append([temppp,i])
return tempp
def get_next_x(now_chess,who):
"""
x獲取下一個位置
"""
temp = now_chess[:]
best_list = None
best_one = -1
if count_zero(temp) <= 3 :
for te in all_go(temp,who):
if best_one == -1:
best_list = te[0]
best_one = te[1]
else :
if evaluation(te[0]) > evaluation(best_list):
best_list = te[0]
best_one = te[1]
return best_one
for te in all_go(temp,who):
for tee in all_go(te[0],who*-1):
for teee in all_go(tee[0],who):
if best_list is None:
best_list = teee[0]
best_one = te[1]
else:
if evaluation(teee[0]) > evaluation(best_list) :
best_list = teee[0]
best_one = te[1]
return best_one
def get_next_o(now_chess,who):
"""
o獲取下一個位置
"""
temp = now_chess[:]
best_list = None
best_one = -1
if count_zero(temp) <= 2 :
for te in all_go(temp,who):
if best_one == -1:
best_list = te[0]
best_one = te[1]
else :
if evaluation(te[0]) < evaluation(best_list):
best_list = te[0]
best_one = te[1]
return best_one
for te in all_go(temp,who):
for tee in all_go(te[0],who*-1):
if best_list is None:
best_list = tee[0]
best_one = te[1]
else:
if evaluation(tee[0]) < evaluation(best_list) :
best_list = tee[0]
best_one = te[1]
return best_one
def is_danger(now_chess,who=0):
"""
判斷自己是否處於危險狀態(即 對手可能已經差一子贏局)
"""
temp = now_chess[:]
for te in all_go(temp,who*-1):
if is_win(te[0],who*-1) == who*-1:
return te[1]
return -1
if __name__ == "__main__":
"""
測試用
"""
chess = [0,0,0,\
0,1,0,\
0,0,0]
#print(get_next_old(chess,-1,1))
#print(all_go(chess,1))
print(get_next_o(chess,-1))
gui.py
圖形及控制 部分
#coding=utf-8
"""
"""
import tkinter as tk
import time
import threading
import random
import chess
init_chess = [0,0,0,0,0,0,0,0,0] #原始棋盤
the_chess = [0,0,0,0,0,0,0,0,0] #記錄棋盤
show_chess = ''
flag = True
who = 1
count_x = 0
count_y = 0
count_z = 0
top = tk.Tk()
top.title('井字棋 -> Fighting')
top.geometry("300x300")
top.resizable()
show_str = tk.StringVar(top)
tips = tk.StringVar(top) #提示信息
#初始化棋盤信息
ch = []
for i in range(9):
ch.append(tk.StringVar(top))
#初始化提示信息
tips.set("")
frame_top = tk.Frame(top)
frame_cont = tk.Frame(top)
frame_bot = tk.Frame(top)
frame_cont1 = tk.Frame(frame_cont)
frame_cont2 = tk.Frame(frame_cont)
frame_cont3 = tk.Frame(frame_cont)
label1 = tk.Label(frame_cont,justify=tk.CENTER,textvariable=show_str,font=("幼圓",30))
# 棋盤顯示label 0~9
l0 = tk.Label(frame_cont1,textvariable=ch[0],font=("幼圓",30),padx=0)
l1 = tk.Label(frame_cont1,textvariable=ch[1],font=("幼圓",30),padx=0)
l2 = tk.Label(frame_cont1,textvariable=ch[2],font=("幼圓",30),padx=0)
l3 = tk.Label(frame_cont2,textvariable=ch[3],font=("幼圓",30),padx=0)
l4 = tk.Label(frame_cont2,textvariable=ch[4],font=("幼圓",30),padx=0)
l5 = tk.Label(frame_cont2,textvariable=ch[5],font=("幼圓",30),padx=0)
l6 = tk.Label(frame_cont3,textvariable=ch[6],font=("幼圓",30),padx=0)
l7 = tk.Label(frame_cont3,textvariable=ch[7],font=("幼圓",30),padx=0)
l8 = tk.Label(frame_cont3,textvariable=ch[8],font=("幼圓",30),padx=0)
label_bottom = tk.Label(frame_bot,justify=tk.CENTER,textvariable=tips,font=("幼圓",20),padx=0)
def update_chess():
"""
更新棋盤
"""
for i in range(9):
if the_chess[i] == 1 :
ch[i].set('|X|')
elif the_chess[i] == -1 :
ch[i].set('|O|')
else :
ch[i].set('| |')
#print(i)
def init_ch():
"""
初始化棋盤
"""
for i in range(9):
the_chess[i] = init_chess[i]
update_chess()
return the_chess
def ai_go_first():
if chess.count_zero(the_chess) == 9:
the_chess[random.randint(0,8)] = -1
update_chess()
forget()
ai_go_fir_b = tk.Button(frame_cont,text='機器先下',command=ai_go_first)
def forget():
ai_go_fir_b.pack_forget()
def but1():
"""
人機對戰
"""
flag = True
init_ch()
tips.set("人機對戰模式")
l0.bind("<Button-1>", touch_l0)
l1.bind("<Button-1>", touch_l1)
l2.bind("<Button-1>", touch_l2)
l3.bind("<Button-1>", touch_l3)
l4.bind("<Button-1>", touch_l4)
l5.bind("<Button-1>", touch_l5)
l6.bind("<Button-1>", touch_l6)
l7.bind("<Button-1>", touch_l7)
l8.bind("<Button-1>", touch_l8)
ai_go_fir_b.pack(side=tk.TOP)
def run(i,id):
"""
創建一個機器對打局
"""
new_chess = init_chess[:]
global count_x
global count_y
global count_z
if id == 1 :
new_chess[random.randint(0,8)] = -1
else :
new_chess[random.randint(0,8)] = 1
x = 0
for x in range(10):
if chess.count_zero(new_chess) > 0 :
#print(chess.count_zero(new_chess))
if id == 1:
#print('*****')
#print(chess.get_next_x(new_chess,id))
pos = chess.get_next_x(new_chess,id)
if pos != -1 :
new_chess[int(chess.get_next_x(new_chess,id))] = id
else :
for xx in range(9):
if new_chess[xx] == 0 :
new_chess[xx] = id
else :
pos = chess.get_next_o(new_chess,id)
if pos != -1 :
new_chess[int(chess.get_next_o(new_chess,id))] = id
else :
for xx in range(9):
if new_chess[xx] == 0 :
new_chess[xx] = id
id = id * -1
if chess.is_win(new_chess,id) == id :
name = ''
if id == 1 :
name = 'X'
update_chess()
print("第 {} 局 : {} 贏了!".format(i+1,name) + ' ' + str(new_chess))
tips.set("第 {} 局 : {} 贏了!".format(i+1,name))
threading.Lock()
count_x = count_x + 1
threading.RLock()
time.sleep(3)
break
else :
name = 'O'
update_chess()
print("第 {} 局 : {} 贏了!".format(i+1,name) + ' ' + str(new_chess))
tips.set("第 {} 局 : {} 贏了!".format(i+1,name))
threading.Lock()
count_y = count_y + 1
threading.RLock()
time.sleep(3)
break
elif chess.is_win(new_chess,id*-1) == id*-1 :
id = id * -1
name = ''
if id == 1 :
name = 'X'
print("第 {} 局 : {} 贏了!".format(i+1,name) + ' ' + str(new_chess))
tips.set("第 {} 局 : {} 贏了!".format(i+1,name))
threading.Lock()
count_x = count_x + 1
threading.RLock()
break
else :
name = 'O'
print("第 {} 局 : {} 贏了!".format(i+1,name) + ' ' + str(new_chess))
tips.set("第 {} 局 : {} 贏了!".format(i+1,name))
threading.Lock()
count_y = count_y + 1
threading.RLock()
break
elif chess.count_zero(new_chess) == 0:
print("第 {} 局 : 平局".format(i+1) + ' ' + str(new_chess))
tips.set("第 {} 局 : 平局".format(i+1))
threading.Lock()
count_z = count_z + 1
threading.RLock()
break
else :
pass
else :
print("第 {} 局 : 平局".format(i+1) + ' ' + str(new_chess))
tips.set("第 {} 局 : 平局".format(i+1))
threading.Lock()
count_z = count_z + 1
threading.RLock()
break
#print(str(i + 1) + ' ' + str(new_chess))
'''if i == 9:
print("第 {} 局 : 平局".format(i+1) + ' ' + str(new_chess))
tips.set("第 {} 局 : 平局".format(i+1))
threading.Lock()
count_z = count_z + 1
threading.RLock()'''
time.sleep(3)
for i in range(9):
the_chess[i] = new_chess[i]
update_chess()
threading.Lock()
tips.set("50局已經結束!\nX 共贏 {}次\nO 共贏 {}次\n平局 {} 次".format(count_x,count_y,count_z))
threading.RLock()
def but2():
"""
機器對戰
"""
print(" ")
ai_go_fir_b.pack_forget()
flag = False
global count_x
global count_y
global count_z
count_x = 0
count_y = 0
count_z = 0
init_ch()
tips.set("機器對戰模式")
l0.unbind("<Button-1>")
l1.unbind("<Button-1>")
l2.unbind("<Button-1>")
l3.unbind("<Button-1>")
l4.unbind("<Button-1>")
l5.unbind("<Button-1>")
l6.unbind("<Button-1>")
l7.unbind("<Button-1>")
l8.unbind("<Button-1>")
id = 1
th = []
for i in range(50):
id = id * -1
try:
th.append(threading.Thread(target=run,args=(i,id)))
th[i].start()
except Exception as e:
print(e)
i = i - 1
#tips.set("50 局已經結束! X 共贏 {}次, O 共贏 {}次, 平局 {} 次".format(count_x,count_y,count_z))
def ai_go(w):
"""
機器走棋 O
"""
if chess.count_zero(the_chess) < 9:
po = chess.is_danger(the_chess,1)
if po != -1 :
the_chess[po] = w
update_chess()
elif constraint(w) == False:
pass
else :
the_chess[chess.get_next_o(the_chess,-1)] = w
update_chess()
if chess.is_win(the_chess,-1) == -1:
tips.set("你輸了!")
if chess.count_zero(the_chess) == 0:
tips.set("平局!")
def constraint(w):
"""
判斷是否處於危險狀態
"""
po = chess.is_danger(the_chess,-1)
if po != -1:
the_chess[po] = w
update_chess()
return False
return True
def peo_go(po):
"""
獲取人們按鍵,並下棋
"""
if the_chess[po] == 0 :
the_chess[po] = who
update_chess()
if chess.is_win(the_chess,who) == who:
tips.set('你贏了!')
elif chess.count_zero(the_chess) == 0:
tips.set("平局!")
else :
ai_go(who*-1)
def touch_l0(e):
peo_go(0)
def touch_l1(e):
peo_go(1)
def touch_l2(e):
peo_go(2)
def touch_l3(e):
peo_go(3)
def touch_l4(e):
peo_go(4)
def touch_l5(e):
peo_go(5)
def touch_l6(e):
peo_go(6)
def touch_l7(e):
peo_go(7)
def touch_l8(e):
peo_go(8)
tk.Button(frame_top,text='人機對決',command=but1).pack(side=tk.LEFT)
tk.Button(frame_top,text='機器對決',command=but2).pack(side=tk.RIGHT)
update_chess()
l0.pack(side=tk.LEFT)
l1.pack(side=tk.LEFT)
l2.pack(side=tk.LEFT)
l3.pack(side=tk.LEFT)
l4.pack(side=tk.LEFT)
l5.pack(side=tk.LEFT)
l6.pack(side=tk.LEFT)
l7.pack(side=tk.LEFT)
l8.pack(side=tk.LEFT)
label_bottom.pack()
frame_cont1.pack()
frame_cont2.pack()
frame_cont3.pack()
frame_top.pack()
frame_cont.pack()
frame_bot.pack()
top.mainloop()
結束回顧:
- ① 一開始計算最佳走法我用迭代函數,但是一直寫不出最佳的效果,后面直接改成循環2/3次遍歷所有可能,計算所有可能的評估值。
- ② 一開始的評估函數只有計算可能贏得邊數,我發現在實際過程中有不准的情況,於是我添加了幾個兩個條件做為附加條件,可以使得一條直線已經有己方棋子的權重加大,可以直接導致己方贏得走法加大權重,導致對方贏的降低權重。X方選擇大者,O方選擇值小者。
- ③ 在機器對戰中我用多線程並行計算,發現平局的情況會導致線程卡死,無法准確計算出平局的次數。后面發現在最佳走法中,有時因為棋盤剩余格子不多,導致遍歷失敗,返回空值,使得機器無子可下的情況。為了解決這個問題,我在最佳走法中添加了一個判斷,若棋盤剩余格子不多,則舍棄部分遍歷,只遍歷一輪,若只剩一個位置,則返回該位置。
- ④ 人機對戰中目前只發現一種贏局方式:先走角落易贏,機器走法單一,即類似下面情況:
X _ O
O O _
X X X
在機器優先中還未找到贏局方法 - ⑤ 起初機器對戰的結果只有固定幾種,導致輸贏單一,於是我先讓XO先下棋方隨機,再讓第一子的位置隨機,計算出的結果便就有意義了,出現了許多種結果。
這次實驗我使用了tkinter庫,百度了許多相關使用的方法。例如,mainloop窗口消息循環盡量以獨立線程運行,避免有使其阻塞的語句,起初我不知道,把機器對戰五十次直接寫在循環里,便導致GUI卡死。Tkinter中可以隨着值變化而改變GUI顯示文本的參數是StringVar,隱藏控件用pack_forget - ⑥ 為了避免評估值相同無法保證最佳位置,我設置了一個遍歷順序:[4,0,2,6,8,1,3,5,7],先中間,再四角,最后四邊