無理でした
Tkinterを使ってクリックでオセロを遊べるようにして、コンピュータ対戦も作って、今回やっとAIを導入するつもりでしたが、難しすぎて断念。
妥協して、3手先まで読んで打つコンピュータを作りました。ミニマックス法には(自分の評価基準で相手が最善の手を選ぶとは限らなくないか…?)という懐疑心があったので、最小値ではなく平均値にして期待値的な選び方をするようにしました。
オセロAIについて考えたこと
オセロは次の盤面が逐次的に決まっていくし、おそらく機械学習のタイプは強化学習であってると思う。しかし、「終了時に枚数が多い方が勝ち」というルールなので報酬が直接設定できず、他の報酬で設定できても状態価値関数が盤面によって変わってしまう。
序盤は定石通り、終盤は詰めor枚数稼ぎとして、中盤のみ考えるようにすれば解決しそうな感じもする。終盤の詰みの段階に入れる盤面を何十枚も覚えさせて、学習を定石以外の盤面~詰みの盤面までに限定すれば、報酬が「目的の盤面に持っていけたかどうか」で設定しやすいし、報酬が近いしで良いと思う。
次回からその線で探求したい。必要があれば、他のテーマで機械学習の練習する。多分その方が良い。
ソースコード
3手読みオセロAI
import tkinter
from tkinter import *
"""
コンピュータが置くときの評価基準
・マスごとの評価値
・置いたときに相手が置ける場所の数
・置いたときの相手の置ける最善手の評価値
"""
class player:
def __init__(self):
self.name="name"
self.disc=0
player1=player()
player2=player()
player1.name="aaa"
player1.disc=1
player2.name="bbb"
player2.disc=2
can_place=[]
cnt_can=4
#マスごとの評価値
place_value=[150,-20, 60,-10,-10, 60,-20,150,
-20,-60,-30, -5, -5,-30,-60,-20,
60,-30, 25, 10, 10, 25,-30, 60,
-10, -5, 10, 10, 10, 10, -5,-10,
-10, -5, 10, 10, 10, 10, -5,-10,
60,-30, 25, 10, 10, 25,-30, 60,
-20,-60,-30, -5, -5,-30,-60,-20,
150,-20, 60,-10,-10, 60,-20,150]
cnt_cannot=0
com_place=19
cnt_opp_can=0
for i in range(0,64):
if i==19 or i==26 or i==37 or i==44:
can_place.append(3)
else:
can_place.append(0)
#ボードの初期化
board=[]#壁も含めた盤面
Re_board=[]#壁をのぞいた盤面
for i in range(0,100):#白石は2、黒石は1、空白は0
if (0<=i and i<=9) or (90<=i and i<=99) or i%10==0 or i%10==9:
board.append(5)
elif i==44 or i==55:
board.append(2)
Re_board.append(i)
elif i==45 or i==54:
board.append(1)
Re_board.append(i)
else:
board.append(0)
Re_board.append(i)
class Application(tkinter.Frame):
def __init__(self,win=None):
super().__init__(win,width=680,height=480,borderwidth=1,relief='groove')
self.win=win
self.pack()
self.pack_propagate(0)
self.create_widgets()
self.create_button(board,can_place)
def create_widgets(self):
self.label_turn=Label(self,text="あなたが先攻です\n黒を置いてください",font=("MSゴシック", "15"),justify="left")
self.label_turn.place(x=500,y=50)
self.label_cnt1=Label(self,text="黒:2枚",font=("MSゴシック", "20"))
self.label_cnt1.place(x=500,y=120)
self.label_cnt2=Label(self,text="白:2枚",font=("MSゴシック", "20"))
self.label_cnt2.place(x=500,y=160)
self.label_win=Label(self,text="",font=("MSゴシック", "15"),justify="left")
self.button_com=Button(self,text="決定",font=("MSゴシック", "20"),command=self.com_run)
self.button_com.place(x=500,y=250)
self.button_com.configure(bg="yellow")
def create_button(self,board,can_place):
self.button=[]
for i in range(0,8):
for j in range(0,8):
if board[(i+1)*10+j+1]==2:
self.button.append(Button(self,text="●",font=("MSゴシック","45"),fg="white",command=self.btn_clicked(self.button,i*8+j)))
elif board[(i+1)*10+j+1]==1:
self.button.append(Button(self,text="●",font=("MSゴシック","45"),command=self.btn_clicked(self.button,i*8+j)))
elif can_place[i*8+j]==3:
self.button.append(Button(self,text="●",font=("MSゴシック","20"),fg="yellow",command=self.btn_clicked(self.button,i*8+j)))
else:
self.button.append(Button(self,command=self.btn_clicked(self.button,i*8+j)))
self.button[i*8+j].place(x=j*60,y=i*60,width=60,height=60,)
self.button[i*8+j].configure(bg="green")
#人間の番
def btn_clicked(self,button,place):
def inner():
def show_board(board,button,can_place,player,finish):#盤面をウィンドウに表示
if player==player1:
self.label_turn["text"]="「決定」を\n押してください"
else:
self.label_turn["text"]="あなたの番です"
cnt1,cnt2=cnt_disc(board)
self.label_cnt1["text"]="黒:"+str(cnt1)+"枚"
self.label_cnt2["text"]="白:"+str(cnt2)+"枚"
if (cnt1+cnt2==64) or (cnt1==0) or (cnt2==0) or finish==True:
self.label_turn["text"]=""
if cnt1>cnt2:
self.label_win["text"]=f'あなたの勝利です'
self.label_win.place(x=500,y=200)
elif cnt1<cnt2:
self.label_win["text"]=f'コンピュータの勝利です'
self.label_win.place(x=500,y=200)
else:
self.label_win["text"]="引き分けです"
self.label_win.place(x=500,y=200)
else:
pass
for i in range(0,8):
for j in range(0,8):
button[i*8+j].destroy()
self.create_button(board,can_place)
for i in range(0,8):
for j in range(0,8):
button[i*8+j].place(x=j*60,y=i*60,width=60,height=60,)
button[i*8+j].configure(bg="green")
def reverse(place_a,board,player):#裏返す関数、戻り値→裏返せる枚数
dir=(-11,-10,-9,-1,1,9,10,11)#方向ベクトル
sum_cnt_rev=0#そこに置いたときに裏返せる枚数
for i in dir:
tmp_place=place_a#placeの初期化
cnt_rev=0#その方向においての裏返せる枚数
while board[tmp_place+i]!=player.disc:
if board[tmp_place+i]==(int(player.disc)-1.5)*-1+1.5:#ディスクが1なら2,2なら1(相手のディスクがある)
tmp_place+=i
cnt_rev+=1
else:#0(空きマス)か5(壁)
cnt_rev=0
break
for j in range(1,cnt_rev+1):
board[place_a+j*i]=player.disc
sum_cnt_rev+=cnt_rev
return sum_cnt_rev
def check(player,board,Re_board):#まだ置ける場所があるかチェックする関数,戻り値→置ける場所の数、座標
dir=(-11,-10,-9,-1,1,9,10,11)#方向ベクトル
cnt_can=0#置ける場所の数
can_place=[]
for place in Re_board:
sum_cnt_rev=0#そこに置いたときに裏返せる枚数
if board[place]==0:
for i in dir:
tmp_place=place#placeの初期化
cnt_rev=0#その方向においての裏返せる枚数
while board[tmp_place+i]!=player.disc:
if board[tmp_place+i]==(int(player.disc)-1.5)*-1+1.5:#ディスクが1なら2,2なら1(相手のディスクがある)
tmp_place+=i
cnt_rev+=1
else:#0(空きマス)か5(壁)
cnt_rev=0
break
sum_cnt_rev+=cnt_rev
else:
pass
if sum_cnt_rev!=0:
cnt_can+=1
can_place.append(place)
else:
pass
return (cnt_can,can_place)
def run(player,board):#実行する関数,戻り値→実行ok:1、実行no:0
#石を置く
a=int(place)
place_a=(a//8+1)*10+(a%8+1)
#裏返す
if board[place_a]==0:
sum_cnt_rev=reverse(place_a,board,player)
if sum_cnt_rev==0:
return 0
else:
board[place_a]=int(player.disc)
return 1
else:
return 0
def cnt_disc(board):
cnt1=0
cnt2=0
for i in Re_board:
if board[i]==1:
cnt1+=1
elif board[i]==2:
cnt2+=1
else:
pass
return (cnt1,cnt2)
def decide_place_run(board,can_place,repeat):#複製して裏返し、盤面を返す
tmp_board=[]
for i in range(0,10):
for j in range(0,10):
tmp_board.append(board[i*10+j])
if repeat==3:
can_place=(can_place//8+1)*10+can_place%8+1#8進数から10進数に変換
if repeat%2==1:
sum_cnt_rev=reverse(can_place,tmp_board,player2)#仮の盤面を裏返す
else:
sum_cnt_rev=reverse(can_place,tmp_board,player1)#仮の盤面を裏返す
if sum_cnt_rev==0 or tmp_board[can_place]!=0:
pass
else:
if repeat%2==1:
tmp_board[can_place]=2
else:
tmp_board[can_place]=1
return tmp_board
def decide_place(board,can_place):#コンピュータの置く場所を決定
global com_place
max_ave_value=-1000
value=-1000
for i in range(0,len(can_place)):#1手先読み
tmp_board=decide_place_run(board,can_place[i],3)#複製して裏返し、盤面を返す
tmp_cnt_can,tmp_place=check(player1,tmp_board,Re_board)#人間が置ける場所の数、座標を入手
sum_value=0
for j in range(0,len(tmp_place)):#2手先読み
tmp_tmp_board=decide_place_run(tmp_board,tmp_place[j],2)
tmp_cnt_can,tmp_tmp_place=check(player2,tmp_tmp_board,Re_board)#コンピュータが置ける場所の数、座標を入手
max_value=-1000
for h in range(0,len(tmp_tmp_place)):#3手先読み
last_board=decide_place_run(tmp_tmp_board,tmp_tmp_place[h],1)
tmp_cnt_can,last_place=check(player1,last_board,Re_board)#人間が置ける場所の数、座標を入手
value_board_com=0#置いたときの盤面を評価(コンピュータ)
value_board_human=0#置いたときの盤面を評価(人)
for k in range(0,8):
for l in range(0,8):
if last_board[(k+1)*10+l+1]==2:
value_board_com+=place_value[k*8+l]
elif last_board[(k+1)*10+l+1]==1:
value_board_human+=place_value[k*8+l]
else:
pass
if tmp_cnt_can!=0:#盤面評価
value=tmp_cnt_can*-8+value_board_com+value_board_human*-1
else:#もし連打可能なら
value=1000
if value>max_value:
max_value=value
else:
pass
sum_value+=max_value
if len(tmp_place)!=0:
ave_value=sum_value//len(tmp_place)
else:
ave_value=1
if ave_value>max_ave_value:
max_ave_value=ave_value
com_place=can_place[i]
else:
pass
can_place=[]
finish=False
global cnt_can
global cnt_cannot
global com_place
self.label_win["text"]=""
if cnt_can!=0:#置ける
run_can=run(player1,board)#実行、成功→1
cnt_can,tmp_can_place=check(player2,board,Re_board)#置ける場所の数、座標を入手
cnt1,cnt2=cnt_disc(board)
if cnt1+cnt2>56:#裏返せる枚数が1番多い場所に置く
max_rev=0
tmp_board=[]
for i in tmp_can_place:
tmp_board.clear()
for j in range(0,10):
for h in range(0,10):
tmp_board.append(board[j*10+h])
sum_cnt_rev=reverse(i,tmp_board,player2)
if sum_cnt_rev>max_rev:
max_rev=sum_cnt_rev
i=(i//10-1)*8+i%10-1
com_place=i
else:
pass
else:
pass
for i in range(0,len(tmp_can_place)):#座標を10進数→8進数に変換
tmp_can_place[i]=(tmp_can_place[i]//10-1)*8+tmp_can_place[i]%10-1
if cnt1+cnt2<57:
decide_place(board,tmp_can_place)
else:
pass
cnt=0
for i in range(0,8):
for j in range(0,8):
for r in range(0,len(tmp_can_place)):
if tmp_can_place[r]==i*8+j:
cnt+=1
else:
pass
if cnt!=0:
can_place.append(3)
else:
can_place.append(0)
cnt=0
cnt_cannot=0
if run_can==1:
show_board(board,button,can_place,player1,finish)
else:
pass
else:#置けない
run_can=1
cnt_can=1
if cnt_cannot==1:#相手も置けなかった
finish=True
show_board(board,button,can_place,player1,finish)
else:
self.label_win["text"]="黒をどこにも置けません\n「決定」を押してください"
self.label_win.place(x=500,y=200)
cnt_cannot+=1
return inner
#コンピュータの番
def com_run(self):
def show_board(board,can_place,finish):#盤面をウィンドウに表示
self.label_turn["text"]="あなたの番です"
cnt1,cnt2=cnt_disc(board)
self.label_cnt1["text"]="黒:"+str(cnt1)+"枚"
self.label_cnt2["text"]="白:"+str(cnt2)+"枚"
if (cnt1+cnt2==64) or (cnt1==0) or (cnt2==0) or finish==True:
self.label_turn["text"]=""
if cnt1>cnt2:
self.label_win["text"]=f'黒の勝利です'
self.label_win.place(x=500,y=200)
elif cnt1<cnt2:
self.label_win["text"]=f'白の勝利です'
self.label_win.place(x=500,y=200)
else:
self.label_win["text"]="引き分けです"
self.label_win.place(x=500,y=200)
else:
pass
for i in range(0,8):
for j in range(0,8):
self.button[i*8+j].destroy()
self.create_button(board,can_place)
for i in range(0,8):
for j in range(0,8):
self.button[i*8+j].place(x=j*60,y=i*60,width=60,height=60,)
self.button[i*8+j].configure(bg="green")
def reverse(place_a,board,player):#裏返す関数、戻り値→裏返せる枚数
dir=(-11,-10,-9,-1,1,9,10,11)#方向ベクトル
sum_cnt_rev=0#そこに置いたときに裏返せる枚数
for i in dir:
tmp_place=place_a#placeの初期化
cnt_rev=0#その方向においての裏返せる枚数
while board[tmp_place+i]!=player.disc:
if board[tmp_place+i]==(int(player.disc)-1.5)*-1+1.5:#ディスクが1なら2,2なら1(相手のディスクがある)
tmp_place+=i
cnt_rev+=1
else:#0(空きマス)か5(壁)
cnt_rev=0
break
for j in range(1,cnt_rev+1):
board[place_a+j*i]=player.disc
sum_cnt_rev+=cnt_rev
return sum_cnt_rev
def check(player,board,Re_board):#まだ置ける場所があるかチェックする関数,戻り値→置ける場所の数、座標
dir=(-11,-10,-9,-1,1,9,10,11)#方向ベクトル
cnt_can=0#置ける場所の数
can_place=[]
for place in Re_board:
sum_cnt_rev=0#そこに置いたときに裏返せる枚数
if board[place]==0:
for i in dir:
tmp_place=place#placeの初期化
cnt_rev=0#その方向においての裏返せる枚数
while board[tmp_place+i]!=player.disc:
if board[tmp_place+i]==(int(player.disc)-1.5)*-1+1.5:#ディスクが1なら2,2なら1(相手のディスクがある)
tmp_place+=i
cnt_rev+=1
else:#0(空きマス)か5(壁)
cnt_rev=0
break
sum_cnt_rev+=cnt_rev
else:
pass
if sum_cnt_rev!=0:
cnt_can+=1
can_place.append(place)
else:
pass
return (cnt_can,can_place)
def run(player,board):#実行する関数,戻り値→実行ok:1、実行no:0
#場所を決めて、石を置く
a=com_place
place_a=(a//8+1)*10+(a%8+1)#8進数→10進数
#裏返す
sum_cnt_rev=reverse(place_a,board,player)
if sum_cnt_rev==0 or board[place_a]!=0:
return 0
else:
board[place_a]=int(player.disc)
return 1
def cnt_disc(board):
cnt1=0
cnt2=0
for i in Re_board:
if board[i]==1:
cnt1+=1
elif board[i]==2:
cnt2+=1
else:
pass
return (cnt1,cnt2)
can_place=[]
finish=False
global cnt_can
global cnt_cannot
self.label_win.pack_forget()
#1の番
if cnt_can!=0:#置けた場合
run_can=run(player2,board)
cnt_can,tmp_can_place=check(player1,board,Re_board)
for i in range(0,len(tmp_can_place)):
tmp_can_place[i]=(tmp_can_place[i]//10-1)*8+tmp_can_place[i]%10-1
cnt=0
for i in range(0,8):
for j in range(0,8):
for r in range(0,len(tmp_can_place)):
if tmp_can_place[r]==i*8+j:
cnt+=1
else:
pass
if cnt!=0:
can_place.append(3)
else:
can_place.append(0)
cnt=0
if run_can==1:
show_board(board,can_place,finish)
else:
pass
else:
run_can=1
cnt_can=1
if cnt_cannot==1:#相手も置けなかった
finish=True
show_board(board,can_place,finish)
else:
self.label_win["text"]="コンピュータは\nどこにも置けません"
self.label_win.place(x=500,y=200)
self.label_turn["text"]="あなたの番です"
cnt_cannot+=1
win=Tk()
win.title("オセロ")
win.geometry('700x500')
app=Application(win=win)
app.mainloop()