nounai.output(spaghetiThinking);

趣味と実益を兼ねて将棋プログラム(研究ツールなど)を作ってみたいと思う私の試行錯誤とか勉強したことを綴ってゆく予定です。 主目的はプログラミングの経験値稼ぎですが、コンピュータ将棋の製作も目指してみたいとも考えています。

【リバーシ】原始的にAI作ってみた

そろそろAIっぽいこともしてみようと思った次第。


まあ着手可能な手からランダムにピックアップするだけのものですが。

ゲーム部分の実装が簡単であろうという理由から、今回の題材は将棋ではなくオセロです。ゲーム木うんぬんとか、その辺の基礎理論的なことの勉強も必要ですし、実装していく上でのノウハウとかも吸収していきたい。で、これらを同時に効率よく身につけていこうと考えた時に、将棋とかいう複雑なゲームを題材にするのはまだ早いでしょうよ?と思いました。まぁAIに限らず、って気はしますが。

で、以下がソースコードです。所要時間は2~3時間ほどでした。プレイヤーは存在せず、黒と白に同一の思考ルーチンを使って終局まで打たせるようにしています。完全にランダムに置くだけなので、AIっぽい処理はほぼないです。BoardをSubjectとしたObserverを作ることで棋譜の記録、及びそのエクスポート(Loggerクラス)をサポートしてます。この辺は以前の記事でやってたことがちょろっと活きてます。

今回オブジェクト指向っぽいことはObserverくらいです。カプセル化とかは一切考慮してないので、このコードを基に色々やろうとするといずれスパゲティになると思います。なのでこのコードは今回でほぼ廃棄。対局の終了条件とかも雑に判定してて、多分(空白のマスがある&両方これ以上置けないのケースとか)漏れてますし。まぁでも、こうしてちゃちゃっと書いてみたコードが実際に対局を行っている様子を見るのはとても満足です。こういうのがモチベーションて奴なんでしょうね。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os,sys
import random
import string,re

class CannotPut(Exception):
        pass

class Cell:
        def __str__(self):
                return "  "
        def value(self):
                return 0
class Empty(Cell):
        pass
class Black(Cell):
        def __str__(self):
                return " o"
        def value(self):
                return 1
class White(Cell):
        def __str__(self):
                return " x"
        def value(self):
                return -1

class Board:
        def __init__(self):
                self.obs = []
                self.b = []
                for i in range(0,8):
                        self.b.insert(len(self.b), [Empty()]*8)
        
        def initBoard(self):
                self.b[3][3] = Black()
                self.b[4][4] = Black()
                self.b[3][4] = White()
                self.b[4][3] = White()

        def __str__(self):
                s = ""
                for i in range(0,8):
                        s += "---"*8 + "\n"
                        s += "|" + "|".join([c.__str__() for c in self.b[i]]) + "|\n"
                s += "---"*8 + "\n"
                return s
        
        def notify(self,cmd):
                for o in self.obs:
                        o.update(cmd)

        def registObs(self, o):
                if isinstance(o, BoardObserver):
                        self.obs.insert(len(self.obs), o)
                return 1

class Put:
        def __init__(self, board, point, turn):
                self.board = board
                self.point = point # (x,y)
                self.reverses = []
                self.turn = turn
                if not isinstance(board.b[point[0]][point[1]], Empty):
                        raise CannotPut, "Already putting"
                
                rel = [(i,j) for i in range(-1,2) for j in range(-1,2)]
                for i,elm in enumerate(rel):
                        if(rel[i]==(0,0)):
                                del rel[i]
                                break
                org = self.point
                
                for v in rel:
                        for i in range(1,8):
                                tgtx = org[0]+v[0]*i
                                tgty = org[1]+v[1]*i
                                try:
                                        if(self.board.b[tgtx][tgty].value() == self.turn):
                                                if(i==1):
                                                        break
                                                self.reverses.insert(len(self.reverses), (tgtx,tgty))
                                                break
                                        elif(self.board.b[tgtx][tgty].value() == 0):
                                                break
                                        else:
                                                continue
                                except IndexError,e:
                                        break
                
                if(self.reverses == []):
                        raise CannotPut, "Exception: Cannot put on given position"
                        print self.board
                        return 
        def execute(self):
                c = Empty()
                if self.turn==1:
                        c = Black()
                else:
                        c = White()
                self.board.b[self.point[0]][self.point[1]] = c
                
                for rel in self.reverses:
                        v = (rel[0] - self.point[0], rel[1] - self.point[1])
                        print "Vector: "+ v.__str__()
                        getbase = lambda x: 0 if x==0 else x/abs(x)
                        base = (getbase(v[0]), getbase(v[1]))
                        border = max( abs(v[0]), abs(v[1]) )
                        for i in range(1, border):
                                p = (base[0]*i, base[1]*i)
                                p = (self.point[0]+p[0], self.point[1]+p[1])
                                self.board.b[p[0]][p[1]] = c

                #print self.reverses
                #print self.board
                self.board.notify(self)
        def undo(self):
                pass

class BoardObserver:
        def update(self, cmd):
                pass
class Logger(BoardObserver):
        def __init__(self):
                self.log = []
        def update(self, cmd):
                self.log.insert(len(self.log), cmd)
        def export(self):
                s = ""
                for i,cmd in enumerate(self.log):
                        s += str(i) + ": "+str(cmd.point) + "\n"
                return s

def think(board,turn):
        nonfill = False
        for i in range(0,8):
                for j in range(0,8):
                        if isinstance(board.b[i][j], Empty):
                                nonfill = True
                                break
        if not nonfill:
                print "Game End."
                return -1

        available = []
        for i in range(0,8):
                for j in range(0,8):
                        try:
                                cmd = Put(board, (i,j), turn)
                                available.insert(len(available), cmd)
                        except CannotPut,e:
                                continue
        if(available==[]):
                print "Game end."
                return -1

        idx = random.randint(0, len(available)-1)
        available[idx].execute()
        print board
        t = "Black's" if turn==-1 else "White's"
        print t+" turn.\n"
        return 1

def main():
        board = Board()
        board.initBoard()
        logger = Logger()
        board.registObs(logger)

        print board
        print "\n"
        """
        cmd = Put(board, (5,3), 1)
        cmd.execute()
        cmd = Put(board, (5,2), -1)
        cmd.execute()
        """
        #print "You are Black"
        #cmd = Put(board, (5,3), 1)
        #cmd.execute()
        #print board
        
        turn = 1
        while(True):
                ret = think(board,turn)
                if ret==-1: break
                turn *= -1
        
        b=0
        w=0
        for i in range(0,8):
                for j in range(0,8):
                        if   isinstance(board.b[i][j], Black): b+=1
                        elif isinstance(board.b[i][j], White): w+=1
        print "Black: "+str(b)
        print "White: "+str(w)
        if b>w:
                print "Black Win."
        elif w>b:
                print "White Win."
        else:
                print "Draw Game."
        
        print "\n*** logger ***"
        print logger.export()

if __name__=="__main__":
        main()

実行結果は以下。対局中の出力(現在の局面)と、終局後の棋譜の出力とで2つあるので別々に出します。

------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  | o| x|  |  |  |
------------------------ 
|  |  |  | x| o|  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 



Vector: (2, 0)
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  | o|  |  |  |
------------------------ 
|  |  |  | o| o|  |  |  |
------------------------ 
|  |  |  | x| o|  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 
|  |  |  |  |  |  |  |  |
------------------------ 

White's turn.

... 中略 ... 
------------------------ 
| o| o| o| o| x| x| o| o|
------------------------ 
| o| o| x| o| o| x| o| o|
------------------------ 
| o| o| x| o| o| o| x| x|
------------------------ 
| o| o| x| x| x| x| x| x|
------------------------ 
| o| x| x| o| o| o| x| x|
------------------------ 
| o| x| x| x| o| x| o| x|
------------------------ 
| o| o| o| o| x| o| o| o|
------------------------ 
| o| o| o| x| x| x| x| o|
------------------------ 

Black's turn.

Game End.
Black: 37
White: 27
Black Win.

そして棋譜部分。

*** logger ***
0: (2, 4)
1: (2, 5)
2: (3, 5)
3: (4, 5)
... 中略 ...
57: (5, 7)
58: (6, 5)
59: (3, 7)

※座標にはタテヨコの区別がありますが、棋譜出力の際とかにその辺のことは考慮してません。将棋の表記である筋=>段と同じ順でオセロも表記しますが、もし局面の出力形式と整合をとるのであれば(最終手を例とすると)本当は(3,7)でなく(7,3)となります。もっとも、配列の添字である0~7番をそのまま出してるだけなのでそこからさらに+1する必要もありますし、「筋」にあたる所は正しくはアルファベット表記しなくてはなりません。ま、こまけぇことはいいんだよということです。

実際にコードを書いてみて思ったことは、Model部分と思考部分が独立になればいいなってことです。強いAIを作るには思考部分の高速化が必要なので、内部表現が変更される可能性は大いにあります。で、そういう高速化に重点を置いた内部表現とUI関係のコードはあまり相性がよくない気がします。Modelの表現を「自分」が扱える形式に変換するAdapterを書くことになるのでしょうが、「自分」に当てはまる要素はView側なのか、思考ルーチン側なのか。はたまた両方なのか。その辺は考える必要があるかもしれません。言い換えると、Modelの内部表現はUIとAIどっち寄りにすべきか?それともどっちにも寄らないようにすべきか?という所でしょうか。