Pythonプログラミングで

チェスを作る

Stage 4 駒の動きを判定する

4-2 ポーンの動きを判定する

ポーンは実に多彩な動きをみせてくれます。しかも白黒あわせて 16 個もありますからね、戦いにおいてもかなり重要な駒になるわけですよ。ええ、ですから、基本の動きに加えて例外などなどルールの量がめちゃめちゃ多い。まったく

無駄なもんばっか作りおって。

ということで、ルール自体は他の方のサイトに任せるとして、我々はさっさとコーディング進めてしまいましょう。

~~~~~~~~~~~~~~~

まず基本となる動きです。ポーンは様々ルールはあれど、やはりいちばんよく使うのは「一歩前進」ですね。この動きができる条件は、

  • 横に動かない
  • 盤面の外に出ない
  • 前のマスに駒が置かれていない

でした。

UNAVAILABLE

そして、いちばん相手側、盤面の端まで来たら必ずクイーン・ルーク・ナイト・ビショップのいずれかにプロモーションしなければいけません。

UNAVAILABLE

キングとポーン以外の駒なら何にでもプロモーションできます。

UNAVAILABLE

プロモーションも一緒に処理すんのか

ではコードを見ていきましょう。

                        
        # 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 を返す条件は、

  • file が同じ
  • rank の移動が適切
  • 移動先に何もない

です。

                        
            # 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
                        
                    
UNAVAILABLE

なんで 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 と定義しているので、同じように確認が取れます。嘘だと思います?いいですよご覧なさいよ。

UNAVAILABLE

~~~~~~~~~~~~~~~

次の elif 文は「相手の駒をとる通常の場合」について確かめています。True を返す条件は、

  • ひとつ隣の file
  • (各方からみて)ひとつ前の rank

となりますね。

UNAVAILABLE
UNAVAILABLE

通常っていっときながらナナメに動くの腹たつな

ひとつ隣とは 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 が変化します。

UNAVAILABLE
                        
            # 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 マス動かした直後に
  • 相手から見てそのポーンの手前のマスに
  • どの駒も置かれていないとき
  • そのマスに
  • そのポーンのすぐ隣にある自分のポーンを動かして
  • 相手のポーンをとることができる

UNAVAILABLE
UNAVAILABLE

これ全部コーディングすんのかよ

さて、「相手が 2 マス前にポーンを動かした直後、そのポーンの...」ってどうやって検証します?しかも相手がポーンを動かした後、2 マス動かしていないポーンではアンパッサンできないので、どれが「その」ポーンか記録しておかなければいけません。

UNAVAILABLE

国語の問題かよ

そこで一度この Board クラスコンストラクタに戻りましょう。board を定義した直後に ep_target というリストを宣言しています。これに

2 マス進んだ直後のポーンの座標

を格納します。例えば白が c2 から c4 にポーンを動かしたら、ep_target = [c - 1, 4 - 1] とします。

UNAVAILABLE

前のターンでポーンを 2 マス進めなかったらどうすんの

いいところに気がつきました。ここで使うのが OVERSIZE です。2-2 で出てきましたよ。

明らかにインデックスに使えない数を使うことで

UNAVAILABLE

盤上に 2 マス進んだポーンはいねえよ

ということを表してあげます。実際、ep_target を宣言しているところではコンストラクタデフォルト引数で [OVERSIZE, OVERSIZE] としていますよね。

UNAVAILABLE

2 マス進んだポーンが盤上からはみ出る=盤上にない

例えば黒が f7 から f5 にポーンを動かしたら、

ep_target = [f - 1, 5 - 1]

ですが、この後白がポーンを 2 マス進めなかったら

ep_target = [OVERSIZE, OVERSIZE]

となるわけです。

~~~~~~~~~~~~~~~

アンパッサンの話に戻りましょう。先ほどお見せした図をもう一度ご覧ください。

UNAVAILABLE

この図だと 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「動かせない」でリターンしてください。

UNAVAILABLE

これでやっと駒1つかよ

ここまで面倒なのはポーンくらいです。ルーク以降はかなり簡単にカタが着きますから、ここまでコーディングできれば軽快なステップで進めますよ。

NEXT 4-3 ルーク・ナイト・ビショップ・クイーンの動きを判定する