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

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

はチェスの特殊ルールのひとつ、プロモーションを実装しました。

今回はキャスリングという特殊ルールを実装していきます。


キャスリングとは

castling と書き、キングの入城という意味です。

キングとルークを一手で同時に動かすことができますが、動き方や条件が決まっています。

キャスリングの条件は次の通りです。

  1. キングが一歩も動いたことがない
  2. キャスリングに関わるルークが一歩も動いたことがない
  3. キャスリングに関わるルークとキングの間に駒がひとつもない
  4. キング・キングが通るマス・キングの移動先のマスが敵の駒に攻撃されていない

キャスリングの動きは、キングサイドキャスリングとクイーンサイドキャスリングの2通りがあります。

いずれの場合でも、キングがルーク側に2歩動き、ルークがキングの反対側の隣のマスに回り込むという動きをします。

キャスリング 完成イメージ

キャスリングの条件を定義

キャスリングの条件を満たさなければキャスリングはできません。

まずはキャスリングの条件を定義します。

キングとキャスリングに関わるルークが一度でも動くと、その方向へのキャスリングはできなくなります。

各方向へのキャスリングが可能かどうかを表す変数を定義します。

main.py
1
class Game:
2
def __init__(self):
3
...
4
# キャスリング
5
# キャスリングのポテンシャルが残っているか
6
self.can_castling = {'W': [True, True], 'B': [True, True]}
7
...
8
...
9
 
10
def main(self):
11
...
12
if target and target.color == self.playersturn:
13
...
14
# キングが動いた
15
# 白
16
if target.name == 'WK':
17
self.can_castling['W'] = [False, False]
18
# 黒
19
if target.name == 'BK':
20
self.can_castling['B'] = [False, False]
21
# ルークが動いた
22
# 白
23
if target.name == 'WR':
24
# クイーンサイド
25
if startpos[0] == 0:
26
self.can_castling['W'][0] = False
27
# キングサイド
28
if startpos[0] == 7:
29
self.can_castling['W'][1] = False
30
# 黒
31
if target.name == 'BR':
32
# クイーンサイド
33
if startpos[0] == 0:
34
self.can_castling['B'][0] = False
35
# キングサイド
36
if startpos[0] == 7:
37
self.can_castling['B'][1] = False
38
...

この変数を使うことで、特定の方向へのキャスリングが可能かどうかが判定できますね。

次は、キャスリングの条件を定義したを作ります。

main.py
1
class Game:
2
...
3
def castling_requirements(self, piece, endpos, side, gameboard):
4
'''
5
キャスリングの条件を満たすとき,True
6
side == 0 -> aファイル側
7
side == 1 -> hファイル側
8
9
Parameters
10
----------
11
piece : obj
12
駒.キングでなければ return は False.
13
endpos : tuple > (int, int)
14
終了位置.絶対座標.
15
side : int > 0, 1
16
0 -- クイーンサイド
17
1 -- キングサイド
18
gameboard : dict > {(int, int): obj, ...}
19
盤面.
20
21
Returns
22
-------
23
bool
24
'''
25
common_req = (self.can_castling[piece.color][side] # キャスリングに関与する駒が一度も動いていない
26
and not self.is_check(piece.color, gameboard)) # キングがチェックされていない
27
# 白のキャスリング
28
if piece.color == 'W':
29
piece_req = (piece.name == 'WK'
30
and (7*side, 0) in gameboard
31
and gameboard[(7*side, 0)].name == 'WR')
32
# クイーンサイド
33
if side == 0:
34
special_req = (endpos == (2, 0)
35
# キングとルークの間に駒がない
36
and (1, 0) not in self.gameboard
37
and (2, 0) not in self.gameboard
38
and (3, 0) not in self.gameboard
39
# and キングが通過するマスが敵に攻撃されていない
40
)
41
# キングサイド
42
if side == 1:
43
special_req = (endpos == (6, 0)
44
# キングとルークの通過するマスに駒がない
45
and (6, 0) not in self.gameboard
46
and (5, 0) not in self.gameboard
47
# and キングが通過するマスが敵に攻撃されていない
48
)
49
# 黒のキャスリング
50
if piece.color == 'B':
51
piece_req = (piece.name == 'BK'
52
and (7*side, 7) in gameboard
53
and gameboard[(7*side, 7)].name == 'BR')
54
# クイーンサイド
55
if side == 0:
56
special_req = (endpos == (2, 7)
57
# キングとルークの通過するマスに駒がない
58
and (1, 7) not in self.gameboard
59
and (2, 7) not in self.gameboard
60
and (3, 7) not in self.gameboard
61
# and キングが通過するマスが敵に攻撃されていない
62
)
63
# キングサイド
64
if side == 1:
65
special_req = (endpos == (6, 7)
66
# キングとルークの通過するマスに駒がない
67
and (6, 7) not in self.gameboard
68
and (5, 7) not in self.gameboard
69
# and キングが通過するマスが敵に攻撃されていない
70
)
71
 
72
return common_req and piece_req and special_req

さて、まだ「キングが通るマス・キングの移動先のマスが敵の駒に攻撃されていない」という条件を表現しなければなりません。

キングの通過するマスが敵に攻撃されていない

キャスリングで通るマスにキングを仮に動かして、その状態でチェック状態になるかどうか確認し、

どの場合でもチェックにならなければ True となるような関数を作り、これを利用します。

castling_requirements()メソッドの内部でしか使わないので、内部で関数を定義しています。

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
return gameboard_tmp
25
26
def path_is_not_attacked(startpos_y, king_route):
27
'''
28
キングが通るマスのどれかが相手の駒に攻撃されていれば False を返す
29
30
Parameters
31
----------
32
startpos_y : int
33
開始位置y座標.
34
king_route : list > [int, ...]
35
キングが通る位置x座標のリスト.
36
37
Returns
38
-------
39
bool
40
'''
41
for pos in king_route:
42
if self.is_check(piece.color, create_tmp_board(startpos_y, (pos, startpos_y))):
43
return False
44
return True
45
46
common_req = (self.can_castling[piece.color][side] # キャスリングに関与する駒が一度も動いていない
47
and not self.is_check(piece.color, gameboard)) # キングがチェックされていない
48
# 白のキャスリング
49
if piece.color == 'W':
50
piece_req = (piece.name == 'WK'
51
and (7*side, 0) in gameboard
52
and gameboard[(7*side, 0)].name == 'WR')
53
# クイーンサイド
54
if side == 0:
55
special_req = (endpos == (2, 0)
56
# キングとルークの間に駒がない
57
and (1, 0) not in self.gameboard
58
and (2, 0) not in self.gameboard
59
and (3, 0) not in self.gameboard
60
# キングが通過するマスが敵に攻撃されていない
61
and path_is_not_attacked(0, [2, 3])
62
)
63
# キングサイド
64
if side == 1:
65
special_req = (endpos == (6, 0)
66
# キングとルークの通過するマスに駒がない
67
and (6, 0) not in self.gameboard
68
and (5, 0) not in self.gameboard
69
# キングが通過するマスが敵に攻撃されていない
70
and path_is_not_attacked(0, [6, 5])
71
)
72
# 黒のキャスリング
73
if piece.color == 'B':
74
piece_req = (piece.name == 'BK'
75
and (7*side, 7) in gameboard
76
and gameboard[(7*side, 7)].name == 'BR')
77
# クイーンサイド
78
if side == 0:
79
special_req = (endpos == (2, 7)
80
# キングとルークの通過するマスに駒がない
81
and (1, 7) not in self.gameboard
82
and (2, 7) not in self.gameboard
83
and (3, 7) not in self.gameboard
84
# キングが通過するマスが敵に攻撃されていない
85
and path_is_not_attacked(7, [2, 3])
86
)
87
# キングサイド
88
if side == 1:
89
special_req = (endpos == (6, 7)
90
# キングとルークの通過するマスに駒がない
91
and (6, 7) not in self.gameboard
92
and (5, 7) not in self.gameboard
93
# キングが通過するマスが敵に攻撃されていない
94
and path_is_not_attacked(7, [6, 5])
95
)
96
 
97
return common_req and piece_req and special_req

これで、castling_requirements()はキャスリングの条件を満たすときのみ True を返すメソッドとなりました。


キャスリング可能なとき、キングの動けるマスを追加

動ける位置を出力しているvalid_moves()メソッドを書き換えます。

main.py
1
def valid_moves(self, piece, startpos, gameboard):
2
...
3
# キャスリング
4
for endpos in [(2, 0), (6, 0), (2, 7), (6, 7)]:
5
if self.castling_requirements(piece, endpos, 0, gameboard):
6
result += [endpos]
7
if self.castling_requirements(piece, endpos, 1, gameboard):
8
result += [endpos]
9
...

これでキャスリングの条件を満たすときにのみ、キングの移動先にキャスリングの移動先が追加されました。

キャスリング キングだけ動く

この時点では、まだルークが動きませんので、ルークの動きを追加していきます。


ルークを動かす

キングは正常に動いているので、盤面を更新するときにルークの位置を適切に処理すれば大丈夫です。

盤面の更新の処理が定義されているrenew_gameboard()メソッドを書き換えます。

main.py
1
def renew_gameboard(self, startpos, endpos, gameboard):
2
...
3
# キャスリング
4
if (gameboard[endpos].abbr == 'K'
5
and abs(startpos[0] - endpos[0]) == 2):
6
# クイーンサイド
7
# 白
8
if endpos == (2, 0):
9
del gameboard[(0, 0)]
10
gameboard[(3, 0)] = Rook('W', 'WR')
11
# 黒
12
if endpos == (2, 7):
13
del gameboard[(0, 7)]
14
gameboard[(3, 7)] = Rook('B', 'BR')
15
# キングサイド
16
# 白
17
if endpos == (6, 0):
18
del gameboard[(7, 0)]
19
gameboard[(5, 0)] = Rook('W', 'WR')
20
# 黒
21
if endpos == (6, 7):
22
del gameboard[(7, 7)]
23
gameboard[(5, 7)] = Rook('B', 'BR')

キャスリングが起こるのはキングが2歩動いたときのみであることを利用しています。

位置によってキャスリングが起こる箇所を判定し、ルークを消して別の位置に新たなルークを置いています。

これでキャスリングの実装が完成しました!

今回の変更も GitHub に上げておきます。


これでようやく、普通のチェスができるようになりました!

いったんきりがついたので、はちょっとプログラムを整形したいと思います。

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

ではまた👋