あぐちゃんさんの物置き場

オタク、ブレボを溶かしがち

気が付いたらオセロAIを書くことになっていた話

言い出しっぺが責任を持ちます

たぶん去年、授業でPythonを扱うことになり、そのうちにグループで何かしらを作って発表することになったことがありました。軽率に「オセロでもやらね?」って言ったら本当にオセロをやることになり、言い出しっぺである僕がAIを書くことになりました。最初は「まあちょろいだろ」って思ってたけど、軽く調べたら不安になるくらい難しいことばかり書いてありました。もうどうでもよくなったので(よくない)、とりあえず思い付きのコードで実装してみた。そしたらなんかそれなりに上手いこと動いたのでよかった(ほんまか?)。丸一日かけて書いたからしんどかった記憶がある。
自分の担当はAIだけだったので、手順決めや表示等はほかの人がやっています。他人のコードを晒すわけにはいかないので、自分が書いたAIとオセロのシステムのコードだけ公開します。ソースコードは以下2つ。

・example.py:使用例
・Reversi.py :システムとAI本体

ソースコード

example.py

import Reversi

#インスタンス生成
a = Reversi.reversi(30,8,'black')

while(1):
    #盤面表示
    a.ShowBoardPic(a.table)

    #プレイヤーの手番
    a.PlayerTurn()

    #ゲーム終了かどうか確認
    if a.EndGameCheck():break
    #AIの手番
    a.AITurn()
    #ゲーム終了かどうか確認
    if a.EndGameCheck():break

Reversi.py

import sys
import copy
import random
from PIL import Image,ImageDraw
import re


#角の追加点
edge_additional = 10

#敵の角の減点
enemy_edge_additional = -10

#辺の追加点
side_additional = 1

#角のL字の追加点
edgel_additional = 3

#外より位置マス内側の減点
outline_additional = -3

#未使用
sideenemy_additional = -3

class reversi:
    lv = 1
    table = [[]]
    n = 8
    side = 0
    
    isVisiblePoint = False


    def __init__(self,level:int,size:int,AIside:str):
        #引数エラー処理群
        if(level <= 1):
            print("Error:Make sure the level is 1 or higher.")
            sys.exit()
        if(size % 2 == 1):
            print("Error:Make sure size is even number.")
            sys.exit()
        if(4 > size):
            print("Error:Make sure minimum size is 6.")
            sys.exit()
        isTrueSide = False
        if(AIside == 'black'):isTrueSide = True
        if(AIside =='white'):isTrueSide = True
        if isTrueSide == False:
            print("Error:Make sure AI side is \"black\" or \"white\".")
        
        #各情報出力
        print("AI level is",level,"\nsize is",size,"×",size)
        
        #各値代入
        self.n = size
        self.lv = level
        if(AIside=='white'):self.side = 1
        if(AIside=='black'):self.side = -1
        self.table = [[0] * size for i in range(size)]
        
        #テーブル初期化 白1 黒-1
        for i in range(self.n):
            for j in range(self.n):
                if (i == size / 2 - 1)&(j == size / 2 - 1):
                    self.table[i][j] = 1
                    self.table[i][j+1] = -1
                    self.table[i+1][j] = -1
                    self.table[i+1][j+1] = 1


    def VisiblePoint(self,a:bool):self.isVisiblePoint = a

    #盤面の様子をjpg出力
    def GenerateBoardPic(self,name:str,board:list):
        im = Image.new('RGB', (self.n * 100, self.n * 100), (0, 128, 0))
        draw = ImageDraw.Draw(im)
        
        for i in range(self.n - 1):
            draw.line(((i + 1) * 100,0,(i + 1) * 100,self.n * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n - 1):
            draw.line((0,(i + 1) * 100,self.n * 100,(i + 1) * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n):
            for j in range(self.n):
                if(board[i][j]==1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(255,255,255),outline=(0,0,0))
                if(board[i][j]==-1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(0,0,0),outline=(0,0,0))
        
        for i in range(0,self.n):
            for j in range(0,self.n):
                draw.text((i * 100 + 40,j * 100 + 40),str((i,j)),fill=(128,0,0))
        
        im.save(name)

    #盤面の様子を標準機能で表示
    def ShowBoardPic(self):
        board = self.table
        im = Image.new('RGB', (self.n * 100, self.n * 100), (0, 128, 0))
        draw = ImageDraw.Draw(im)
        
        for i in range(self.n - 1):
            draw.line(((i + 1) * 100,0,(i + 1) * 100,self.n * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n - 1):
            draw.line((0,(i + 1) * 100,self.n * 100,(i + 1) * 100),width = 3,fill=(0,0,0))
        
        for i in range(self.n):
            for j in range(self.n):
                if(board[i][j]==1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(255,255,255),outline=(0,0,0))
                if(board[i][j]==-1):
                    draw.ellipse((i * 100 + 5,j * 100 + 5,i * 100 + 95,j * 100 + 95),width = 3,fill=(0,0,0),outline=(0,0,0))
        
        for i in range(0,self.n):
            for j in range(0,self.n):
                draw.text((i * 100 + 40,j * 100 + 40),str((i,j)),fill=(128,0,0))
        
        im.show()


    def OneLinerFromAdditionalPoint(self,s:int,line:list):
        isToSearch = True
        if len(line) < 3:isToSearch = False
        if(len(line)!=1):
            if line[1] != s * (-1):isToSearch = False
        
        ans = 0
        if isToSearch:
            #指定色基準に反転
            for i in range(0,len(line)):
                line[i] *= s
        
            #追加点の計算
            for i in range(1,len(line) - 1):
                #0以外,0の並びが見つかったら追加点0にしてやめる
                if (line[i]!=0)&(line[i + 1] == 0):
                    ans = 0
                    break
                    
                #-1,1の並びを探す
                ans += 1
                if(line[i]==-1)&(line[i + 1] == 1): break
                #最後まで移動したら追加点0
                if i==len(line) - 2:
                    ans = 0
                    break
        return ans

    #配置可能な場所か確認(0) 返り値:(追加取得マス数,[方向])
    def isTruePos(self,x:int,y:int,board:list,col:str):
        #引数確認
        if(x > self.n - 1):return [0,None]
        if(y > self.n - 1):return [0,None]
        
        
        s = 0
        if(col == 'white'):s = 1
        if(col == 'black'):s = -1
        if(s==0):return [0,None]
        
        #既におかれているなら0
        if(board[x][y] != 0) : return [0,None]
        
        #総計用
        AddPoint = 0
        
        #方向
        direction = []
        
        #右方向
        line = []
        for i in range(x,self.n):
            line.append(board[i][y])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('right')
        
        #左方向
        line = []
        for i in range(0,x + 1):
            line.append(board[x-i][y])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('left')
        
        #上方向
        line = []
        for i in range(0,y + 1):
            line.append(board[x][y-i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('upper')
        
        #下方向
        line = []
        for i in range(y,self.n):
            line.append(board[x][i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('downer')
        
        #右斜め上方向
        line = []
        for i in range(0,self.n):
            if((x + i > self.n - 1)|(y - i < 0)):break
            line.append(board[x + i][y - i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('right_upper')
        
        #右斜め下方向
        line = []
        for i in range(0,self.n):
            if((x + i > self.n - 1)|(y + i > self.n - 1)):break
            line.append(board[x + i][y + i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('right_downer')
        
        #左斜め上方向
        line = []
        for i in range(0,self.n):
            if((x - i < 0)|(y - i < 0)):break
            line.append(board[x - i][y - i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('left_upper')
        
        #左斜め下方向
        line = []
        for i in range(0,self.n):
            if((x - i < 0)|(y + i > self.n - 1)):break
            line.append(board[x - i][y + i])
        tmp = self.OneLinerFromAdditionalPoint(s,line)
        AddPoint += tmp
        if tmp !=0:direction.append('left_downer')
        
        return [AddPoint,direction]


    def TablePoint(self,x:int,y:int,board:list,col:str):
        t = 0
        myside = 1 if col == 'white' else -1
        enemyside = -myside
        
        #数
        for i in range(0,self.n):
            for j in range(0,self.n):
                if board[i][j] == myside:
                    t += 1
        
        #辺
        for k in range(0,self.n):
            if board[0][k] == myside:
                t += side_additional
        for k in range(0,self.n):
            if board[k][0] == myside:
                t += side_additional
        for k in range(0,self.n):
            if board[self.n - 1][k] == myside:
                t += side_additional
        for k in range(0,self.n):
            if board[k][self.n - 1] == myside:
                t += side_additional
        
        #角
        if board[0][0] == myside:t += edge_additional
        if board[self.n - 1][0] == myside:t += edge_additional
        if board[0][self.n - 1] == myside:t += edge_additional
        if board[self.n - 1][self.n - 1] == myside:t += edge_additional
        
        #辺より1マス内側に置こうとする場合
        if x == 1:t += outline_additional
        if y == 1:t += outline_additional
        if x == self.n - 2:t += outline_additional
        if y == self.n - 2:t += outline_additional
        
        #敵が角を取った場合
        if board[0][0] == enemyside:t += enemy_edge_additional
        if board[self.n - 1][0] == enemyside:t += enemy_edge_additional
        if board[0][self.n - 1] == enemyside:t += enemy_edge_additional
        if board[self.n - 1][self.n - 1] == enemyside:t += enemy_edge_additional
        
        return t


    def MaxPoint(self,board:list,col:str):
        pos = []
        index = 0
        enemyside = -1 if col == 'white' else 1
        myside = -enemyside
        
        maximum = [0,0,-999]
        
        for i in range(0,self.n):
            for j in range(0,self.n):
                t = self.isTruePos(i,j,board,col)[0]
                if t == 0:continue
                t = 0
                for k in range(0,self.n):
                    for l in range(0,self.n):
                        if self.table[k][l] == myside:t += 1
                
                #得点計算用盤面
                tmpb = copy.deepcopy(board)
                self.BoardUpdate(i,j,col,tmpb)
                
                t += self.TablePoint(i,j,tmpb,col)
                
                pos.append([i,j,t])
                if index == 0:maximum = [i,j,t]
                index += 1
        
        
        for i in range(0,len(pos)):
            if(pos[i][2]>maximum[2]):maximum = pos[i]
        
        return maximum


    def FlipOnLine(self,line,col):
        s = 1 if col == 'white' else -1
        for i in range(len(line)):
            #同色がきたら抜ける
            if line[i] == s:break
            #異色がきたら反転
            if line[i] == -s:line[i] = s
        return line


    def BoardUpdate(self,x,y,col:str,board:list):
        p = self.isTruePos(x,y,board,col)
        if(p[0] < 1):return
        
        for s in p[1]:
            if s== 'right':
                line = []
                for i in range(x,self.n):
                    line.append(board[i][y])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x + i][y] = line[i]
            
            if s == 'left':
                line = []
                for i in range(0,x):
                    line.append(board[x - i][y])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x - i][y] = line[i]
            
            if s == 'upper':
                line = []
                for i in range(0,y):
                    line.append(board[x][y - i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x][y - i] = line[i]

            if s == 'downer':
                line = []
                for i in range(y,self.n):
                    line.append(board[x][i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x][y + i] = line[i]
            
            if s == 'right_upper':
                line = []
                for i in range(0,self.n):
                    if((x + i > self.n - 1)|(y - i < 0)):break
                    line.append(board[x + i][y - i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x + i][y - i] = line[i]
            
            if s == 'right_downer':
                line = []
                for i in range(0,self.n):
                    if((x + i > self.n - 1)|(y + i > self.n - 1)):break
                    line.append(board[x + i][y + i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x + i][y + i] = line[i]
            
            if s == 'left_upper':
                line = []
                for i in range(0,self.n):
                    if((x - i < 0)|(y - i < 0)):break
                    line.append(board[x - i][y - i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x - i][y - i] = line[i]
            
            if s == 'left_downer':
                line = []
                for i in range(0,self.n):
                    if((x - i < 0)|(y + i > self.n - 1)):break
                    line.append(board[x - i][y + i])
                line = self.FlipOnLine(line,col)
                for i in range(0,len(line)):
                    board[x - i][y + i] = line[i]
        
        #指定地点に自色を置く
        board[x][y] = 1 if col=='white' else -1


    def NextMaxPointPos(self,board:list,col:str):
        enemycol = 'black' if col == 'white' else 'white'
        
        pos = []
        
        index = 0
        
        #候補地に設置
        for ai in range(0,self.n):
            for aj in range(0,self.n):
                atmp = copy.deepcopy(board)
                a = self.isTruePos(ai,aj,atmp,col)[0]
                if a == 0:continue
                self.BoardUpdate(ai,aj,col,atmp)
                
                #敵の最善手で進める
                epos = self.MaxPoint(atmp,enemycol)
                self.BoardUpdate(epos[0],epos[1],enemycol,atmp)
                
                #次の手の得点を算出
                ptmp = self.MaxPoint(atmp,col)
                
                #得点と座標を格納
                pos.append([ai,aj,ptmp[2] - epos[2]])
        
        #得点で昇順ソート
        for i in range(0,len(pos) - 1):
            for j in range(i,len(pos)):
                if pos[i][2] < pos[j][2]:
                    tmp = pos[i]
                    pos[i] = pos[j]
                    pos[j] = tmp
        
        #同得点のものからランダム選出
        mp = pos[0][2]
        index = 0
        for i in range(1,len(pos)):
            if mp != pos[i][2]:
                break
            index += 1
        
        for i in range(0,index):
            a = random.randint(0,index - 1)
            b = random.randint(0,index - 1)
            tmp = pos[a]
            pos[a] = pos[b]
            pos[b] = tmp
        
        
        return (pos[0][0],pos[0][1])


    def isAbleToPut(self,board:list,col:str):
        for i in range(0,self.n):
            for j in range(0,self.n):
                if self.isTruePos(i,j,board,col)[0] != 0:return True
        return False


    def ReadLvLoop(self,level,board:list,col:str):
        #一手先を出す
        pos = self.NextMaxPointPos(board,col)
        
        if self.isAbleToPut(board,col):
            #これ以上先を読めなければ、座標を返して再帰ループから抜ける
            return (pos[0],pos[1])
        
        #一手先の盤面をつくる
        tmpb = copy.deepcopy(board)
        self.BoardUpdate(pos[0],pos[1],col,tmpb)
        
        if level == 1:
            #level - 1回繰り返したら次の手を返して再帰ループから抜ける
            return (pos[0],pos[1])
        
        print(level)
        #一手先の盤面を現在の盤面、level - 1として再帰呼び出し
        return self.ReadLvLoop(level - 1,tmpb,col)

#AIのターン
    def AITurn(self):
        self.EndGameCheck()
        mycol = 'white' if self.side == 1 else 'black'
        if self.isAbleToPut(self.table,mycol) == False:
            print('AI passed')
            return
        
        fs = self.ReadLvLoop(self.lv,self.table,mycol)
        self.BoardUpdate(fs[0],fs[1],mycol,self.table)
        
        print(fs)
        
        if self.isVisiblePoint == True:
            a = self.TablePoint(fs[0],fs[1],self.table,mycol)
            print(a)

#プレイヤーのターン
    def PlayerTurn(self):
        self.EndGameCheck()
        mycol = 'white' if self.side == -1 else 'black'
        if self.isAbleToPut(self.table,mycol) == False:
            print('You passed')
            return
        
        x = 0
        y = 0
        
        while(1):
            print('x y')
            tmp = input()
            
            chk = re.search(r'\d\s\d',tmp)
            
            if chk == None:
                print("Invalid!")
                continue
            
            i = chk.group(0).split()
            
            x = int(i[0])
            y = int(i[1])
            
            a = self.isTruePos(x,y,self.table,mycol)[0]
            if a == 0:
                print("Invalid!")
                continue
            break
        
        
        self.BoardUpdate(x,y,mycol,self.table)
        
        if self.isVisiblePoint == True:
            a = self.TablePoint(x,y,self.table,mycol)
            print(a)

#ゲームが終了したかどうか判定
    def EndGameCheck(self):
        if (self.isAbleToPut(self.table,'white') == False) & (self.isAbleToPut(self.table,'black') == False):
            print('GAME OVER!')
            w = 0
            b = 0
            for i in range(0,self.n):
                for j in range(0,self.n):
                    if self.table[i][j] == 1:
                        w += 1
                    if self.table[i][j] == -1:
                        b += 1
            print('WHITE:',w,' BLACK:',b)
            return True
        return False

3,2\n とか打つと、左から4つ目、上から3つ目の位置に自分の色を置けます。
このまま動かすと、デバッグ用に用意した盤面の画像が別窓で表示されます。画像データだけを出力もできるので、tkinterとかで適当にGUIに表示するのがいいかもしれません。

仕組み

AIのカラクリとしては割と単純で、盤面の状態を点数化して、その点数が高くなるように指していきます。点数は、盤面に自分の色が増えるほど高くなり、また角や辺をたくさん取れるとより点数が高くなるようにしています。AIは、プレイヤーも常に点数が高くなるように指してくると想定して盤面を読み進めます。これをすべてのマスに対して行い、一定手数先まで読んだときに一番点数が高かったマスに指します。レベルの数値は、この先読みする手数となっています。

まとめ

クラスメイトを数人ボコボコにできたので非常に満足です。
2つのシステムで盤面を共有できるので、やろうと思えばプレイヤー vsプレイヤーとか、AI vs AIもできます。