nounai.output(spaghetiThinking);

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

MVCっぽい考察

前回前々回の実装を通して、将棋をするための基盤(Model)部分の設計についてそれなり考えられるようになってきました。前回前々回にやってたことはMVCにおけるModel部分ですが、今回は盤の部分や駒の操作(Moveみたいなこと)についてです。MVCのVも絡んだ、視点的にはちょびっとマクロな話題になります。思いついたことを書き殴っておくことにします。


まず、今回は簡易コードによってプロトタイプを書いています。成果物(ソース、実行結果)は後半の方に掲載していますので、ここに来られた方はまずそれを見てもらった方が私の考えが伝わりやすいかもしれません。まぁ、自分のグタグタした考えをつらつら並べるよりはコードを見てもらう方がわかりやすいだろうということですね。ちなみにこの取り組み自体はデザインパターンとか設計の勉強、という意味合いが結構強いです。使いどころとか、構成が適切かどうかという議論はさほど自信がないですが、とりあえず今回のコードではデザインパターンをいくつか使用しています。具体的には以下。

  • Observer
  • Command

どの部分がそうなのか、とかいった話はコードと実行結果の後ろで述べます。とりあえず、以下しばらくは考察したこととかのメモに入ります。

考察MEMO

Boardまわりのコードはほぼ(BoardをSubjectとした、Boardに対する)Observerが必須と考えます。少なくとも、局面が更新される類の操作が行われた際にはView、プレイヤー、棋譜ロガー、あとはタイマー、この辺りに変更を知らせる必要がある。これらの要素は局面の更新など、トリガーとなるイベントに連動する形で自身の処理を回すのが自然な発想かと思います。で、Boardの仕様変更(これは今後起こる可能性が大)の影響でまるごと実装やり直し、なんてのは勘弁してほしいので、少しでもBoardとその他の要素の間の結合を弱くしておく必要があるはずです。多分Observerが一番自然でしょう。

一度Boardを実装してみましたが、どうもピンと来ませんでした。"将棋ゲーム"どの情報をどのクラスで持つか、といった部分で話が難しくなってるのかな、と思います。

それともう1つ。確かOO原則のひとつに「1クラスには単一の責任のみ持たせよ」といった趣旨の教訓があったような気がします。Boardを実装してみてあまりよろしくないと感じたことの1つとして、考えなしに実装しているとBoardの責任が膨れ上がってしまうな、ということがあります。多分どんなプロジェクトでもそういう問題は発生するんでしょうけどね。リファクタリング本にもそういう注意喚起がありました。

で、考えた結果、Boardの責任は「駒の所在情報の保持」にほぼ専念させておくのが良いのではないか?getter/setterとか、isPiece(suji,dan)みたいな基本的なインタフェースは規定するものの、「駒を動かす」とか、(論理的な)操作についてはBoardの責任ではなく、Commandを使って責任を分離させてしまうのがいいんじゃないか?との考えに至りました。

Commandを使うことで、undo(≒待った)とかの実装もCommandの中で修正が効きます。

また、棋譜を採取する方法も割と考えやすくなります。LoggerをBoardをリッスンするObserverとして実装することはほぼ確定していますので、少なくとも棋譜関係のコードはBoardとは独立になる所がメリットです。実際ログ関係の部分はシステム基幹の実装とは無関係であるべきですし。Commandを使うことによって待ったのログも割とスマートに収集できると思います。コンピュータと対戦してるとつい待ったを使ってしまいたくなりますが、その辺のログもバッチリ残すよ、ってのは中々面白い機能だなと思います。

実装してみた

ここからは実際に書いてみたコードです。まず要件的なことを列挙しておきます。

UIが容易にスイッチングできるようにする
ViewはCUI(TUI=TextUIと言った方が正確かも)、GUIの両方を想定し、起動時(できれば実行時も)切り替えが可能になるような設計を目指します。そのような設計にしておくことで、(特にGUIの場合)Model部分を再利用しつつ別のUIを書くことができます。私自身はCUIで十分だと思っていますが、将来GUIを組むことはあり得る話だし、他人に向けてアプリケーションを公開する展開もないとは言いきれません。UIなんてのはユーザの意見次第ですぐ変更の必要が発生する部分なので、そういう設計にしておく必要性はかなり高いです。
(Model部分をSubjectとする)Observerパターンの適用。
(上と被る話ですが、)Observerパターンの適用により、ゲームの進行に連動してViewと棋譜ロガーが動くようにします。ゲームの局面が更新されるのと連動して何かしらの処理、というケースは多分多いでしょう。持ち時間機能にしたってそうですし、(現状では一切考えてませんが、)GUIにおいて着手のエフェクトを掛けるとか、そういうことをやるのであればObserverの仕組みが必要になってきます。Observerになりうるモノ、というのは今回でこそ棋譜ロガーとテキストベースのViewだけですが、実際はもっとたくさんあるものだと考えて設計するのが吉でしょう。
ゲームの進行に関わるオペレーションをCommandパターンで実装。
盤を表現するクラス(Board)は駒の所在情報の保持する責任を持たせます。インタフェースは簡単なアクセサメソッド+α程度にとどめ、「動かす」といった操作はCommandに任せます。「責任の分離」というOOの原則に則る、という理由もありますが割と後付けです。Commandパターン適用可能性の1つである「クライアントが起こしたアクションの履歴を残しておき、場合によってはundoをサポートしたい場合」という部分が私の欲求とマッチしていた、というのが主な動機です。

クラス図とかはもし気が向けば後日掲載します。 キーとなるクラスは

  • Board (Observerパターンにおける"Subject"クラス)
  • BoardObserver (Observerパターンにおける"Observer")
  • GameCommand (Commandパターンの"Command"。Boardを知っている)

の3つ。以下ソースです。

ソース

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

import string
import re

class Board:
        def __init__(self):
                self.observers = []
                self.status = []

        def registObserver(self, ob):
                if(not isinstance(ob, BoardObserver)):
                        raise TypeError, "Board.registObserver(): arg is not BoardObserver"
                self.observers.insert(len(self.observers), ob)
        
        def notifyAll(self):
                for o in self.observers:
                        o.update(self.status[-1])

        def pushState(self, s):
                self.status.insert(len(self.status), s)
        
        def getTotalMove(self):
                return len(self.status)
        
class BoardObserver:
        def update(self, stat):
                pass

class Logger(BoardObserver):
        def __init__(self):
                self.kifu = []
        def update(self, stat):
                self.kifu.insert(len(self.kifu), stat)
        def __iter__(self):
                for l in self.kifu:
                        yield l
        def export(self):
                mblack = re.compile("^\+")
                mwhite = re.compile("^-")
                mresign= re.compile("^%RESIGN")
                s = ""
                for n,l in enumerate(self.kifu):
                        if(n==0):
                                s += "# 対局開始!\n"
                        if(not mblack.match(l) is None):
                                """
                                コメント行などもありうるので手数とファイル内の行数は本来一致しない。
                                が、その辺は簡易実装ということでスルー
                                """
                                s += l+" #先手 "+str(n)+"手目\n"
                        if(not mwhite.match(l) is None):
                                s += l+" #後手 "+str(n)+"手目\n"
                        if(not mresign.match(l) is None):
                                s += "%Resign #投了\n"
                s += "# 対局終了"
                return s

class View(BoardObserver):
        def update(self, stat):
                mb = re.compile("^\+")
                mw = re.compile("^-")
                mr = re.compile("^%RESIGN")
                if(not mb.match(stat) is None):
                        print "(VIEW)Latest move BLACK: "+stat
                if(not mw.match(stat) is None):
                        print "(VIEW)Latest move WHITE: "+stat
                if(not mr.match(stat) is None):
                        print "(VIEW)RESIGN command accepted."


class GameCommand:
        def __init__(self, board, cmdstring):
                self.board = board
                self.cmd = cmdstring
        def execute(self):
                print "Do something..."
                board.nofityAll()

class CmdMove(GameCommand):
        def execute(self):
                print "[CmdMove] execute"
                self.board.pushState(self.cmd)
                self.board.notifyAll()

class CmdResign(GameCommand):
        def execute(self):
                print "[CmdResign] execute"
                print "[CmdResign] TotalMove = "+str(self.board.getTotalMove())
                self.board.pushState(self.cmd)
                self.board.notifyAll()

def main():
        bo = Board()
        logger = Logger()
        view = View()
        
        bo.registObserver(logger)
        bo.registObserver(view)
        
        print "[TEST]GAME START"
        print "========================================="

        kflist = ["+7776FU","-3334FU", "+2625FU", "-8384FU", "%RESIGN"]
        expmove = re.compile("^[+-]")
        expspecial = re.compile("^%")
        for m in kflist:
                if(not expmove.match(m) is None):
                        cmd = CmdMove(bo, m)
                if(not expspecial.match(m) is None):
                        # 簡易コードなのでRESIGNのみを想定
                        cmd = CmdResign(bo, m)
                cmd.execute()
        
        print "===============GAME END================"
        
        print "\n*************************"
        print "[Logger] PRINT KIFU"
        for line in logger:
                print line
        
        print "\n*************************"
        print "[Logger.export()] PRINT KIFU EXPORTED FORM"
        print logger.export()
        
if __name__=="__main__":
        main()

ViewLoggerはBoardObserverを親に持っています。BoardObserverはJavaで言う所のinterfaceに相当します。もっと具体的に言うならAWTやSwingで登場するActionListenerとかです。Boardに登録されているリスナ(observer)が、ちゃんとObserverとしてのインタフェースを持ってることを保証してもらいます。また、GameCommandのサブクラスで着手(Move)や投了(Resign)の実装を行ってます。

これの実行結果は以下。

実行結果

[TEST]GAME START
=========================================
[CmdMove] execute
(VIEW)Latest move BLACK: +7776FU
[CmdMove] execute
(VIEW)Latest move WHITE: -3334FU
[CmdMove] execute
(VIEW)Latest move BLACK: +2625FU
[CmdMove] execute
(VIEW)Latest move WHITE: -8384FU
[CmdResign] execute
[CmdResign] TotalMove = 4
(VIEW)RESIGN command accepted.
===============GAME END================

*************************
[Logger] PRINT KIFU
+7776FU
-3334FU
+2625FU
-8384FU
%RESIGN

*************************
[Logger.export()] PRINT KIFU EXPORTED FORM
# 対局開始!
+7776FU #先手 0手目
-3334FU #後手 1手目
+2625FU #先手 2手目
-8384FU #後手 3手目
%Resign #投了
# 対局終了

LoggerやViewは継承を使って拡張や変更を容易に行えることが分かっていただけるかなと思います。百聞は何とやらなので、以下のセクションでは実際にViewを拡張(変更)してみます。GUIは設計部分とは関係ないコードが増えるのでCUI上で変化をつけてみるに留めますが、GUIでも本質はそう変わらないはずですし拡張による影響がBoard等と無関係に行えることは理解いただけるはずです。実装する上でやることはViewを継承して独自のupdate()を実装するだけ。拡張ViewであるView2クラスは以下。

Viewの拡張例

# 比較のため元のViewクラスも掲載
class View(BoardObserver):
        def update(self, stat):
                mb = re.compile("^\+")
                mw = re.compile("^-")
                mr = re.compile("^%RESIGN")
                if(not mb.match(stat) is None):
                        print "(VIEW)Latest move BLACK: "+stat
                if(not mw.match(stat) is None):
                        print "(VIEW)Latest move WHITE: "+stat
                if(not mr.match(stat) is None):
                        print "(VIEW)RESIGN comand accepted."

# 拡張のため新たに作成したView2クラス
# ちょっとしたViewの変更などが発生してもこのようにサブクラス化で対応できる。
# Open-Closed の原則も満たしている。
class View2(View):
        def update(self, stat):
                mb = re.compile("^\+")
                mw = re.compile("^-")
                mr = re.compile("^%RESIGN")
                if(not mb.match(stat) is None):
                        print "(VIEW)先手の着手: "+stat
                if(not mw.match(stat) is None):
                        print "(VIEW)後手の着手: "+stat
                if(not mr.match(stat) is None):
                        print "(VIEW)投了."

Viewを生成する側、すなわちmain側ではView()とView2()を切り替えるだけです。まぁこの辺もFactoryMethodを使うとかして抽象化した方がいい気はしますけど。main()のコードは元のやつからちょろっと改変してますが、それぞれの実行結果は以下。

Viewクラス(Original)

===== Use View class =====
[CmdMove] execute
(VIEW)Latest move BLACK: +7776FU
[CmdMove] execute
(VIEW)Latest move WHITE: -3334FU
[CmdMove] execute
(VIEW)Latest move BLACK: +2625FU
[CmdMove] execute
(VIEW)Latest move WHITE: -8384FU
[CmdResign] execute
[CmdResign] TotalMove = 4
(VIEW)RESIGN command accepted.

View2クラス(Extend)

===== Use View2 class =====
[CmdMove] execute
(VIEW)先手の着手: +7776FU
[CmdMove] execute
(VIEW)後手の着手: -3334FU
[CmdMove] execute
(VIEW)先手の着手: +2625FU
[CmdMove] execute
(VIEW)後手の着手: -8384FU
[CmdResign] execute
[CmdResign] TotalMove = 4
(VIEW)投了.

とまあこのようになります。将来の変更としてありうる感じなのは、ViewならUIの変更や追加、Loggerについてはサポートするフォーマットの追加や、同一フォーマットでエクスポート形式を変更・拡張(コンピュータの読み筋もコメントで含める形式にするとか)するなどが挙げられますね。Loggerのシナリオについてはレベルの違う拡張が含まれているので、棋譜まわりを真剣に実装するならもうちょい設計を考える余地があるでしょう。

また、今は棋譜に相当するリストを入力として読み上げるだけですが、この辺は工夫の余地大アリです。 入力文字列はだいたいCSAフォーマットに従うように記述してます。で、この構文をメインループ内で簡単に解析しているのですが、ViewとLoggerにもそのコードが存在しています。なんか同じ処理を繰り返している感じでセンスがないですね。本題とは違うので特に装飾とかは付けませんけど、この辺私が納得してないことはちょっと強調しておきたいです。このあたりの設計は多分Stateパターンあたりがマッチするんじゃないかと思いますが、まぁそれはいずれ。

参考書籍