0
0

pythonでオセロAI作ってみた1

Last updated at Posted at 2023-09-18

無理でした

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()
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0