一応、棋譜のメタ情報(プレイヤ名のみ)と棋譜本体部分を抽出することには成功しました。動いたことによって駒を取ったのか?王手か?みたいな部分は全くなのですが、一応棋譜学習を行う上での必要最低限にはかなり近づいたんじゃないかと思います。αを名乗れるクオリティかどうかは疑問ですけども。
フォーマットについて。内部形式には特にデザインパターンとかを使わないことにします。せっかく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パターンを使っています。この辺の記事も合わせて参照していただけるとうれしいです。では本日はここまで。