Stage 4 駒の動きを判定する
4-2 ポーンの動きを判定する
ポーンは実に多彩な動きをみせてくれます。しかも白黒あわせて 16 個もありますからね、戦いにおいてもかなり重要な駒になるわけですよ。ええ、ですから、基本の動きに加えて例外などなどルールの量がめちゃめちゃ多い。まったく
無駄なもんばっか作りおって。
ということで、ルール自体は他の方のサイトに任せるとして、我々はさっさとコーディング進めてしまいましょう。
~~~~~~~~~~~~~~~
まず基本となる動きです。ポーンは様々ルールはあれど、やはりいちばんよく使うのは「一歩前進」ですね。この動きができる条件は、
- 横に動かない
- 盤面の外に出ない
- 前のマスに駒が置かれていない
でした。
そして、いちばん相手側、盤面の端まで来たら必ずクイーン・ルーク・ナイト・ビショップのいずれかにプロモーションしなければいけません。
キングとポーン以外の駒なら何にでもプロモーションできます。
プロモーションも一緒に処理すんのか
ではコードを見ていきましょう。
# PAWN
elif piece == PAWN:
# not promoting at the edge
if (toRANK == 8 - 1 or toRANK == 1 - 1) and promote not in [R, N, B, Q]:
logger.info('NECESSARY TO PROMOTE')
return False
# normal motion (one step forward); the same FILE, appropriate RANK, TO = EMPTY
# NOTE: if player is WHITE (=1), the rank number has to increase. vice versa
if frFILE == toFILE and toRANK - frRANK == player and self.board[toFILE][toRANK] == EMPTY:
return True
# normal capturing; next FILE, appropriate RANK, TO = opponent
if abs(toFILE - frFILE) == 1 and toRANK - frRANK == player and fundam.PosNeg(self.board[toFILE][toRANK]) == -player:
return True
# first two steps; adequate frRANK the same FILE, appropriate RANK, passing squares are EMPTY
if ((player == WHITE and frRANK == 2 - 1) or (player == BLACK and frRANK == 7 - 1)) and frFILE == toFILE and toRANK - frRANK == 2 * player and self.board[frFILE][frRANK + player] == self.board[toFILE][toRANK] == EMPTY:
return True
# en passant; FR - ep_target, TO - ep_target, TO = EMPTY
if abs(self.ep_target[FILE] - frFILE) == 1 and frRANK == self.ep_target[RANK] and toFILE == self.ep_target[FILE] and toRANK - self.ep_target[RANK] == player and self.board[toFILE][toRANK] == EMPTY:
return True
# all other pawn moves are invalid
logger.debug('INVALID MOTION of PAWN')
return False
先頭 elif となっているのは piece == EMPTY つまり移動元に駒がない場合に続けているからですね。
最上部の if 文は「端についたのにプロモーションしてないロクデナシをしょっぴく」という意味です。
# PAWN
elif piece == PAWN:
# not promoting at the edge
if (toRANK == 8 - 1 or toRANK == 1 - 1) and promote not in [R, N, B, Q]:
logger.info('NECESSARY TO PROMOTE')
return False
プレーヤーの色で場合分けする必要はありません。白は第 1 rank に、黒は第 8 rank にポーンを置くことがありませんから。
次が基本動作のコーディング、ここで True を返す条件は、
です。
# normal motion (one step forward); the same FILE, appropriate RANK, TO = EMPTY
# NOTE: if player is WHITE (=1), the rank number has to increase. vice versa
if frFILE == toFILE and toRANK - frRANK == player and self.board[toFILE][toRANK] == EMPTY:
return True
なんで rank の差をとって player と比較してんだ?
実はこれ、ちょっとチートっぽい計算なんですよ。白の場合で考えてみましょう。白がポーンを進めるとき、rank の番号が 1 つ増えますから、toRANK - frRANK = 1 となります。一方で、2-2 で WHITE = 1 と定義していますよね。だから
if frFILE == toFILE and toRANK - frRANK == player ...
return True
として rank の移動が適切か確かめています。黒の場合は rank の番号が 1 つ減りますし BLACK = -1 と定義しているので、同じように確認が取れます。嘘だと思います?いいですよご覧なさいよ。
~~~~~~~~~~~~~~~
次の elif 文は「相手の駒をとる通常の場合」について確かめています。True を返す条件は、
となりますね。
通常っていっときながらナナメに動くの腹たつな
ひとつ隣とは file の差が 1 か -1 かであることを意味しますから、絶対値 abs を取れば両方 1 として処理できます。したがってコードはこう。
# normal capturing; next FILE, appropriate RANK, TO = opponent
if abs(toFILE - frFILE) == 1 and toRANK - frRANK == player and fundam.PosNeg(self.board[toFILE][toRANK]) == -player:
return True
次いで最初の 2 マス移動です。初めて動かすポーンにかぎり発動するこの条件では、白なら第 2 rank, 黒なら第 7 rank にあるポーンを動かした時のみ player * 2 のぶん rank が変化します。
# first two steps; adequate frRANK the same FILE, appropriate RANK, passing squares are EMPTY
if ((player == WHITE and frRANK == 2 - 1) or (player == BLACK and frRANK == 7 - 1)) and frFILE == toFILE and toRANK - frRANK == 2 * player and self.board[frFILE][frRANK + player] == self.board[toFILE][toRANK] == EMPTY:
return True
ここではプレーヤーの色で場合分けが必要になりますよ。端からひとつ内側の rank はどちらのポーンもいる可能性があるので。
それともうひとつ、移動先だけでなく通過するマスにもほかの駒があってはいけないことも忘れてはいけません。
# first two steps; adequate frRANK the same FILE, appropriate RANK, passing squares are EMPTY
if ((player == WHITE and frRANK == 2 - 1) or (player == BLACK and frRANK == 7 - 1)) and frFILE == toFILE and toRANK - frRANK == 2 * player and self.board[frFILE][frRANK + player] == self.board[toFILE][toRANK] == EMPTY:
return True
~~~~~~~~~~~~~~~
ポーンの最後には厄介なアンパッサンが残っています。なんでこんなルール作りおったんでしょうね。まったく
余計なことしてくれやがって。
アンパッサンの条件をもう一度確認しましょう。
- 相手がポーンを 2 マス動かした直後に
- 相手から見てそのポーンの手前のマスに
- どの駒も置かれていないとき
- そのマスに
- そのポーンのすぐ隣にある自分のポーンを動かして
- 相手のポーンをとることができる
これ全部コーディングすんのかよ
さて、「相手が 2 マス前にポーンを動かした直後、そのポーンの...」ってどうやって検証します?しかも相手がポーンを動かした後、2 マス動かしていないポーンではアンパッサンできないので、どれが「その」ポーンか記録しておかなければいけません。
国語の問題かよ
そこで一度この Board クラスのコンストラクタに戻りましょう。board を定義した直後に ep_target というリストを宣言しています。これに
2 マス進んだ直後のポーンの座標
を格納します。例えば白が c2 から c4 にポーンを動かしたら、ep_target = [c - 1, 4 - 1]
とします。
前のターンでポーンを 2 マス進めなかったらどうすんの
いいところに気がつきました。ここで使うのが OVERSIZE です。2-2 で出てきましたよ。
明らかにインデックスに使えない数を使うことで
盤上に 2 マス進んだポーンはいねえよ
ということを表してあげます。実際、ep_target を宣言しているところではコンストラクタのデフォルト引数で [OVERSIZE, OVERSIZE] としていますよね。
2 マス進んだポーンが盤上からはみ出る=盤上にない
例えば黒が f7 から f5 にポーンを動かしたら、
ep_target = [f - 1, 5 - 1]
ですが、この後白がポーンを 2 マス進めなかったら
ep_target = [OVERSIZE, OVERSIZE]
となるわけです。
~~~~~~~~~~~~~~~
アンパッサンの話に戻りましょう。先ほどお見せした図をもう一度ご覧ください。
この図だと ep_target に置いてあるのが白のポーン、その隣に黒のポーンがある構図になります。これをもとにアンパッサンができる条件を並べてみると
- ep_target のとなりに移動元
- ep_target の奥(rank が frRANK + player)に移動先
- 移動先が EMPTY
となりますよね。コードと一緒に確認しましょう。
# en passant; FR - ep_target, TO - ep_target, TO = EMPTY
if abs(self.ep_target[FILE] - frFILE) == 1 and frRANK == self.ep_target[RANK] and toFILE == self.ep_target[FILE] and toRANK - self.ep_target[RANK] == player and self.board[toFILE][toRANK] == EMPTY:
return True
「隣に移動元」であれば file が一つぶんだけ違くて rank は同じですから太字の通り。
# en passant; FR - ep_target, TO - ep_target, TO = EMPTY
if abs(self.ep_target[FILE] - frFILE) == 1 and frRANK == self.ep_target[RANK] and toFILE == self.ep_target[FILE] and toRANK - self.ep_target[RANK] == player and self.board[toFILE][toRANK] == EMPTY:
return True
移動先は ep_target と file が同じ。
# en passant; FR - ep_target, TO - ep_target, TO = EMPTY
if abs(self.ep_target[FILE] - frFILE) == 1 and frRANK == self.ep_target[RANK] and toFILE == self.ep_target[FILE] and toRANK - self.ep_target[RANK] == player and self.board[toFILE][toRANK] == EMPTY:
return True
rank は一つ前に進んでいますから player が加わっていますね。
# en passant; FR - ep_target, TO - ep_target, TO = EMPTY
if abs(self.ep_target[FILE] - frFILE) == 1 and frRANK == self.ep_target[RANK] and toFILE == self.ep_target[FILE] and toRANK - self.ep_target[RANK] == player and self.board[toFILE][toRANK] == EMPTY:
return True
そして「移動先が EMPTY」の判定もお忘れなく。
# en passant; FR - ep_target, TO - ep_target, TO = EMPTY
if abs(self.ep_target[FILE] - frFILE) == 1 and frRANK == self.ep_target[RANK] and toFILE == self.ep_target[FILE] and toRANK - self.ep_target[RANK] == player and self.board[toFILE][toRANK] == EMPTY:
return True
さて、ここまででようやくポーンの判定が終わりました。もうポーンに許された動きは残っていませんから、ここまでリターンされなかったものを全て False「動かせない」でリターンしてください。
これでやっと駒1つかよ
ここまで面倒なのはポーンくらいです。ルーク以降はかなり簡単にカタが着きますから、ここまでコーディングできれば軽快なステップで進めますよ。