チェスアプリ開発(18) キャスリングの拡張(条件の一般化)

 

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

前回はチェス960の初期配置を生成して、基本的な部分は遊べるようになりました。

しかしチェス960のキャスリングは通常のキャスリングを拡大解釈する必要があります。

今回はキャスリングの部分のコードを書き換えていきます。

拡張前のキャスリングの実装は第12回に載せています。

関連記事

チェスアプリ開発(12) キャスリングの実装

チェスアプリ開発(12) キャスリングの実装


チェス960におけるキャスリング

チェス960におけるキャスリングは、通常のチェスのキャスリング後のキングとルークの位置と同じになるようにキングとルークを動かします。

つまり、a ファイル(白から見て左側)へのキャスリング(通常のチェスでのロングキャスリング)ではキングが c ファイルに、ルークが d ファイルに移動し、

h ファイル(白から見て右側)へのキャスリング(通常のチェスでのショートキャスリング)ではキングが g ファイルに、ルークが f ファイルに移動します。

キャスリングの条件も通常のチェスと変わりません。


通常のチェスと異なるのは、初期位置が異なるために、

キングやルークが全く移動しなかったり、かなり大きく移動したりすることがあるということです。

キングが1歩しか移動しないキャスリングもありえ、その場合はキャスリングなのかキングが動いただけなのかが曖昧になります。

そのあたりも考えながらコードに落とし込んでいきます。


キャスリングのコードを編集

通常のチェスのキャスリングの実装は第12回で行いました。

この時点では「キングは必ず2歩移動する」など固定的な性質を利用して書いていた部分がありますので、これを一般化していきます。


キャスリングの条件を満たすかどうかを返すメソッドでは、

common_req, piece_req, special_reqという3つの条件がすべてTrueのときにTrueを返すようにしていました。

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
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def castling_requirements(self, piece, endpos, side, gameboard):
    '''
    キャスリングの条件を満たすとき,True
    side == 0 -> aファイル側
    side == 1 -> hファイル側

    Parameters
    ----------
    piece : obj
        駒.キングでなければ return は False.
    endpos : tuple > (int, int)
        終了位置.絶対座標.
    side : int > 0, 1
        0 -- クイーンサイド
        1 -- キングサイド
    gameboard : dict > {(int, int): obj, ...}
        盤面.

    Returns
    -------
    bool
    '''
    def create_tmp_board(startpos_y, endpos):
        '''
        キングの通過するマスが攻撃されていないことを確認するために,
        キングがそのマスに動いたときに攻撃されるかを見るための
        仮の盤面を出力する
        
        Parameters
        ----------
        startpos_y : int
            開始位置y座標.
        endpos : tuple > (int, int)
            終了位置.絶対座標.

        Returns
        -------
        gameboard_tmp : dict > {(int, int): obj, ...}
        '''
        ...

    def path_is_not_attacked(startpos_y, king_route):
        '''
        キングが通るマスのどれかが相手の駒に攻撃されていれば False を返す
        
        Parameters
        ----------
        startpos_y : int
            開始位置y座標.
        king_route : list > [int, ...]
            キングが通る位置x座標のリスト.

        Returns
        -------
        bool
        '''
        ...

    common_req = (self.can_castling[piece.color][side]  # キャスリングに関与する駒が一度も動いていない
                    and not self.is_check(piece.color, gameboard))  # キングがチェックされていない
    # 白のキャスリング
    if piece.color == 'W':
        piece_req = (piece.name == 'WK'
                        and (7*side, 0) in gameboard
                        and gameboard[(7*side, 0)].name == 'WR')
        # クイーンサイド
        if side == 0:
            special_req = (endpos == (2, 0)
                            # キングとルークの間に駒がない
                            and (1, 0) not in self.gameboard
                            and (2, 0) not in self.gameboard
                            and (3, 0) not in self.gameboard
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(0, [2, 3])
                            )
        # キングサイド
        if side == 1:
            special_req = (endpos == (6, 0)
                            # キングとルークの通過するマスに駒がない
                            and (6, 0) not in self.gameboard
                            and (5, 0) not in self.gameboard
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(0, [6, 5])
                            )
    # 黒のキャスリング
    if piece.color == 'B':
        piece_req = (piece.name == 'BK'
                        and (7*side, 7) in gameboard
                        and gameboard[(7*side, 7)].name == 'BR')
        # クイーンサイド
        if side == 0:
            special_req = (endpos == (2, 7)
                            # キングとルークの通過するマスに駒がない
                            and (1, 7) not in self.gameboard
                            and (2, 7) not in self.gameboard
                            and (3, 7) not in self.gameboard
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(7, [2, 3])
                            )
        # キングサイド
        if side == 1:
            special_req = (endpos == (6, 7)
                            # キングとルークの通過するマスに駒がない
                            and (6, 7) not in self.gameboard
                            and (5, 7) not in self.gameboard
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(7, [6, 5])
                            )

    return common_req and piece_req and special_req

このうちcommon_reqについては編集する必要がありません。

これは「キャスリングに関与する駒が一度も動いていない」および「キングがチェックされていない」ことを保証する変数ですが、

これらはキングやルークの初期位置が関わらないからです。


piece_reqはどうでしょう。

これは「動かす駒が自分のキングであること」と「キャスリングに関与するルークが初期位置にあること」を保証します。

前半は関係しませんが、後半は初期位置が関係してくるので、編集が必要です。

必要になるのはルークの初期位置です。


special_reqも編集が必要です。

これには「キングのキャスリング後の位置」「ルークの通過するマスの座標」「キングが通過するマスの座標」の3つが必要であり、

このうち後ろの2つを変更しなければなりません。

それには「ルークの初期位置」「キングの初期位置」が最低限必要になります。

ルーク・キングの初期位置を取得する

初期位置といっても x 座標しか使いませんので、それだけ取得できれば十分です。

初期位置はgamesモジュールに定義されている各クラスのプロパティplacersに、次のような形で定義されています。

1
2
placers = {1: [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook],
        2: [Pawn] * size}

x 座標はリストのインデックスと等しいので、enumerate関数を使って次のように初期位置のリストを取得することができます。

1
2
rook_init_pos = [pos for pos, piece in enumerate(placers[1]) if piece == Rook]
king_init_pos = placers[1].index(King)

ルークは複数個存在しえますが、キングはひとつしか存在しえないので、indexを使えばよいでしょう。


これらを使って、piece_reqspecial_reqを書き換えると、例えば次のようになります。

1
2
3
4
5
piece_req = (piece.name == 'WK'
                and (7*side, 0) in gameboard
                and gameboard[(7*side, 0)].name == 'WR')
                and (rook_init_pos[side], 0) in gameboard
                and gameboard[(rook_init_pos[side], 0)].name == 'WR')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
gameboard_tmp = copy(gameboard)
# キャスリングに関与するキングとルークは除外して考える
if (king_init_pos, 0) in gameboard_tmp:
    del gameboard_tmp[(king_init_pos, 0)]
if (rook_init_pos[side], 0) in gameboard_tmp:
    del gameboard_tmp[(rook_init_pos[side], 0)]
# キングとルークの通過するマス
king_route = list(range(2, king_init_pos)) + list(range(2, king_init_pos, -1))
rook_route = list(range(3, rook_init_pos[side])) + list(range(3, rook_init_pos[side], -1))
special_req = (endpos == (2, 0)
                # キングとルークの通過するマスに駒がない
                and (1, 0) not in self.gameboard
                and (2, 0) not in self.gameboard
                and (3, 0) not in self.gameboard
                and not any((x, 0) in gameboard_tmp
                    for x in king_route + rook_route)
                # キングが通過するマスが敵に攻撃されていない
                and path_is_not_attacked(0, [2, 3])
                and path_is_not_attacked(0, list(x for x in range(2, king_init_pos))
                )

同様にして他の場合についても書き換えると次のようになります。

盤面の大きさはゲームによっては8とは限らないので、sizeから計算しています。

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
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def castling_requirements(self, piece, endpos, side, gameboard):
    ...
    size = self.kind.size
    
    rook_init_pos = [pos for pos, piece in enumerate(self.kind.placers[1]) if piece == Rook]
    king_init_pos = self.kind.placers[1].index(King)
    ...
    common_req = (self.can_castling[piece.color][side]  # キャスリングに関与する駒が一度も動いていない
                    and not self.is_check(piece.color, gameboard))  # キングがチェックされていない
    # 白のキャスリング
    if piece.color == 'W':
        piece_req = (piece.name == 'WK'
                        and (7*side, 0) in gameboard
                        and gameboard[(7*side, 0)].name == 'WR')
                        and (rook_init_pos[side], 0) in gameboard
                        and gameboard[(rook_init_pos[side], 0)].name == 'WR')
        gameboard_tmp = copy(gameboard)
        # キャスリングに関与するキングとルークは除外して考える
        if (king_init_pos, 0) in gameboard_tmp:
            del gameboard_tmp[(king_init_pos, 0)]
        if (rook_init_pos[side], 0) in gameboard_tmp:
            del gameboard_tmp[(rook_init_pos[side], 0)]
        # クイーンサイド
        if side == 0:
            # キングとルークの通過するマス
            king_route = list(range(2, king_init_pos)) + list(range(2, king_init_pos, -1))
            rook_route = list(range(3, rook_init_pos[side])) + list(range(3, rook_init_pos[side], -1))
            special_req = (endpos == (2, 0)
                            # キングとルークの間に駒がない
                            and (1, 0) not in self.gameboard
                            and (2, 0) not in self.gameboard
                            and (3, 0) not in self.gameboard
                            and not any((x, 0) in gameboard_tmp
                                for x in king_route + rook_route)
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(0, [2, 3])
                            and path_is_not_attacked(0, list(x for x in range(2, king_init_pos)))
                            )
        # キングサイド
        if side == 1:
            # キングとルークの通過するマス
            king_route = list(range(size - 2, king_init_pos)) + list(range(size - 2, king_init_pos, -1))
            rook_route = list(range(size - 3, rook_init_pos[side])) + list(range(size - 3, rook_init_pos[side], -1))
            special_req = (endpos == (6, 0)
            special_req = (endpos == (size - 2, 0)
                            # キングとルークの通過するマスに駒がない
                            and (6, 0) not in self.gameboard
                            and (5, 0) not in self.gameboard
                            and not any((x, 0) in gameboard_tmp
                                for x in king_route + rook_route)
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(0, [6, 5])
                            and path_is_not_attacked(0, list(x for x in range(size - 2, king_init_pos, -1)))
                            )
    # 黒のキャスリング
    if piece.color == 'B':
        piece_req = (piece.name == 'BK'
                        and (7*side, 7) in gameboard
                        and gameboard[(7*side, 7)].name == 'BR')
                        and (rook_init_pos[side], size - 1) in gameboard
                        and gameboard[(rook_init_pos[side], size - 1)].name == 'BR')
        gameboard_tmp = copy(gameboard)
        # キャスリングに関与するキングとルークは除外して考える
        if (king_init_pos, size - 1) in gameboard_tmp:
            del gameboard_tmp[(king_init_pos, size - 1)]
        if (rook_init_pos[side], size - 1) in gameboard_tmp:
            del gameboard_tmp[(rook_init_pos[side], size - 1)]
        # クイーンサイド
        if side == 0:
            # キングとルークの通過するマス
            king_route = list(range(2, king_init_pos)) + list(range(2, king_init_pos, -1))
            rook_route = list(range(3, rook_init_pos[side])) + list(range(3, rook_init_pos[side], -1))
            special_req = (endpos == (2, 7)
            special_req = (endpos == (2, size - 1)
                            # キングとルークの通過するマスに駒がない
                            and (1, 7) not in self.gameboard
                            and (2, 7) not in self.gameboard
                            and (3, 7) not in self.gameboard
                            and not any((x, size - 1) in gameboard_tmp
                                for x in king_route + rook_route)
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(7, [2, 3])
                            and path_is_not_attacked(size - 1, list(x for x in range(2, king_init_pos)))
                            )
        # キングサイド
        if side == 1:
            # キングとルークの通過するマス
            king_route = list(range(size - 2, king_init_pos)) + list(range(size - 2, king_init_pos, -1))
            rook_route = list(range(size - 3, rook_init_pos[side])) + list(range(size - 3, rook_init_pos[side], -1))
            special_req = (endpos == (6, 7)
            special_req = (endpos == (size - 2, size - 1)
                            # キングとルークの通過するマスに駒がない
                            and (6, 7) not in self.gameboard
                            and (5, 7) not in self.gameboard
                            and not any((x, size - 1) in gameboard_tmp
                                for x in king_route + rook_route)
                            # キングが通過するマスが敵に攻撃されていない
                            and path_is_not_attacked(7, [6, 5])
                            and path_is_not_attacked(size - 1, list(x for x in range(size - 2, king_init_pos, -1)))
                            )

    return common_req and piece_req and special_req

create_tmp_boardの中身も変更しておきます。

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
25
26
27
def castling_requirements(self, piece, endpos, side, gameboard):
    ...
    def create_tmp_board(startpos_y, endpos):
        '''
        キングの通過するマスが攻撃されていないことを確認するために,
        キングがそのマスに動いたときに攻撃されるかを見るための
        仮の盤面を出力する

        Parameters
        ----------
        startpos_y : int
            開始位置y座標.
        endpos : tuple > (int, int)
            終了位置.絶対座標.

        Returns
        -------
        gameboard_tmp : dict > {(int, int): obj, ...}
        '''
        gameboard_tmp = copy(gameboard)
        if (4, startpos_y) in gameboard_tmp:
            gameboard_tmp[endpos] = gameboard_tmp[(4, startpos_y)]
            del gameboard_tmp[(4, startpos_y)]
        if (king_init_pos, startpos_y) in gameboard_tmp:
                gameboard_tmp[endpos] = gameboard_tmp[(king_init_pos, startpos_y)]
                del gameboard_tmp[(king_init_pos, startpos_y)]
        return gameboard_tmp

これでキャスリングの条件判定は正しく行えるようになりました。

ただし、まだ駒の再配置のコードの変更や、キングが1歩しか移動しない場合の曖昧性の排除が必要です。

長くなってしまったので、それは次回以降にまわしたいと思います。

お読みいただきありがとうございました。

ではまた👋