Python プログラムで動かすフェアリーチェスアプリ開発、連載第4回です。
は、ポーンについてその動きの仕組みを見たうえで、最初の2歩の動きを追加してみました。
今回からはチェックやチェックメイトを判定できるようにしましょう!
ゲーム構造の核心に近い部分に触れていきますよ。
すでにisCheck()
はあるが…
チェックを判定する
はすでに用意されています。main.py1def isCheck(self):2#ascertain where the kings are, check all pieces of opposing color against those kings, then if either get hit, check if its checkmate3king = King4kingDict = {}5pieceDict = {BLACK : [], WHITE : []}6for position,piece in self.gameboard.items():7if type(piece) == King:8kingDict[piece.Color] = position9print(piece)10pieceDict[piece.Color].append((piece,position))11#white12if self.canSeeKing(kingDict[WHITE],pieceDict[BLACK]):13self.message = "White player is in check"14if self.canSeeKing(kingDict[BLACK],pieceDict[WHITE]):15self.message = "Black player is in check"1617def canSeeKing(self,kingpos,piecelist):18#checks if any pieces in piece list (which is an array of (piece,position) tuples) can see the king in kingpos19for piece,position in piecelist:20if piece.isValid(position,kingpos,piece.Color,self.gameboard):21return True
とはいえ、これだけでは十分ではありません。
- チェックメイトの判定
- ステイルメイトの判定
- 自らチェックされに行くような手の禁止
この 3 つを追加していきたいと思います。
どれを先にやる?
チェックメイト・ステイルメイトは、動けないという状態を共有しているので、
つながりが深く、同じような書き方ができると考えられます。
これらの判定には、誰がどこに動けるかの情報が使われます。
自らチェックされに行くような手を先に禁止しておくことで、駒がどこに移動可能かという情報が整理されるので、
先にこちらに取り掛かりたいと思います。
自らチェックされに行くような手の禁止
駒の移動先の可能性を制御しているのは、isValid()
メソッドです。
pieces.py1def isValid(self,startpos,endpos,Color,gameboard):2if endpos in self.availableMoves(startpos[0],startpos[1],gameboard, Color = Color):3return True4return False
今からこれをチェックと関連づけたいのに、これはPiece
Game
クラスにあるisCheck()
などをこちらに取り込むことができません。
ということで、このメソッドにGame
クラス内にお引っ越ししていただきましょう。
(紛らわしいのでisValidMove()
と改名します。)
ただし、住所が変わりますので、self
の意味が変わります。
self
が何かについては、解説してくれているサイトが多くあるのですが、かいつまんで言うと、
クラス内のメソッドがself
といったとき、それはその所属するクラスをもとに生成された
Piece
クラス内にいたとき、self
はポーンやナイトなど駒のことを指すわけです。
メソッド内でself
を使っているので、Game
クラス内に移動した後は新たに駒を示す引数を追加する必要があります。
main.py1class Game:2...3def isValidMove(self, piece, startpos, endpos, Color, gameboard):4'''pieceのstartpos -> endposの動きが可能であるときTrueを返す'''5if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color = Color):6return True7return False
ここで引数について、piece
は動く駒、startpos
とendpos
は開始位置と終了位置、Color
は駒色、gameboard
は盤面のことです。
さて、「自らチェックされに行くような手を禁止する」というのは、
piece
がキングで、キングのendpos
が敵の攻撃範囲になければいいということでしょうか?
いえ、実は動く駒はキングとは限りません。
キングの盾になっていた駒が動くことでキングを攻撃にさらすこともあります。
だから、駒の種類を限らず「その駒が仮にその動きをしたとすると、その場合チェック状態になるか」を見ればいいのです。
チェック状態にあるかどうかを判定させる
ところで、さきほどのisCheck()
は返り値を取りません。
駒色を引数にとり、その色側がチェックされているときにTrue
を返すようにしましょう。
main.py1def isCheck(self):2def isCheck(self, color):3king = King4kingDict = {}5pieceDict = {BLACK : [], WHITE : []}6for position, piece in self.gameboard.items():7if type(piece) == King:8kingDict[piece.Color] = position9pieceDict[piece.Color].append((piece, position))10if self.canSeeKing(kingDict[WHITE], pieceDict[BLACK]):11if color == WHITE:12if self.canSeeKing(kingDict[WHITE], pieceDict[BLACK]):13self.message = "White player is in check"14return True15if self.canSeeKing(kingDict[BLACK], pieceDict[WHITE]):16elif color == BLACK:17if self.canSeeKing(kingDict[BLACK], pieceDict[WHITE]):18self.message = "Black player is in check"19return True
うーん、工夫すればもうちょっと簡潔に書けますね。
コードの頭に一文追加します。
main.py1opponent = {WHITE: BLACK, BLACK: WHITE}23class Game:4...5def isCheck(self, color):6kingDict = {}7pieceDict = {BLACK : [], WHITE : []}8for position, piece in self.gameboard.items():9if type(piece) == King:10kingDict[piece.Color] = position11pieceDict[piece.Color].append((piece, position))12if self.canSeeKing(kingDict[color], pieceDict[opponent[color]]):13self.message = f"{color} player is in check"14return True
これでisCheck()
はチェック判定用に使えるようになりました。
「白はチェックされてる?」と聞けば「白はね、されてるよ」とか「白はね、されてるとはいえないみたいだよ」とか答えてくれます。
ところで、動いた後の盤面を仮定して、その盤面上でチェック判定をするんでした。
ところがisCheck()
(とcanSeeKing()
)で使われている盤面はself.gameboard
です。
これは現在の本当の盤面なので、仮定された盤面は使えません。
main.py1def isCheck(self, color):2def isCheck(self, color, gameboard):3kingDict = {}4pieceDict = {BLACK : [], WHITE : []}5for position, piece in self.gameboard.items():6for position, piece in gameboard.items():7if type(piece) == King:8kingDict[piece.Color] = position9pieceDict[piece.Color].append((piece, position))10if self.canSeeKing(kingDict[color], pieceDict[opponent[color]]):11if self.canSeeKing(kingDict[color], pieceDict[opponent[color]], gameboard):12self.message = f"{color} player is in check"13return True1415def canSeeKing(self, kingpos, piecelist):16def canSeeKing(self, kingpos, piecelist, gameboard):17for piece, position in piecelist:18if piece.isValid(position, kingpos, piece.Color, self.gameboard):19if piece.isValid(position, kingpos, piece.Color, gameboard):20return True
引数を追加しました。
もしself.gameboard
を使いたくなったらこの引数に指定してしまえば大丈夫です。(gameboard=self.gameboard
)
盤面の更新
駒の動きを指定した後、盤面は更新されますが、それはどの部分で実行されているのでしょうか。
gameboard
を頼りに探すと、main()
内にあります。
main.py1self.gameboard[endpos] = self.gameboard[startpos]2del self.gameboard[startpos]
終了位置に開始位置の駒を移し、開始位置の駒を消すという処理が盤面の更新になります。
盤面を仮定する
盤面を仮定するには、新たな変数を作ってそれにself.gameboard
をコピーし、
新たな盤面に対して盤面を更新すればいいでしょう。
その後isCheck()
にかけ、True
になればその手は禁止します。
main.py1from copy import copy2...3class Game:4...5def isValidMove(self, piece, startpos, endpos, Color, gameboard):6'''pieceのstartpos -> endposの動きが可能であるときTrueを返す'''7if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color = Color):8# 盤面の複製9gameboardTmp = copy(gameboard)10# 複製した盤面の更新11gameboardTmp[endpos] = gameboardTmp[startpos]12del gameboardTmp[startpos]13# チェック判定14if self.isCheck(piece.Color, gameboardTmp):15return False16else:17return True18else:19return False
引数Color
とpiece.Color
は同じ意味なので統一しても構わないでしょう。
main.py1def isValidMove(self, piece, startpos, endpos, Color, gameboard):2def isValidMove(self, piece, startpos, endpos, gameboard):3'''pieceのstartpos -> endposの動きが可能であるときTrueを返す'''4if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color = Color):5if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color=piece.Color):6...
ところで、copy()
は組み込み関数ではないので、import
が必要となります。
なぜ直接代入しない?
copy()
を使用しないと、盤面の更新をした際にもとの盤面も一緒に書き換えられてしまいます。
これは
がミュータブル(変更可能)であることから起こります。リストでも同様のことは起こります。
>>> a = [1, 2, 3]>>> b = a>>> b.append(4)>>> a[1, 2, 3, 4]
今までの編集でisCheck()
やisValidMove()
の引数が変わっているので、
それらのメソッドを使っている関係各所の引数も調整しておきましょう。
例えば、main()
はこのようになります。
main.py1def main(self):23while True:4self.printBoard()5print(self.message)6self.message = ""7startpos, endpos = self.parseInput()8try:9target = self.gameboard[startpos]10except:11self.message = "could not find piece; index probably out of range"12target = None1314if target:15print("found "+str(target))16if target.Color != self.playersturn:17self.message = "you aren't allowed to move that piece this turn"18continue19if self.isValidMove(target, startpos, endpos, self.gameboard):20self.message = "that is a valid move"21self.gameboard[endpos] = self.gameboard[startpos]22del self.gameboard[startpos]23self.isCheck(target.Color, self.gameboard)24...
長くなってしまったので、今回はこのへんで区切りたいと思います。
はチェックメイトの判定についてです。ではまた