チェスアプリ開発(4) チェックされに行く動きの禁止

 

Python プログラムで動かすフェアリーチェスアプリ開発、連載第4回です。

前回は、ポーンについてその動きの仕組みを見たうえで、

最初の2歩の動きを追加してみました。

今回からはチェックやチェックメイトを判定できるようにしましょう!

ゲーム構造の核心に近い部分に触れていきますよ。


すでにisCheck()はあるが…

チェックを判定する メソッド クラス内で定義される関数のようなもの はすでに用意されています。

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def isCheck(self):
    #ascertain where the kings are, check all pieces of opposing color against those kings, then if either get hit, check if its checkmate
    king = King
    kingDict = {}
    pieceDict = {BLACK : [], WHITE : []}
    for position,piece in self.gameboard.items():
        if type(piece) == King:
            kingDict[piece.Color] = position
        print(piece)
        pieceDict[piece.Color].append((piece,position))
    #white
    if self.canSeeKing(kingDict[WHITE],pieceDict[BLACK]):
        self.message = "White player is in check"
    if self.canSeeKing(kingDict[BLACK],pieceDict[WHITE]):
        self.message = "Black player is in check"
 
def canSeeKing(self,kingpos,piecelist):
    #checks if any pieces in piece list (which is an array of (piece,position) tuples) can see the king in kingpos
    for piece,position in piecelist:
        if piece.isValid(position,kingpos,piece.Color,self.gameboard):
            return True

とはいえ、これだけでは十分ではありません。

  • チェックメイトの判定
  • ステイルメイトの判定
  • 自らチェックされに行くような手の禁止

この 3 つを追加していきたいと思います。


どれを先にやる?

チェックメイト・ステイルメイトは、動けないという状態を共有しているので、

つながりが深く、同じような書き方ができると考えられます。

これらの判定には、誰がどこに動けるかの情報が使われます。

自らチェックされに行くような手を先に禁止しておくことで、駒がどこに移動可能かという情報が整理されるので、

先にこちらに取り掛かりたいと思います。


自らチェックされに行くような手の禁止

駒の移動先の可能性を制御しているのは、isValid()メソッドです。

pieces.py
1
2
3
4
def isValid(self,startpos,endpos,Color,gameboard):
    if endpos in self.availableMoves(startpos[0],startpos[1],gameboard, Color = Color):
        return True
    return False

今からこれをチェックと関連づけたいのに、これはPiece クラス オブジェクトの型、設計図のようなもの 内のメソッドですね。

GameクラスにあるisCheck()などをこちらに取り込むことができません。

ということで、このメソッドにGameクラス内にお引っ越ししていただきましょう。

(紛らわしいのでisValidMove()と改名します。)

ただし、住所が変わりますので、selfの意味が変わります。

selfが何かについては、解説してくれているサイトが多くあるのですが、かいつまんで言うと、

クラス内のメソッドがselfといったとき、それはその所属するクラスをもとに生成された インスタンス(オブジェクト) クラスをもとに作られた分身のようなもの を指します。

Pieceクラス内にいたとき、selfはポーンやナイトなど駒のことを指すわけです。

メソッド内でselfを使っているので、Gameクラス内に移動した後は新たに駒を示す引数を追加する必要があります。

main.py
1
2
3
4
5
6
7
class Game:
    ...
    def isValidMove(self, piece, startpos, endpos, Color, gameboard):
        '''pieceのstartpos -> endposの動きが可能であるときTrueを返す'''
        if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color = Color):
            return True
        return False

ここで引数について、pieceは動く駒、startposendposは開始位置と終了位置、Colorは駒色、gameboardは盤面のことです。

さて、「自らチェックされに行くような手を禁止する」というのは、

pieceがキングで、キングのendposが敵の攻撃範囲になければいいということでしょうか?

いえ、実は動く駒はキングとは限りません。

キングの盾になっていた駒が動くことでキングを攻撃にさらすこともあります。

だから、駒の種類を限らず「その駒が仮にその動きをしたとすると、その場合チェック状態になるか」を見ればいいのです。

チェック状態にあるかどうかを判定させる

ところで、さきほどのisCheck()は返り値を取りません。

駒色を引数にとり、その色側がチェックされているときにTrueを返すようにしましょう。

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def isCheck(self):
def isCheck(self, color):
    king = King
    kingDict = {}
    pieceDict = {BLACK : [], WHITE : []}
    for position, piece in self.gameboard.items():
        if type(piece) == King:
            kingDict[piece.Color] = position
        pieceDict[piece.Color].append((piece, position))
    if self.canSeeKing(kingDict[WHITE], pieceDict[BLACK]):
    if color == WHITE:
        if self.canSeeKing(kingDict[WHITE], pieceDict[BLACK]):
            self.message = "White player is in check"
            return True
    if self.canSeeKing(kingDict[BLACK], pieceDict[WHITE]):
    elif color == BLACK:
        if self.canSeeKing(kingDict[BLACK], pieceDict[WHITE]):
            self.message = "Black player is in check"
            return True

うーん、工夫すればもうちょっと簡潔に書けますね。

コードの頭に一文追加します。

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
opponent = {WHITE: BLACK, BLACK: WHITE}
 
class Game:
    ...
    def isCheck(self, color):
        kingDict = {}
        pieceDict = {BLACK : [], WHITE : []}
        for position, piece in self.gameboard.items():
            if type(piece) == King:
                kingDict[piece.Color] = position
            pieceDict[piece.Color].append((piece, position))
        if self.canSeeKing(kingDict[color], pieceDict[opponent[color]]):
            self.message = f"{color} player is in check"
            return True

これでisCheck()はチェック判定用に使えるようになりました。

「白はチェックされてる?」と聞けば「白はね、されてるよ」とか「白はね、されてるとはいえないみたいだよ」とか答えてくれます。


ところで、動いた後の盤面を仮定して、その盤面上でチェック判定をするんでした。

ところがisCheck()(とcanSeeKing())で使われている盤面はself.gameboardです。

これは現在の本当の盤面なので、仮定された盤面は使えません。

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def isCheck(self, color):
def isCheck(self, color, gameboard):
    kingDict = {}
    pieceDict = {BLACK : [], WHITE : []}
    for position, piece in self.gameboard.items():
    for position, piece in gameboard.items():
        if type(piece) == King:
            kingDict[piece.Color] = position
        pieceDict[piece.Color].append((piece, position))
    if self.canSeeKing(kingDict[color], pieceDict[opponent[color]]):
    if self.canSeeKing(kingDict[color], pieceDict[opponent[color]], gameboard):
        self.message = f"{color} player is in check"
        return True
 
def canSeeKing(self, kingpos, piecelist):
def canSeeKing(self, kingpos, piecelist, gameboard):
    for piece, position in piecelist:
        if piece.isValid(position, kingpos, piece.Color, self.gameboard):
        if piece.isValid(position, kingpos, piece.Color, gameboard):
            return True

引数を追加しました。

もしself.gameboardを使いたくなったらこの引数に指定してしまえば大丈夫です。(gameboard=self.gameboard

盤面の更新

駒の動きを指定した後、盤面は更新されますが、それはどの部分で実行されているのでしょうか。

gameboardを頼りに探すと、main()内にあります。

main.py
1
2
self.gameboard[endpos] = self.gameboard[startpos]
del self.gameboard[startpos]

終了位置に開始位置の駒を移し、開始位置の駒を消すという処理が盤面の更新になります。

盤面を仮定する

盤面を仮定するには、新たな変数を作ってそれにself.gameboardをコピーし、

新たな盤面に対して盤面を更新すればいいでしょう。

その後isCheck()にかけ、Trueになればその手は禁止します。

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from copy import copy
...
class Game:
    ...
    def isValidMove(self, piece, startpos, endpos, Color, gameboard):
        '''pieceのstartpos -> endposの動きが可能であるときTrueを返す'''
        if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color = Color):
            # 盤面の複製
            gameboardTmp = copy(gameboard)
            # 複製した盤面の更新
            gameboardTmp[endpos] = gameboardTmp[startpos]
            del gameboardTmp[startpos]
            # チェック判定
            if self.isCheck(piece.Color, gameboardTmp):
                return False
            else:
                return True
        else:
            return False

引数Colorpiece.Colorは同じ意味なので統一しても構わないでしょう。

main.py
1
2
3
4
5
6
def isValidMove(self, piece, startpos, endpos, Color, gameboard):
def isValidMove(self, piece, startpos, endpos, gameboard):
    '''pieceのstartpos -> endposの動きが可能であるときTrueを返す'''
    if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color = Color):
    if endpos in piece.availableMoves(startpos[0], startpos[1], gameboard, Color=piece.Color):
        ...

ところで、copy()は組み込み関数ではないので、importが必要となります。

❓なぜ直接代入しない?

copy()を使用しないと、盤面の更新をした際にもとの盤面も一緒に書き換えられてしまいます。

これは ディクショナリ キーと値が 1 対 1 に対応する形でまとめられた構造 がミュータブル(変更可能)であることから起こります。

リストでも同様のことは起こります。

>>> a = [1, 2, 3]
>>> b = a
>>> b.append(4)
>>> a
[1, 2, 3, 4]

今までの編集でisCheck()isValidMove()の引数が変わっているので、

それらのメソッドを使っている関係各所の引数も調整しておきましょう。

例えば、main()はこのようになります。

main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def main(self):
 
    while True:
        self.printBoard()
        print(self.message)
        self.message = ""
        startpos, endpos = self.parseInput()
        try:
            target = self.gameboard[startpos]
        except:
            self.message = "could not find piece; index probably out of range"
            target = None
 
        if target:
            print("found "+str(target))
            if target.Color != self.playersturn:
                self.message = "you aren't allowed to move that piece this turn"
                continue
            if self.isValidMove(target, startpos, endpos, self.gameboard):
                self.message = "that is a valid move"
                self.gameboard[endpos] = self.gameboard[startpos]
                del self.gameboard[startpos]
                self.isCheck(target.Color, self.gameboard)
                ...

長くなってしまったので、今回はこのへんで区切りたいと思います。

次回はチェックメイトの判定についてです。

ではまた👋