nounai.output(spaghetiThinking);

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

インタプリタっぽいもののα版完成&内部形式について

一応、棋譜のメタ情報(プレイヤ名のみ)と棋譜本体部分を抽出することには成功しました。動いたことによって駒を取ったのか?王手か?みたいな部分は全くなのですが、一応棋譜学習を行う上での必要最低限にはかなり近づいたんじゃないかと思います。αを名乗れるクオリティかどうかは疑問ですけども。


フォーマットについて。内部形式には特にデザインパターンとかを使わないことにします。せっかくpythonには便利な辞書型があることですし。簡単なラッパーくらいは作るかもしれませんが。以下(クリックで表示)のような感じを考えています。

隠す

"""
kifudocument = {
        "header":
                "player":
                        "+": "*****"
                        "-": "*****"
                "期戦とか":
                        ****
        "body":
                [{
                                "type"   : "regular"/"special"
                                "command": (RESIGN|CHUDAN|...)/""  # Follow the CSA Specification
                                "turn"   : "+"/"-"
                                "from"   : (0,0)/ ([1-9],[1-9])
                                "to"     : ([1-9], [1-9])
                                "piece"  : (FU|KY|KE|GI|KI|KA|HI|OU|TO|NY|NK|NG|UM|RY|NONE)
                }]

基本はCSA仕様に倣うつもり。
"""

class TE:
        class TURN:        # turn
                BLACK="+"
                WHITE="-"
                B=BLACK
                W=WHITE
                N=""
        class PT:        # pieceType
                FU="FU"
                KY="KY"
                KE="KE"
                GI="GI"
                KI="KI"
                KA="KA"
                HI="HI"
                OU="OU"
                TO="TO"
                NY="NY"
                NK="NK"
                NG="NG"
                UM="UM"
                RY="RY"
                NONE=""
        class TYPE:
                REGULAR = "REGULAR"
                SPECIAL = "SPECIAL"
        class COMMAND:
                RESIGN = "RESIGN"
                MATTA  = "MATTA"
                CHUDAN = "CHUDAN"
                NONE = ""

        """def __init__(self):
                self.type = 
                self.turn = TE.TURN.N
                self.fr = (0,0)
                self.to = (0,0)
                self.pi = PT.NONE
        """
        @classmethod
        def Create(self, mtype=TYPE.REGULAR, command=COMMAND.NONE, turn=TURN.N, fr=(0,0),to=(0,0),piece=PT.NONE):
                return {
                        "type"    : mtype,
                        "command" : command,
                        "turn"    : turn,
                        "from"    : fr,
                        "to"      : to,
                        "piece"   : piece }

隠す

多分内部表現はこれでほぼ間に合うと思う。この後はこの構造を前提にコードを組んでいきたいので、この表現が変更されないようにしたい。おそらく変更はないと思うが、拡張としてありそうな可能性を挙げるなら

  • 消費時間
  • (コンピュータの場合)読み筋コメント

あたりではなかろうか。消費時間は普通にありうるのであらかじめ追加しとく必要がありそう。コンピュータ将棋作る気であれば読み筋情報はかなりおいしいので、できれば採取したい。flodgateの棋譜限定でいいから何とか格納可能なようにはしときいです。が、「拡張」であればまだ対応は容易なはずなので後回しに。

検討は必要ですけど、実装もさっさと進めたいし、手を動かしたことで得られる考察とか設計の反省点とか、そういうものも大事にしたいので、そのあたりは平行作業ですかね。

実装したコードは以下。かなりゴリゴリやっていますが、棋譜情報の大部分をカットの上無視するようになっているのでその分かなりマイルドではあります。

コードの表示

コードを隠す

本体部分。冒頭でインポートしてるkifudocumentは自分でつくったもの。で表示してるのと同一なので省きます。

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

import string,re,os
import kifudocument as kd

"""
kifudocument = {
        "header":
                "player":
                        "black": "*****"
                        "white": "*****"
                "期戦とか":
                        ****
        "body":
                [{
                                "type" : "regular"/"special"
                                "turn" : "+"/"-"
                                "from" : (0,0)/ ([1-9],[1-9])
                                "to"   : ([1-9], [1-9])
                                "piece": (FU|KY|KE|GI|KI|KA|HI|OU|TO|NY|NK|NG|UM|RY)
                }]
"""

class CSADocumentBuilder(kd.DocumentBuilder):
        def __init__(self, obj):
                self.obj = obj
                self.product = None
                self.meta = None
                self.kifu = None
                self.body = None
                for s in obj:
                        if(isinstance(s, SMeta)):
                                self.meta = s
                        if(isinstance(s, SMove)):
                                self.kifu = s.moves
        def buildHeader(self):
                if self.meta is None:
                        self.body = {}
                        return
                self.body = {"player": self.meta.player}
        def buildBody(self):
                pass
        def getProduct(self):
                return {"header":self.body, "body":self.kifu}

class CSADocumentBuilderFactory(kd.DocumentBuilderFactory):
        @classmethod
        def Create(cls, obj):
                return CSADocumentBuilder(obj)

class Ut:
        mver                = re.compile("^V[1-9](\.[0-9])?")
        mmeta                = re.compile("^($.*|N[+-])")
        mstart                = re.compile("^([+\-]$|P)")
        mmoves                = re.compile("^[+\-%T]")
        mcomment        = re.compile("^'")
        
        isVer                 = staticmethod(lambda l: True if not Ut.mver.match(l) is None else False)
        isMeta                = staticmethod(lambda l: True if not Ut.mmeta.match(l) is None else False)
        isStart                = staticmethod(lambda l: True if not Ut.mstart.match(l) is None else False)
        isMove                = staticmethod(lambda l: True if not Ut.mmoves.match(l) is None else False)
        isComment   = staticmethod(lambda l: True if not Ut.mmoves.match(l) is None else False)
        isValid                = staticmethod(lambda l: True if (Ut.isVer(l) or Ut.isMeta(l) or Ut.isStart(l) or Ut.isMove(l) or Ut.isComment(l)) else False)
        
class SimpleCsaInterpreter:
        def __init__(self, f):
                self.context = f
                self.lines = f.readlines()
                f.seek(0)
        
        def prepare(self):
                self.lines = filter(lambda l: l if not l=="" else None, reduce(lambda a,b: a+b, map(lambda l: re.split("[,\r\n]",l), self.lines)))
                #self.lines = filter(lambda l: l if not re.match("^$",l) else None, self.lines)
        def interpret(self):
                self.prepare()
                stat = SIni()
                for n,l in enumerate(self.lines):
                        if re.match("^[\$'T]",l): continue
                        try:
                                stat = stat.setCurrentLine(l).interpret()
                        except Exception,e:
                                print "line "+str(n)+": "+e.__str__()
                return stat
        def isSelfState(self):
                pass

class CsaSyntaxError(Exception):
        pass

class CsaState:
        def __init__(self,l="",pre=None):
                self.line = l
                self.prestate = pre
        def setPrestate(self, pre):
                self.prestate = pre
                return self
        def setCurrentLine(self, line):
                self.line = line
                return self
        def delegate(self):
                if(not Ut.isValid(self.line)):
                        raise CsaSyntaxError, line
                s = SIni()
                if(Ut.isVer(self.line)):
                        s = SVer()
                elif(Ut.isMeta(self.line)):
                        s = SMeta()
                elif(Ut.isStart(self.line)):        
                        s = SStart()
                elif(Ut.isMove(self.line)):
                        s = SMove()
                elif(Ut.isComment(self.line)):
                        s = SComment()
                s = s.setCurrentLine(self.line).setPrestate(self)
                return s.interpret()
        def interpret(self):
                if self.isSelfState():
                        return self.interpretline()
                else:
                        return self.delegate()
        def isSelfState(self):
                return False
        def interpretline(self):
                return SIni().setCurrentLine(self.line).interpret()
        
        def tolist(self):
                ret = []
                cur = self
                while not cur.prestate is None:
                        ret.insert(0, cur)
                        cur = cur.prestate
                return ret
        def __iter__(self):
                for elm in self.tolist():
                        yield elm

class SIni(CsaState):
        def __init__(self,l="",pre=None):
                self.line = l
                self.prestate = None
        def isSelfState(self): return False
        def interpret(self):
                return self.delegate()
        #def _interpretline(self): pass

class ConcleteState(CsaState):
        def __init__(self,l="",pre=SIni()):
                self.line = l
                self.prestate = pre
                self.buf = []
        def pushBuf(self,l):
                self.buf.insert(len(self.buf), l)
        def getLatestBuf(self):
                try:
                        return self.buf[-1]
                except Exception:
                        return None
        #def __iter__(self):
        #        for l in self.buf:
        #                yield l


class SVer(ConcleteState):
        def isSelfState(self):
                if(Ut.isVer(self.line)):return True
                return False
        def interpretline(self):
                #print "SVer        : "+self.line
                self.pushBuf(self.line)
                return self
class SMeta(ConcleteState):
        BLACK=kd.TE.TURN.BLACK
        WHITE=kd.TE.TURN.WHITE
        def __init__(self,l="",pre=SIni()):
                ConcleteState.__init__(self,l,pre)
                self.player = {SMeta.BLACK: "", SMeta.WHITE: ""}
        def isSelfState(self):
                if(Ut.isMeta(self.line)):return True
                return False
        def interpretline(self):
                #print "SMeta        : "+self.line
                self.pushBuf(self.line)
                if(re.match("^N[+-]",self.line)):
                        if(self.line[1]=="+"):
                                self.player[SMeta.BLACK]=self.line[2:]
                        if(self.line[1]=="-"):
                                self.player[SMeta.WHITE]=self.line[2:]
                return self
class SStart(ConcleteState):
        """
        実装の手間を省くため、内容に関係なく全部平手と解釈させる。
        本来なら、クライアント側にはそれがわからないような感じのコードにならないとおかしいが、
        多分そこすら省くかも
        """
        def __init__(self, l="",pre=SIni()):
                #ConcleteState.__init__(l,pre)
                ConcleteState.__init__(self,l,pre)
        def isSelfState(self):
                if(Ut.isStart(self.line)):return True
                return False
        def interpretline(self):
                #print "SStart        : "+self.line
                preline = self.getLatestBuf()
                self.pushBuf(self.line)
                return self
class SMove(ConcleteState):
        """
        一番避けて通れない部分の実装
        データ構造は辞書で簡易的に対応
        """
        def __init__(self, l="",pre=SIni()):
                ConcleteState.__init__(self,l,pre)
                self.moves = []
        def isSelfState(self):
                if(Ut.isMove(self.line)):return True
                return False
        def interpretline(self):
                #print "SMove        : "+self.line
                self.pushBuf(self.line)
                
                move = {
                        "type": "REGULAR",
                        "command": "",
                        "turn":"",
                        "from":(0,0),
                        "to":(0,0),
                        "piece": "N" }
                if(self.line[0]=="+" or self.line[0]=="-"):
                        """
                        Regular move
                        """
                        if(self.line[0]=="+"):        move["turn"] = "+"
                        if(self.line[0]=="-"):        move["turn"] = "-"        
                        
                        if(not re.match("^(00[1-9]{2}|[1-9]{4})$",self.line[1:5])): raise CsaSyntaxError, self.line
                        move["from"] = (int(self.line[1]), int(self.line[2]))
                        move["to"]   = (int(self.line[3]), int(self.line[4]))
                        
                        if(not re.match("(FU|KY|KE|GI|KI|KA|HI|OU|TO|NY|NK|NG|UM|RY)$", self.line[5:7])):
                                print "raise CsaSyntaxError: pi "+self.line[5:7]
                                raise CsaSyntaxError, self.line
                        move["piece"] = self.line[5:7]
                        self.moves.insert(len(self.moves), move)
                elif(self.line[0]=="%"):
                        """
                        Special move
                        """
                        move["type"]    = "SPECIAL"
                        move["command"] = self.line[1:]
                        self.moves.insert(len(self.moves), move)
                else:
                        print "raise CsaSyntaxError"
                        raise CsaSyntaxError, self.line
                return self
class SComment(ConcleteState):
        def isSelfState(self):
                if(Ut.isComment(self.line)):return True
                return False
        def interpretline(self):
                #print "SComment        : "+self.line
                self.pushBuf(self.line)
                return self

#class AbstractDocumentBuilder:
        """
        Builderの目的は「同じ作成手順」で「中身が異なるもの」を作れること。
        今回適用するのであれば、使い方は割とクライアントに近い側で、コードは
                def constract(self)
                        builder = Factory.CreateConcleteDocumentBuilder()
                        for s in result:  # iterate stat(s) chain list
                                builder.add(s)
                        builder.buildMetaInfo()
                        builder.buildKifuList()
                        document = builder.getProduct()
                        return document
        のようになると思われる
        
        Builderを使う動機として、
        Documentを構成する方法とか、表現形式とか、そういう部分の変更に対して
        上記のコードが独立であり、再利用できるということが重要。
        
        """
#        pass

def test():
        itp = SimpleCsaInterpreter(file("sample.csa"))
        res = itp.interpret()
        """
        print "=============="
        for elm in res:
                #if(elm.__class__ == SMove):
                #        for l in elm.buf: print l
                #else: 
                print elm.__class__.__name__
                print elm.buf
        
        for elm in res:
                if(isinstance(elm, SMove)):
                        for m in elm.moves:
                                print m
        """

        # 棋譜ディレクトリ以下の全てのcsaファイルについて適用
        builder = CSADocumentBuilderFactory.Create(res)
        builder.buildHeader()
        builder.buildBody()
        doc = builder.getProduct()
        print "================================================="
        print doc["header"]
        #print doc["body"]
        

        p = os.popen("ls kifus/*.csa", "r")
        for fn in p.readlines():
                itp = SimpleCsaInterpreter(file(fn[:-1]))
                res = itp.interpret()
                b = CSADocumentBuilderFactory.Create(res)
                b.buildHeader()
                b.buildBody()
                ret = b.getProduct()
                print ret["body"]
                print "========================"

if __name__=="__main__":
        test()

コードを隠す

上記のコードの実行結果は以下。長いので中略してます。

実行結果の表示

実行結果を隠す

=================================================
# サンプル用の棋譜。ヘッダ部とボディ部を表示
{'player': {'+': 'Bonanza', '-': 'YSS'}}
[{'from': (2, 7), 'turn': '+', 'to': (2, 6), 'command': '', 'piece': 'FU', 'type': 'REGULAR'}, ...略... , {'from': (0, 0), 'turn': '+', 'to': (7, 7), 'command': '', 'piece': 'GI', 'type': 'REGULAR'}, {'from': (5, 8), 'turn': '-', 'to': (6, 9), 'command': '', 'piece': 'TO', 'type': 'REGULAR'}]
========================
#... 棋譜リスト全てについて表示 ...
[{'from': (2, 7), 'turn': '+', 'to': (2, 6), 'command': '', 'piece': 'FU', 'type': 'REGULAR'}, ...略... , {'from': (5, 9), 'turn': '-', 'to': (5, 8), 'command': '', 'piece': 'UM', 'type': 'REGULAR'}, {'from': (0, 0), 'turn': '', 'to': (0, 0), 'command': 'TORYO', 'piece': 'N', 'type': 'SPECIAL'}]
========================

実行結果を隠す

...なんかサンプル用の棋譜は投了が反映されてない。なんかおかしいです。

ちなみにサンプル用の棋譜だけ表示させるようにする(test()の最初の方のコード)と以下のようになります。こっちは正常なのでなんかどこかでつまらないミスをしてると思われます。

{'from': (7, 7), 'turn': '+', 'to': (7, 6), 'command': '', 'piece': 'FU', 'type': 'REGULAR'}
{'from': (8, 3), 'turn': '-', 'to': (8, 4), 'command': '', 'piece': 'FU', 'type': 'REGULAR'}
{'from': (7, 9), 'turn': '+', 'to': (6, 8), 'command': '', 'piece': 'GI', 'type': 'REGULAR'}
{'from': (3, 3), 'turn': '-', 'to': (3, 4), 'command': '', 'piece': 'FU', 'type': 'REGULAR'}
{'from': (6, 7), 'turn': '+', 'to': (6, 6), 'command': '', 'piece': 'FU', 'type': 'REGULAR'}
(中略)
{'from': (0, 0), 'turn': '+', 'to': (2, 2), 'command': '', 'piece': 'KI', 'type': 'REGULAR'}
{'from': (0, 0), 'turn': '', 'to': (0, 0), 'command': 'TORYO', 'piece': 'N', 'type': 'SPECIAL'}

実行結果を隠す

これを元に盤上の動きをシミュレートし、意味的な誤りをチェックしていく感じになります。「取った」とか「避けた」「王手」「合駒」などなど、手の性質のようなものを検出することも必要になってくるでしょう。盤と駒については以前のプロトタイプ実装があるのでその辺も活用していけたらいいなあ、と。

これからは既存コードとの絡み合いも増えてくるでしょうし、テストのノウハウもそろそろ勉強が必要ですねぇ...。あとはデザインパターンに固執せず、手抜ける所では辞書型をはじめとする組み込み型を使っていくようにした方がいい。下手にパターンを使うと混乱するからやめとけ、という教えもありますし、組み込み型は自前のクラスと違って十分テストされてて信頼できますので。バグを発見しやすくするための実装、というのも研究していく必要がありそうです。

コードの解説とかした方がいいのでしょうが、自分で見返してみても汚すぎることとと、気力の問題により記事には盛り込みません。解釈部分についてはStateパターンを使っています。この辺の記事も合わせて参照していただけるとうれしいです。では本日はここまで。