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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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


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

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

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


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

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

main.py
1
def castling_requirements(self, piece, endpos, side, gameboard):
2
'''
3
キャスリングの条件を満たすとき,True
4
side == 0 -> aファイル側
5
side == 1 -> hファイル側
6
7
Parameters
8
----------
9
piece : obj
10
駒.キングでなければ return は False.
11
endpos : tuple > (int, int)
12
終了位置.絶対座標.
13
side : int > 0, 1
14
0 -- クイーンサイド
15
1 -- キングサイド
16
gameboard : dict > {(int, int): obj, ...}
17
盤面.
18
19
Returns
20
-------
21
bool
22
'''
23
def create_tmp_board(startpos_y, endpos):
24
'''
25
キングの通過するマスが攻撃されていないことを確認するために,
26
キングがそのマスに動いたときに攻撃されるかを見るための
27
仮の盤面を出力する
28
29
Parameters
30
----------
31
startpos_y : int
32
開始位置y座標.
33
endpos : tuple > (int, int)
34
終了位置.絶対座標.
35
36
Returns
37
-------
38
gameboard_tmp : dict > {(int, int): obj, ...}
39
'''
40
...
41
42
def path_is_not_attacked(startpos_y, king_route):
43
'''
44
キングが通るマスのどれかが相手の駒に攻撃されていれば False を返す
45
46
Parameters
47
----------
48
startpos_y : int
49
開始位置y座標.
50
king_route : list > [int, ...]
51
キングが通る位置x座標のリスト.
52
53
Returns
54
-------
55
bool
56
'''
57
...
58
59
common_req = (self.can_castling[piece.color][side] # キャスリングに関与する駒が一度も動いていない
60
and not self.is_check(piece.color, gameboard)) # キングがチェックされていない
61
# 白のキャスリング
62
if piece.color == 'W':
63
piece_req = (piece.name == 'WK'
64
and (7*side, 0) in gameboard
65
and gameboard[(7*side, 0)].name == 'WR')
66
# クイーンサイド
67
if side == 0:
68
special_req = (endpos == (2, 0)
69
# キングとルークの間に駒がない
70
and (1, 0) not in self.gameboard
71
and (2, 0) not in self.gameboard
72
and (3, 0) not in self.gameboard
73
# キングが通過するマスが敵に攻撃されていない
74
and path_is_not_attacked(0, [2, 3])
75
)
76
# キングサイド
77
if side == 1:
78
special_req = (endpos == (6, 0)
79
# キングとルークの通過するマスに駒がない
80
and (6, 0) not in self.gameboard
81
and (5, 0) not in self.gameboard
82
# キングが通過するマスが敵に攻撃されていない
83
and path_is_not_attacked(0, [6, 5])
84
)
85
# 黒のキャスリング
86
if piece.color == 'B':
87
piece_req = (piece.name == 'BK'
88
and (7*side, 7) in gameboard
89
and gameboard[(7*side, 7)].name == 'BR')
90
# クイーンサイド
91
if side == 0:
92
special_req = (endpos == (2, 7)
93
# キングとルークの通過するマスに駒がない
94
and (1, 7) not in self.gameboard
95
and (2, 7) not in self.gameboard
96
and (3, 7) not in self.gameboard
97
# キングが通過するマスが敵に攻撃されていない
98
and path_is_not_attacked(7, [2, 3])
99
)
100
# キングサイド
101
if side == 1:
102
special_req = (endpos == (6, 7)
103
# キングとルークの通過するマスに駒がない
104
and (6, 7) not in self.gameboard
105
and (5, 7) not in self.gameboard
106
# キングが通過するマスが敵に攻撃されていない
107
and path_is_not_attacked(7, [6, 5])
108
)
109
110
return common_req and piece_req and special_req

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

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

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


piece_reqはどうでしょう。

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

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

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


special_reqも編集が必要です。

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

main.py
1
def castling_requirements(self, piece, endpos, side, gameboard):
2
...
3
def create_tmp_board(startpos_y, endpos):
4
'''
5
キングの通過するマスが攻撃されていないことを確認するために,
6
キングがそのマスに動いたときに攻撃されるかを見るための
7
仮の盤面を出力する
8
9
Parameters
10
----------
11
startpos_y : int
12
開始位置y座標.
13
endpos : tuple > (int, int)
14
終了位置.絶対座標.
15
16
Returns
17
-------
18
gameboard_tmp : dict > {(int, int): obj, ...}
19
'''
20
gameboard_tmp = copy(gameboard)
21
if (4, startpos_y) in gameboard_tmp:
22
gameboard_tmp[endpos] = gameboard_tmp[(4, startpos_y)]
23
del gameboard_tmp[(4, startpos_y)]
24
if (king_init_pos, startpos_y) in gameboard_tmp:
25
gameboard_tmp[endpos] = gameboard_tmp[(king_init_pos, startpos_y)]
26
del gameboard_tmp[(king_init_pos, startpos_y)]
27
return gameboard_tmp

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

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

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

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

ではまた👋