Reputation: 744
I'm currently working on a chess engine, which is working so far but is taking forever to generate moves. The check detection takes by far the longest since many moves have to be generated.
I got stuck after trying many things and can't really figure out how to make it more efficient. Here is how I do it:
To check if a move is allowed/legal, I check, if the side making the move is in check afterwards:
def allowed_move(self, board, pos, move, colour):
copy_board = copy.deepcopy(board)
(x, y) = pos
(new_x, new_y) = move
if copy_board[x][y] == "k_w":
self.white_king = (new_x, new_y)
copy_board[new_x][new_y] = copy_board[x][y]
copy_board[x][y] = " "
self.in_check(colour, copy_board)
self.white_king = (x, y)
elif copy_board[x][y] == "k_b":
self.black_king = (new_x, new_y)
copy_board[new_x][new_y] = copy_board[x][y]
copy_board[x][y] = " "
self.in_check(colour, copy_board)
self.black_king = (x, y)
else:
copy_board[new_x][new_y] = copy_board[x][y]
copy_board[x][y] = " "
self.in_check(colour, copy_board)
if colour == "_w" and self.white_check:
return False
if colour == "_b" and self.black_check:
return False
return True
To determine if a side is in check, i generate every move of the opponents side and check if they would be able to capture the king on their next move. (This is the bad part)
sq[1:] just determines the colour of the current square
self.(colour).king is the current position of the king
move is a tuple containing the coordinates of the end square of each move
def in_check(self, colour, board):
self.white_check = False
self.black_check = False
for x, line in enumerate(board):
for y, sq in enumerate(line):
if sq[1:] == "_w":
for (move, _, _) in self.get_all_moves(board, (x, y)):
if move == self.black_king:
self.black_check = True
if sq[1:] == "_b":
for (move, _, _) in self.get_all_moves(board, (x, y)):
if move == self.white_king:
self.white_check = True
This is by far the most expensive operation in my engine and I'm wondering if I could simplify it somehow. The pseudo-legal-moves are calculated using this file, for anyone interested. To complete all moves, en-passant, casting, and promotion are generated separately. I also generate a list of possible, legal moves per side after each executed move, but since the check-detection uses a potential move, those lists cant be used.
class Rook:
def __init__(self, pos, brd):
self.board = brd
self.pos = pos
def moves(self):
pos_caps = []
(x, y) = self.pos
clr = self.board[x][y][1:]
for i in range(- 1, - y - 1, -1):
if self.board[x][y + i][1:] != clr:
pos_caps.append((x, y + i))
for v in range(- 1, i, - 1):
if self.board[x][y + v] != " ":
pos_caps.remove((x, y + i))
break
for i in range(1, 8 - y):
if self.board[x][y + i][1:] != clr:
pos_caps.append((x, y + i))
for v in range(1, i):
if self.board[x][y + v] != " ":
pos_caps.remove((x, y + i))
break
for i in range(- 1, - x - 1, -1):
if self.board[x + i][y][1:] != clr:
pos_caps.append((x + i, y))
for v in range(- 1, i, - 1):
if self.board[x + v][y] != " ":
pos_caps.remove((x + i, y))
break
for i in range(1, 8 - x):
if self.board[x + i][y][1:] != clr:
pos_caps.append((x + i, y))
for v in range(1, i):
if self.board[x + v][y] != " ":
pos_caps.remove((x + i, y))
break
return pos_caps
class Knight:
def __init__(self, pos, brd):
self.board = brd
self.pos = pos
(self.x_pos, self.y_pos) = self.pos
self.clr = self.board[self.x_pos][self.y_pos][1:]
def moves(self):
pos_moves = []
(x, y) = self.pos
if x < 6 and y < 7:
pos_moves.append((x + 2, y + 1))
if x < 6 and y > 0:
pos_moves.append((x + 2, y - 1))
if x > 1 and y < 7:
pos_moves.append((x - 2, y + 1))
if x > 1 and y > 0:
pos_moves.append((x - 2, y - 1))
if x < 7 and y < 6:
pos_moves.append((x + 1, y + 2))
if x < 7 and y > 1:
pos_moves.append((x + 1, y - 2))
if x > 0 and y < 6:
pos_moves.append((x - 1, y + 2))
if x > 0 and y > 1:
pos_moves.append((x - 1, y - 2))
for mv in reversed(pos_moves):
(x, y) = mv
if self.board[x][y][1:] == self.clr:
pos_moves.remove(mv)
return pos_moves
class King:
def __init__(self, pos, brd):
self.board = brd
self.pos = pos
(self.x_pos, self.y_pos) = self.pos
self.clr = self.board[self.x_pos][self.y_pos][1:]
def moves(self):
all_moves = []
(x, y) = self.pos
if x > 0:
all_moves.append((x - 1, y))
if y > 0:
all_moves.append((x - 1, y - 1))
if y < 7:
all_moves.append((x - 1, y + 1))
if x < 7:
all_moves.append((x + 1, y))
if y > 0:
all_moves.append((x + 1, y - 1))
if y < 7:
all_moves.append((x + 1, y + 1))
if y > 0:
all_moves.append((x, y - 1))
if y < 7:
all_moves.append((x, y + 1))
for mv in reversed(all_moves):
(x, y) = mv
if self.board[x][y][1:] == self.clr:
all_moves.remove(mv)
return all_moves
class Queen:
def __init__(self, pos, brd):
self.board = brd
self.pos = pos
(self.x_pos, self.y_pos) = self.pos
self.clr = self.board[self.x_pos][self.y_pos][1:]
def moves(self):
pos_caps = []
(x, y) = self.pos
clr = self.board[x][y][1:]
for i in range(- 1, - y - 1, -1):
if self.board[x][y + i][1:] != clr:
pos_caps.append((x, y + i))
for v in range(- 1, i, - 1):
if self.board[x][y + v] != " ":
pos_caps.remove((x, y + i))
break
for i in range(1, 8 - y):
if self.board[x][y + i][1:] != clr:
pos_caps.append((x, y + i))
for v in range(1, i):
if self.board[x][y + v] != " ":
pos_caps.remove((x, y + i))
break
for i in range(- 1, - x - 1, -1):
if self.board[x + i][y][1:] != clr:
pos_caps.append((x + i, y))
for v in range(- 1, i, - 1):
if self.board[x + v][y] != " ":
pos_caps.remove((x + i, y))
break
for i in range(1, 8 - x):
if self.board[x + i][y][1:] != clr:
pos_caps.append((x + i, y))
for v in range(1, i):
if self.board[x + v][y] != " ":
pos_caps.remove((x + i, y))
break
com = min(x, y)
for i in range(1, com + 1):
if self.board[x - i][y - i][1:] != clr:
pos_caps.append((x - i, y - i))
for v in range(1, i):
if self.board[x - v][y - v] != " ":
pos_caps.remove((x - i, y - i))
break
com = min(7 - x, 7 - y)
for i in range(1, com + 1):
if self.board[x + i][y + i][1:] != clr:
pos_caps.append((x + i, y + i))
for v in range(1, i):
if self.board[x + v][y + v] != " ":
pos_caps.remove((x + i, y + i))
break
com = min(7 - x, y)
for i in range(1, com + 1):
if self.board[x + i][y - i][1:] != clr:
# print(str((x + i, y - i)) + "Appending")
pos_caps.append((x + i, y - i))
for v in range(1, i):
if self.board[x + v][y - v] != " ":
# print(str((x + i, y - i)) + "Removing")
pos_caps.remove((x + i, y - i))
break
com = min(x, 7 - y)
for i in range(1, com + 1):
if self.board[x - i][y + i][1:] != clr:
pos_caps.append((x - i, y + i))
for v in range(1, i):
if self.board[x - v][y + v] != " ":
pos_caps.remove((x - i, y + i))
break
return pos_caps
class Pawn_white:
def __init__(self, pos, brd):
self.board = brd
self.pos = pos
(self.x_pos, self.y_pos) = self.pos
self.clr = "_w"
def moves(self):
all_moves = []
(x, y) = self.pos
if y > 0:
if y == 6:
all_moves.append((x, y - 1))
all_moves.append((x, y - 2))
else:
all_moves.append((x, y - 1))
if x > 0:
if self.board[x - 1][y - 1][1:] != self.clr:
all_moves.append((x - 1, y - 1))
if x < 7:
if self.board[x + 1][y - 1][1:] != self.clr:
all_moves.append((x + 1, y - 1))
for mv in reversed(all_moves):
(x, y) = mv
if self.board[x][y][1:] == self.clr:
all_moves.remove(mv)
return all_moves
class Pawn_black:
def __init__(self, pos, brd):
self.board = brd
self.pos = pos
(self.x_pos, self.y_pos) = self.pos
self.clr = "_b"
def moves(self):
all_moves = []
(x, y) = self.pos
if y == 1:
all_moves.append((x, y + 1))
all_moves.append((x, y + 2))
elif y < 7:
all_moves.append((x, y + 1))
if x > 0:
if self.board[x - 1][y + 1] != self.clr:
all_moves.append((x - 1, y + 1))
if x < 7:
if self.board[x + 1][y + 1] != self.clr:
all_moves.append((x + 1, y + 1))
for mv in reversed(all_moves):
(x, y) = mv
if self.board[x][y][1:] == self.clr:
all_moves.remove(mv)
return all_moves
class Bishop:
def __init__(self, pos, brd):
self.board = brd
self.pos = pos
def moves(self):
pos_caps = []
(x, y) = self.pos
clr = self.board[x][y][1:]
com = min(x, y)
for i in range(1, com + 1):
if self.board[x - i][y - i][1:] != clr:
pos_caps.append((x - i, y - i))
for v in range(1, i):
if self.board[x - v][y - v] != " ":
pos_caps.remove((x - i, y - i))
break
com = min(7 - x, 7 - y)
for i in range(1, com + 1):
if self.board[x + i][y + i][1:] != clr:
pos_caps.append((x + i, y + i))
for v in range(1, i):
if self.board[x + v][y + v] != " ":
pos_caps.remove((x + i, y + i))
break
com = min(7 - x, y)
for i in range(1, com + 1):
if self.board[x + i][y - i][1:] != clr:
pos_caps.append((x + i, y - i))
for v in range(1, i):
if self.board[x + v][y - v] != " ":
pos_caps.remove((x + i, y - i))
break
com = min(x, 7 - y)
for i in range(1, com + 1):
if self.board[x - i][y + i][1:] != clr:
pos_caps.append((x - i, y + i))
for v in range(1, i):
if self.board[x - v][y + v] != " ":
pos_caps.remove((x - i, y + i))
break
return pos_caps
I hope I made everything clear about my problem/code. Any help is appreciated.
Upvotes: 11
Views: 3319
Reputation: 42143
Here is the consolidated (and partially validated) code from my first answer. I inverted (r,c) to (c,r) everywhere.
SETUP
from collections import defaultdict
targets = dict()
positions = [ (c,r) for c in range(8) for r in range(8) ]
def valid(P):
return [(c,r) for c,r in P if c in range(8) and r in range(8)]
targets["up"] = { (c,r):valid( (c,r+v) for v in range(1,8))
for c,r in positions }
targets["down"] = { (c,r):valid( (c,r-v) for v in range(1,8))
for c,r in positions }
targets["left"] = { (c,r):valid( (c-h,r) for h in range(1,8))
for c,r in positions }
targets["right"] = { (c,r):valid( (c+h,r) for h in range(1,8))
for c,r in positions }
targets["upleft"] = { (c,r):[(cl,ru) for (_,ru),(cl,_) in zip(targets["up"][c,r],targets["left"][c,r])]
for c,r in positions }
targets["upright"] = { (c,r):[(cr,ru) for (_,ru),(cr,_) in zip(targets["up"][c,r],targets["right"][c,r])]
for c,r in positions }
targets["downleft"] = { (c,r):[(cl,rd) for (_,rd),(cl,_) in zip(targets["down"][c,r],targets["left"][c,r])]
for c,r in positions }
targets["downright"] = { (c,r):[(cr,rd) for (_,rd),(cr,_) in zip(targets["down"][c,r],targets["right"][c,r])]
for c,r in positions }
targets["vhPaths"] = ["up","down","left","right"]
targets["diagPaths"] = ["upleft","upright","downleft","downright"]
targets["allPaths"] = targets["vhPaths"]+targets["diagPaths"]
targets["rook"] = { (c,r):[p for path in targets["vhPaths"] for p in targets[path][c,r]]
for c,r in positions }
targets["bishop"] = { (c,r):[p for path in targets["diagPaths"] for p in targets[path][c,r]]
for c,r in positions }
targets["queen"] = { (c,r):[p for path in targets["allPaths"] for p in targets[path][c,r]]
for c,r in positions }
targets["king"] = { (c,r):[p for path in targets["allPaths"] for p in targets[path][c,r][:1]]
for c,r in positions }
targets["knight"] = { (c,r):valid((c+h,r+v) for v,h in [(2,1),(2,-1),(1,2),(1,-2),(-2,1),(-2,-1),(-1,2),(-1,-2)])
for c,r in positions }
targets["wpawn"] = { (c,r):valid([(c,r+1)]*(r>0) + [(c,r+2)]*(r==1))
for c,r in positions }
targets["bpawn"] = { (c,r):valid([(c,r-1)]*(r<7) + [(c,r-2)]*(r==6))
for c,r in positions }
targets["wptake"] = { (c,r):valid([(c+1,r+1),(c-1,r+1)]*(r>0))
for c,r in positions }
targets["bptake"] = { (c,r):valid([(c+1,r-1),(c-1,r-1)]*(r<7))
for c,r in positions }
targets["wcastle"] = defaultdict(list,{ (4,0):[(2,0),(6,0)] })
targets["bcastle"] = defaultdict(list,{ (4,7):[(2,7),(6,7)] })
targets["breakCastle"] = defaultdict(list,{ (4,7):[(2,7),(6,7)],
(7,7):[(6,7)], (0,7):[(2,7)],
(4,0):[(2,0),(6,0)],
(7,0):[(6,0)], (1,0):[(2,0)]})
targets["rook"]["paths"] = targets["vhPaths"]
targets["bishop"]["paths"] = targets["diagPaths"]
targets["queen"]["paths"] = targets["allPaths"]
targets["q_w"] = targets["q_b"] = targets["queen"]
targets["k_w"] = targets["k_b"] = targets["king"]
targets["r_w"] = targets["r_b"] = targets["rook"]
targets["b_w"] = targets["b_b"] = targets["bishop"]
targets["n_w"] = targets["n_b"] = targets["knight"]
targets["p_w"],targets["p_w!"] = targets["wpawn"],targets["wptake"]
targets["p_b"],targets["p_b!"] = targets["bpawn"],targets["bptake"]
for r,c in positions:
targets[(r,c)] = defaultdict(list)
for direction in targets["allPaths"]:
path = targets[direction][r,c]
for i,(tr,tc) in enumerate(path):
targets[(r,c)][tr,tc]=path[:i]
Check! Detection
def lineOfSight(board,A,B,ignore=None):
return all(board[c][r]=="" or (c,r)==ignore for c,r in targets[A][B])
def getKingPos(board,player):
king = "k_"+player
return next((c,r) for c,r in positions if board[c][r]==king)
# also used to avoid self check! in king move generation
def isCheck(board,player,kingPos=None,ignore=None):
paths = ("q_b","r_b","b_b","n_b",f"p_{player}!")
if kingPos is None: kingPos = getKingPos(board,player)
return any( board[c][r][:1]==path[:1]
and board[c][r][-1:] != player
and lineOfSight(board,kingPos,(c,r),ignore)
for path in paths
for c,r in targets[path][kingPos] )
Move Generation
helper functions...
# { pinnedPosition : pinnedByPosition }
def getPinned(board,player):
opponent = "b" if player=="w" else "w"
kingPos = getKingPos(board,player)
pinned = dict()
for piece in ("q_"+opponent, "r_"+opponent, "b_"+opponent):
for tc,tr in targets[piece][kingPos]:
if board[tc][tr] != piece: continue
span = [board[sc][sr][-1:] for sc,sr in targets[tc,tr][kingPos]]
if span.count(player)==1 and opponent not in span:
pinnedPos = targets[tc,tr][kingPos][span.index(player)]
pinned[pinnedPos] = (tc,tr)
return pinned
def linearMoves(board,position,player,piece):
for path in targets[piece]["paths"]:
for c,r in targets[path][position]:
if board[c][r][-1:] != player : yield (position,(c,r))
if board[c][r] != "" : break
def directMoves(board,position,player,piece,condition=lambda *p:True):
for c,r in targets[piece][position]:
if board[c][r][-1:] == player: continue
if condition(c,r): yield (position,(c,r))
def switch(v): yield lambda *c: v in c
actual move generation...
def getMoves(board,player):
enPassant,brokenCastles = board[8:] or (None,set())
moves = []
for c,r in positions:
if board[c][r][-1:] != player: continue
piece = board[c][r]
for case in switch(piece[0]):
if case("b","r","q"):
moves += linearMoves(board,(c,r),player,piece)
elif case("n"):
moves += directMoves(board,(c,r),player,piece)
elif case("p"):
moves += directMoves(board,(c,r),player,piece,
lambda tc,tr:board[tc][tr]==""
and lineOfSight(board,(c,r),(tc,tr)))
moves += directMoves(board,(c,r),player,piece+"!",
lambda tc,tr:board[tc][tr] != "" or (tc,tr) == enPassant )
elif case("k"):
moves += directMoves(board,(c,r),player,piece,
lambda tc,tr: not isCheck(board,player,(tc,tr),(c,r)))
if isCheck(board,player): continue
moves += directMoves(board,(c,r),player,player+"castle",
lambda tc,tr: board[tc][tr] == ""
and not (tc,tr) in brokenCastles
and lineOfSight(board,(c,r),(tc,tr))
and not isCheck(board,player,(tc,tr),(c,r))
and not isCheck(board,player,targets[c,r][tc,tr][0],(c,r)))
pinned = getPinned(board,player)
if pinned: # Pinned pieces can only move on the threat line
kingPos = getKingPos(board,player)
moves = [ (p,t) for p,t in moves if p not in pinned
or t == pinned[p] or t in targets[kingPos][pinned[p]] ]
return moves
To complete the move generation conditions, some states must be set by previous moves:
enPassant
is the position skipped by the last two-square pawn move. It should be assigned when a pawn moves by two squares and set to None
on every other move.
enPassant = next(iter(targets[fromPosition][toPosition]*(piece=="p")),None)
brokenCastles
is a set of target king-castle positions for castles that have been invalidated by moving a king or a rook. if can be updated unconditionally after every move:
brokenCastles.update(targets["breakCastle"][fromPosition])
These states have to be kept somewhere in association with the current board. This may justify creating a class for boards rather than using a simple list of lists. The information could also be held in the 9th and subsequent items of the board list if you find that creating a class is overkill
Pretty print
def boardLines(board):
symbol = { "":".....","r":".[…].", "n":". />.", "b":". ∆ .",
"q":".{Ö}.", "k":". † .","p":". o .",
"_b":".(█).", "_w":".(_)."}
lines = []
lines += [" 0 1 2 3 4 5 6 7 "]
lines += [" ╔═════╤═════╤═════╤═════╤═════╤═════╤═════╤═════╗"]
def fill(c,r,p):
return symbol[board[c][r][p:1+2*p]].replace("."," ░"[(r&1)==(c&1)])
for r in reversed(range(8)):
lines += [" ╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢"]*(r<7)
lines += [" ║" + "│".join(fill(c,r,0) for c in range(8))+ "║"]
lines += [f"{r} ║"+ "│".join(fill(c,r,1) for c in range(8))+ f"║ {r}"]
lines += [" ╚═════╧═════╧═════╧═════╧═════╧═════╧═════╧═════╝"]
lines += [" 0 1 2 3 4 5 6 7 "]
return lines
def printBoard(board,indent=" "):
for line in boardLines(board):print(indent+line)
...
"""
0 1 2 3 4 5 6 7
╔═════╤═════╤═════╤═════╤═════╤═════╤═════╤═════╗
║ […] │░ />░│ ∆ │░{Ö}░│ † │░ ∆ ░│ /> │░[…]░║
7 ║ (█) │░(█)░│ (█) │░(█)░│ (█) │░(█)░│ (█) │░(█)░║ 7
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░ o ░│ o │░ o ░│ o │░ o ░│ o │░ o ░│ o ║
6 ║░(█)░│ (█) │░(█)░│ (█) │░(█)░│ (█) │░(█)░│ (█) ║ 6
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║
5 ║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║ 5
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║
4 ║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║ 4
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║
3 ║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║ 3
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║
2 ║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║ 2
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║ o │░ o ░│ o │░ o ░│ o │░ o ░│ o │░ o ░║
1 ║ (_) │░(_)░│ (_) │░(_)░│ (_) │░(_)░│ (_) │░(_)░║ 1
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░[…]░│ /> │░ ∆ ░│ {Ö} │░ † ░│ ∆ │░ />░│ […] ║
0 ║░(_)░│ (_) │░(_)░│ (_) │░(_)░│ (_) │░(_)░│ (_) ║ 0
╚═════╧═════╧═════╧═════╧═════╧═════╧═════╧═════╝
0 1 2 3 4 5 6 7
"""
Superficial tests:
board = [ ["q_b", "", "", "", "", "", "", "" ],
["", "", "", "", "", "", "", "" ],
["", "", "", "", "", "", "", "" ],
["", "", "", "", "", "", "", "" ],
["k_w", "", "", "", "", "", "", "k_b"],
["", "", "", "", "", "", "", "n_b"],
["", "", "", "", "", "", "", "" ],
["", "", "", "", "", "", "", "r_w"]]
...
printBoard(board)
"""
0 1 2 3 4 5 6 7
╔═════╤═════╤═════╤═════╤═════╤═════╤═════╤═════╗
║ │░░░░░│ │░░░░░│ † │░ />░│ │░[…]░║
7 ║ │░░░░░│ │░░░░░│ (█) │░(█)░│ │░(_)░║ 7
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║
6 ║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║ 6
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║
5 ║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║ 5
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║
4 ║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║ 4
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║
3 ║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║ 3
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║
2 ║░░░░░│ │░░░░░│ │░░░░░│ │░░░░░│ ║ 2
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║
1 ║ │░░░░░│ │░░░░░│ │░░░░░│ │░░░░░║ 1
╟─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────╢
║░{Ö}░│ │░░░░░│ │░ † ░│ │░░░░░│ ║
0 ║░(█)░│ │░░░░░│ │░(_)░│ │░░░░░│ ║ 0
╚═════╧═════╧═════╧═════╧═════╧═════╧═════╧═════╝
0 1 2 3 4 5 6 7
"""
... whites ...
for (c,r),(tc,tr) in getMoves(board,"w"):
print(board[c][r],(c,r),"-->",(tc,tr))
k_w (4, 0) --> (4, 1)
k_w (4, 0) --> (3, 1)
k_w (4, 0) --> (5, 1)
r_w (7, 7) --> (7, 6)
r_w (7, 7) --> (7, 5)
r_w (7, 7) --> (7, 4)
r_w (7, 7) --> (7, 3)
r_w (7, 7) --> (7, 2)
r_w (7, 7) --> (7, 1)
r_w (7, 7) --> (7, 0)
r_w (7, 7) --> (6, 7)
r_w (7, 7) --> (5, 7)
print(isCheck(board,"w")) # True
... blacks ...
for (c,r),(tc,tr) in getMoves(board,"b"):
print(board[c][r],(c,r),"-->",(tc,tr))
q_b (0, 0) --> (0, 1)
q_b (0, 0) --> (0, 2)
q_b (0, 0) --> (0, 3)
q_b (0, 0) --> (0, 4)
q_b (0, 0) --> (0, 5)
q_b (0, 0) --> (0, 6)
q_b (0, 0) --> (0, 7)
q_b (0, 0) --> (1, 0)
q_b (0, 0) --> (2, 0)
q_b (0, 0) --> (3, 0)
q_b (0, 0) --> (4, 0)
q_b (0, 0) --> (1, 1)
q_b (0, 0) --> (2, 2)
q_b (0, 0) --> (3, 3)
q_b (0, 0) --> (4, 4)
q_b (0, 0) --> (5, 5)
q_b (0, 0) --> (6, 6)
q_b (0, 0) --> (7, 7)
k_b (4, 7) --> (4, 6)
k_b (4, 7) --> (3, 7)
k_b (4, 7) --> (3, 6)
k_b (4, 7) --> (5, 6)
k_b (4, 7) --> (2, 7)
print(isCheck(board,"b")) # False
print(getPinned(board,"b")) # {(5, 7): (7, 7)}
[EDIT] bonus code ...
If you store legal moves and only want to recalculate them for positions affected by the last move ...
# Return positions of first piece in line of sight
# for a list of path names
def nextInLine(board,pathNames,position,ignore=None):
for path in pathNames:
pos = next(((c,r) for c,r in targets[path][position]
if board[c][r] and (c,r) != ignore),None)
if pos: yield pos
# Determine which positions may need move recalculation after making a move
# - moves associated with the fromPosition are assumed to be cleared
# - both kings should be re-evaluated after every move
# - this may include a few extra positions (speed/precision trade-off)
def moveRecalc(board,player,fromPosition,toPosition):
recalc = {toPosition, getKingPos(board,"w"), getKingPos(board,"b")}
for position in (fromPosition,toPosition,*filter(None,[enPassant])):
recalc.update(nextInLine(board,targets["allPaths"],position))
recalc.update((c,r) for c,r in targets["knight"][position]
if board[c][r][:1]=="n")
return recalc
A faster function to detect pinned positions (radiating from king's position):
# { pinnedPosition : pinnedByPosition }
def getPinned(board,player):
kingPos = getKingPos(board,player)
pinned = dict()
for path in targets["allPaths"]:
inLine = ((c,r) for c,r in targets[path][kingPos] if board[c][r])
pc,pr = next(inLine,(None,None)) # own piece
if pc is None or board[pc][pr][-1:] != player: continue
ac,ar = next(inLine,(None,None)) # opponent attacker
if ac is None or board[ac][ar][-1:] == player: continue
aPiece = board[ac][ar][:1]
if aPiece == "q" \
or aPiece == "r" and (ac == pc or ar == pr) \
or aPiece == "b" and (ac != pc and ar != pr):
pinned[pc,pr] = (ac,ar)
return pinned
Coordinates that threaten a player at a given position:
def getThreat(board,position,player="",ignore=None,pinned=None):
c,r = position
for ac,ar in nextInLine(board,targets["allPaths"],position,ignore=ignore):
piece = board[ac][ar]
if piece[-1:] == player: continue
for case in switch(board[ac][ar][:1]):
if case("n") : break
if case("r") and (ac-c)*(ar-r) : break
if case("b") and not (ac-c)*(ar-r): break
if case("p","k") and (c,r) not in targets[piece][ac,ar]: break
if pinned and (ac,ar) in pinned:
pc,pr = pinned[ac,ar]
if (ar-r)*(ac-pc) != (ac-c)*(ar-pr): break
yield ac,ar
for ac,ar in targets["knight"][position]:
if board[ac][ar][:1]=="n" and board[ac][ar][:1]!=player:
yield ac,ar
# print(any(getThreat(board,(5,7))),*getThreat(board,(5,7)))
# True (4, 7) (7, 7)
# print(any(getThreat(board,(2,1)))) # False
# print(any(getThreat(board,getKingPos(board,"w"),"w"))) # True
# could be used to implement isCheck (may be faster too):
def isCheck(board,player,kingPos=None,ignore=None):
if kingPos is None: kingPos = getKingPos(board,player)
return any(getThreat(board,kingPos,player,ignore))
Putting everything together
SETUP: (initial board positions)
initialBoard = [ ["r_w","p_w","","","","","p_b","r_b"],
["n_w","p_w","","","","","p_b","n_b"],
["b_w","p_w","","","","","p_b","b_b"],
["q_w","p_w","","","","","p_b","q_b"],
["k_w","p_w","","","","","p_b","k_b"],
["b_w","p_w","","","","","p_b","b_b"],
["n_w","p_w","","","","","p_b","n_b"],
["r_w","p_w","","","","","p_b","r_b"],
None,set()] # enPassant, brokenCastles
Making a move, with updates for special moves:
from copy import deepcopy
def playMove(board,fromPosition,toPosition,promotion=""):
(fromC,fromR),(toC,toR) = fromPosition,toPosition
piece,player = board[fromC][fromR].split("_")
board = [deepcopy(r) for r in board]
board[toC][toR],board[fromC][fromR] = board[fromC][fromR],""
# promotion
if piece == "p" and toR in (0,7):
while promotion not in ("q","r","n","b"):
promotion = input("Promote pawn to (q,r,n,b): ")[:1]
piece = promotion
board[toC][toR] = piece+"_"+player
# en passant
enPassant,brokenCastles = board[8:] or (None,set())
if piece=="p" and toPosition == enPassant:
print("enPassant!")
board[toC][fromR] = ""
enPassant = next(iter(targets[fromPosition][toPosition]*(piece=="p")),None)
# castle
if piece=="k" and abs(toC-fromC)>1:
rookFrom = ((fromC>toC)*7,fromR)
rookTo = targets[fromPosition][toPosition][0]
board = playMove(board,player,rookFrom,rookTo)
brokenCastles = brokenCastles.union(targets["breakCastle"][fromPosition])
board[8:] = (enPassant,brokenCastles)
return board
A dumb computer opponent:
import random
def computerMove(board,player,legalMoves):
return random.choice(legalMoves),"q"
Simple game play implementation ...
def playChess(board=None,player="white",computer=None):
if board is None: board = initialBoard
opponent = "black" if player == "white" else "white"
while True:
printBoard(board)
legalMoves = getMoves(board,player[:1])
if isCheck(board,player[:1]):
legalMoves = [ move for move in legalMoves
if not isCheck(playMove(board,*move,"q"),player[:1])]
if not legalMoves: print("CHECK MATE!");return opponent
print("CHECK!")
elif not legalMoves:
print("STALEMATE!");return "DRAW"
while True:
print(f"{player}'s move: (cr-cr):",end=" ")
if player==computer:
move,promote = computerMove(board,player,legalMoves)
print( "-".join(f"{c}{r}" for c,r in move))
break
move,promote = input(),"?"
if move == "resign": return opponent
if move == "draw":
if input(f"Does {opponent} accept a draw? ")=="y": return "DRAW"
else: continue
try:
move = tuple(divmod(p,10) for p in map(int,move.split("-")))
if move in legalMoves: break
except: pass
print("Not a valid move, try again")
print("Legal Moves:",*(f"{fc}{fr}-{tc}{tr}"
for (fc,fr),(tc,tr) in sorted(legalMoves)))
board = playMove(board,*move,promote)
player,opponent = opponent,player
Run the game ...
stats = {"black":0, "white":0, "DRAW":0}
while True:
print("Specify moves as cr-cr e.g. 04-06 to move from (0,4) to (0,6)")
outcome = playChess(computer="black")
stats[outcome] += 1
print(*(f"{p}: {c} " for p,c in stats.items()))
print()
if input("continue (y/n)?:")=="n":break
Upvotes: 7
Reputation: 42143
There is much to be done with pre-calculated data structures. For example, you could prepare a dictionary with the possible destinations from any positions for every piece type and orientation. With that, you wouldn't need complex code to check available moves.
[SEE MY SECOND ANSWER FOR CONSOLIDATED AND ADJUSTED CODE]
You could also use it to perform a first verification for check!. You would do that by checking the positions the king could reach if it were another piece. For example if you find a rook at a position where a rook could move from the king's position then there is a potential for check!. Doing this for each piece type will allow you to know if evaluating actual moves is necessary.
from collections import defaultdict
targets = dict()
positions = [ (r,c) for r in range(8) for c in range(8) ]
def valid(positions):
return [(r,c) for r,c in positions if r in range(8) and c in range(8)]
start with basic trajectories ...
targets["up"] = { (r,c):valid( (r+v,c) for v in range(1,8))
for r,c in positions }
targets["down"] = { (r,c):valid( (r-v,c) for v in range(1,8))
for r,c in positions }
targets["vertical"] = { (r,c):targets["up"][r,c]+targets["down"][r,c]
for r,c in positions }
targets["left"] = { (r,c):valid( (r,c+h) for h in range(1,8))
for r,c in positions }
targets["right"] = { (r,c):valid( (r,c+h) for h in range(1,8))
for r,c in positions }
targets["horizontal"] = { (r,c):targets["left"][r,c]+targets["right"][r,c]
for r,c in positions }
targets["upleft"] = { (r,c):[(ru,cl) for (ru,_),(_,cl) in zip(targets["up"][r,c],targets["left"][r,c])]
for r,c in positions }
targets["upright"] = { (r,c):[(ru,cr) for (ru,_),(_,cr) in zip(targets["up"][r,c],targets["right"][r,c])]
for r,c in positions }
targets["downleft"] = { (r,c):[(rd,cl) for (rd,_),(_,cl) in zip(targets["down"][r,c],targets["left"][r,c])]
for r,c in positions }
targets["downright"] = { (r,c):[(rd,cr) for (rd,_),(_,cr) in zip(targets["down"][r,c],targets["right"][r,c])]
for r,c in positions }
targets["diagUL"] = { (r,c):targets["upleft"][r,c]+targets["downright"][r,c]
for r,c in positions }
targets["diagDL"] = { (r,c):targets["downleft"][r,c]+targets["upright"][r,c]
for r,c in positions }
then combine them for each piece type ...
targets["king"] = { (r,c):valid( (r+v,c+h) for v in (-1,0,1) for h in (-1,0,1) if v or h)
for r,c in positions }
targets["rook"] = { (r,c):targets["horizontal"][r,c]+targets["vertical"][r,c]
for r,c in positions }
targets["bishop"] = { (r,c):targets["diagUL"][r,c]+targets["diagDL"][r,c]
for r,c in positions }
targets["queen"] = { (r,c):targets["rook"][r,c]+targets["bishop"][r,c]
for r,c in positions }
targets["knight"] = { (r,c):valid((r+v,c+h) for v,h in [(2,1),(2,-1),(1,2),(1,-2),(-2,1),(-2,-1),(-1,2),(-1,-2)])
for r,c in positions }
targets["wpawn"] = { (r,c):valid([(r+1,c)]*(r>0) + [(r+2,c)]*(r==1))
for r,c in positions }
targets["bpawn"] = { (r,c):valid([(r-1,c)]*(r<7) + [(r-2,c)]*(r==6))
for r,c in positions }
targets["wptake"] = { (r,c):valid([(r+1,c+1),(r+1,c-1)]*(r>0))
for r,c in positions }
targets["bptake"] = { (r,c):valid([(r-1,c+1),(r-1,c-1)]*(r<7))
for r,c in positions }
targets["wcastle"] = defaultdict(list,{ (0,4):[(0,2),(0,6)] })
targets["bcastle"] = defaultdict(list,{ (7,4):[(7,2),(7,6)] })
This will allow you to directly get the list of potential move positions for any piece anywhere on the board.
For example:
targets["bishop"][5,4]
# [(6, 3), (7, 2), (4, 5), (3, 6), (2, 7), (4, 3), (3, 2), (2, 1), (1, 0), (6, 5), (7, 6)]
To know if there is a potential check on the white king at 5,4, you can perform a quick verification before going into move simulations:
kingPos = (5,4)
checkByQueen = any(board[r][c]=="q_b" for r,c in targets["queen"][kingPos])
checkByKnight = any(board[r][c]=="n_b" for r,c in targets["knight"][kingPos])
checkByRook = any(board[r][c]=="r_b" for r,c in targets["rook"][kingPos])
checkByBishop = any(board[r][c]=="b_b" for r,c in targets["bishop"][kingPos])
checkByPawn = any(board[r][c]=="p_b" for r,c in targets["wptake"][kingPos])
if none of those are True, then there is no threat to the white king. If checkByQueen, checkByRook or checkByBishop is True, then you'll need to verify occlusion by another piece in between but that would have already reduced the number of cases considerably.
You could also enhance the dictionary to give you the positons between two squares on the board using a position as key (instead of a string).
for r,c in positions:
targets[(r,c)] = defaultdict(list)
for direction in ("up","down","left","right","upleft","upright","downleft","downright"):
path = targets[direction][r,c]
for i,(tr,tc) in enumerate(path):
targets[(r,c)][tr,tc]=path[:i]
This would allow you to easily check if there is a piece in between two positions. For example, if you find a queen at (5,0) you can check if the king is in line of sight using this:
queenPos = next((r,c) for r,c in targets["queen"][kingPos]
if board[r][c]=="q_b") # (5,0)
targets[kingPos][queenPos] # [(5, 3), (5, 2), (5, 1)]
lineOfSight = all(board[r][c]=="" for r,c in targets[kingPos][queenPos])
This can be combined into the above conditions to give a comprehensive verification:
def lineOfSight(A,B):
return all(board[r][c]=="" for r,c in targets[A][B])
checkByQueen = any(board[r][c]=="q_b" and lineOfSight(kingPos,(r,c))
for r,c in targets["queen"][kingPos] )
checkByRook = any(board[r][c]=="r_b" and lineOfSight(kingPos,(r,c))
for r,c in targets["rook"][kingPos] )
checkByBishop = any(board[r][c]=="b_b" and lineOfSight(kingPos,(r,c))
for r,c in targets["bishop"][kingPos])
Using all this you would not need to simulate moves at all to detect a check!, you could do it in a single line:
isCheck = any( board[r][c]==opponent and lineOfSight(kingPos,(r,c))
for opponent,piece in [("q_b","queen"),("r_b","rook"),("b_b","bishop"),("n_b","knight"),("p_b","wptake")]
for r,c in target[piece][kingPos] )
Sample content:
for r,c in positions:
print("FROM",(r,c))
for piece in targets:
print(f" {piece:10}:",*targets[piece][r,c])
...
FROM (2, 4)
up : (3, 4) (4, 4) (5, 4) (6, 4) (7, 4)
down : (1, 4) (0, 4)
vertical : (3, 4) (4, 4) (5, 4) (6, 4) (7, 4) (1, 4) (0, 4)
left : (2, 3) (2, 2) (2, 1) (2, 0)
right : (2, 5) (2, 6) (2, 7)
horizontal: (2, 3) (2, 2) (2, 1) (2, 0) (2, 5) (2, 6) (2, 7)
upleft : (3, 3) (4, 2) (5, 1) (6, 0)
upright : (3, 5) (4, 6) (5, 7)
downleft : (1, 3) (0, 2)
downright : (1, 5) (0, 6)
diagUL : (3, 3) (4, 2) (5, 1) (6, 0) (1, 5) (0, 6)
diagDL : (1, 3) (0, 2) (3, 5) (4, 6) (5, 7)
king : (1, 4) (1, 5) (2, 3) (2, 5) (3, 3) (3, 4)
rook : (2, 3) (2, 2) (2, 1) (2, 0) (2, 5) (2, 6) (2, 7) (3, 4) (4, 4) (5, 4) (6, 4) (7, 4) (1, 4) (0, 4)
bishop : (3, 3) (4, 2) (5, 1) (6, 0) (1, 5) (0, 6) (1, 3) (0, 2) (3, 5) (4, 6) (5, 7)
queen : (2, 3) (2, 2) (2, 1) (2, 0) (2, 5) (2, 6) (2, 7) (3, 4) (4, 4) (5, 4) (6, 4) (7, 4) (1, 4) (0, 4) (3, 3) (4, 2) (5, 1) (6, 0) (1, 5) (0, 6) (1, 3) (0, 2) (3, 5) (4, 6) (5, 7)
wpawn : (3, 4)
bpawn : (1, 4)
wptake : (3, 5) (3, 3)
bptake : (1, 5) (1, 3)
knight : (4, 5) (4, 3) (3, 6) (3, 2) (0, 5) (0, 3) (1, 6) (1, 2)
...
[EDIT]
To leverage this for move generation, you still have to add some conditions but I believe the dictionary should make the logic simpler and faster:
# add to setup ...
targets["bishop"]["paths"] = ["upleft","upright","downleft","downright"]
targets["rook"]["paths"] = ["up","down","left","right"]
targets["queen"]["paths"] = targets["bishop"]["paths"]+targets["rook"]["paths"]
def linearMoves(position,opponent,piece):
if position in pinnedPositions: return # see below
for direction in targets[piece]["paths"]
for r,c in targets[direction][position]:
if board[r][c]=="": yield (position,(r,c)); continue
if board[r][c].endswith(opponent): yield(position,(r,c))
break
... initialize move generation cycle
# flag white pieces that are pinned
# (do this before each move generation)
pinnedPositions = set()
for piece,path in [("q_b","queen"),("r_b","rook"),("b_b","bishop"):
for T in targets[path][kingPos]:
if board[T] != piece: continue
pinned = [[board[r][c][-1:] for r,c in targets[T][kingPos]]
if pinned.count("w")==1 and "b" not in pinned:
pinnedPositions.add(targets[T][kingPos][pinned.index("w")])
... for each piece on the board ...
moves = []
# Move white bishop from position bishosPos ...
moves += linearMoves(bishopPos,"b","bishop")
# Move white rook from position rookPos ...
moves += linearMoves(rookPos,"b","rook")
# Move white queen from position queenPos ...
moves += linearMoves(queenPos,"b","queen")
# Move white knight from position knightPos ...
moves += ( (knightPos,(r,c)) for r,c in targets["knight"][knightPos]
if board[r][c][-1:]!="w" )
# Move white pawn from position pawnPos ...
moves += ( (pawnPos,(r,c)) for r,c in targets["wpawn"][pawnPos]
if board[r][c][-1:]=="" and lineOfSight(pawnPos,(r,c)) )
moves += ( (pawnPos,(r,c)) for r,c in targets["wptake"][pawnPos]
if board[r][c][-1:]=="b" )
# Move white king from position kingPos ...
# (need to filter this so king doesn't place itself in check!)
moves += ( (kingPos,(r,c)) for r,c in targets["king"][kingPos]
if board[r][c][-1]!="w" )
There are more exceptions to be managed such as "castling" and "en passant" but most of the code should be simpler (and probably faster).
Upvotes: 14
Reputation: 166
It looks like you complicate things in your move generation and check detection which makes it really slow.
Better check detection approach
Now you say that you generate all legal moves for the opponent and see if they can capture the king. This is super slow and a better approach is to look from your own kings perspective and see if there are any enemy pieces in any directions after you have made the move, it could look something like this (where square is your king square):
def is_in_check(square):
enemy_color, friendly_color = ('b', 'w') if self.is_white_turn else ('w', 'b')
# Check out from all directions from the king
for i, d in enumerate(s.directions):
for j in range(1, 8): # Check the entire row/column in that direction
end_square = square + d*j
piece_color, piece_type = self.board[end_square][0], self.board[end_square][1]
if is_on_board(end_square ):
if piece_color == friendly_color and piece_type != 'K':
break
elif piece_color == enemy_color:
# 5 different cases:
# 1. Orthogonally from king and piece is a rook
# 2. Diagonally from king and piece is a bishop
# 3. 1 square away diagonally from king and piece is a pawn
# 4. Any direction and piece is a queen
# 5. Any direction 1 square away and piece is a king
if (0 <= i <= 3 and piece_type == 'R') or \
(4 <= i <= 7 and piece_type == 'B') or \
(j == 1 and piece_type == 'p' and ((enemy_color == 'w' and 6 <= i <= 7) or (enemy_color == 'b' and 4 <= i <= 5))) or \
(piece_type == 'Q') or \
(j == 1 and piece_type == 'K'):
return True
else: # Enemy piece that is not applying check or pin
break
else: # i, j is off board
break
# Check for knight checks
for d in s.knight_moves:
end_piece = self.board[square + d]
if is_on_board(end_square):
if end_piece[1] == 'N' and end_piece[0] == enemy_color: # Enemy knight attacking king
return True
return False
Ask in comment if the code is unclear, I copied most from my early engine so it might not be exactly as your representation. The idea is to look out from all directions from the king. If you find own piece or is off board, break and go on to next direction. If you find enemy piece, then there are the 5 cases commented in the code: if you look diagonally and enemy piece is bishop etc. This lookup is very fast since at maximum you have to look at 27 places if king is in middle of board and no piece blocking, but often much much less.
Move generation
I have spent a lot of time trying to make my Python engine as fast as possible and started as you with a 2D array board representation. It works, but a 1D board representation is faster (although a bit harder to get your head around).
But as for your 2D representation there are 2 approaches as I see it:
1. Generate pseudo legal moves with legal check later
It looks like you have a working approach. I find it a bit nicer to loop through the possible directions instead of having it in 4 separate loops, something like this for the queen for example (sorry for showing my 1D approach, it is however similar for you, just other directions):
def get_queen_moves(square):
# Up, left, down, right, up/left, up/right, down/left, down/right
for d in [-10, -1, 10, 1, -11, -9, 9, 11]:
for i in range(1, 8): # At most 7 squares in each direction
end_square = square + d*i
end_piece = self.board[end_square]
# If square is enemy piece or empty square, append move
if end_piece in [enemy_pieces, empty_square]:
moves.append(square, end_square)
# If enemy piece, then break the direction since we can't go further here
if end_piece in enemy_pieces:
break
# Found own piece, can't move here so move on to next direction
else:
break
At your minimax (negamax in my case, same approach anyways) search you will do something like this:
def negamax(depth, alpha, beta):
# Depth = 0, return value from the quiescence search
if depth == 0:
return self.quiescence(alpha, beta)
# Get pseudo legal moves
children = gen_moves(self.gamestate)
# Negamax recursive loop
for child in children:
# If move is legal, make it. Otherwise move on to the next candidate.
# In my make_move function I return 1 if I am not left in check, otherwise I unmake the move there and return 0.
if self.gamestate.make_move(child):
# Do a normal search
score = -self.negamax(depth - 1, -beta, -alpha, True)
# Take back move
self.gamestate.unmake_move()
If you implement move ordering and alpha/beta etc you will much likely save much time to not check the legality for all moves, but only for the moves you are considering. I hope I make myself clear here.
2. Generate pins and only legal moves
I like generating pins first and then generate only legal moves. It is a bit more complicated, so please ask if my code is unclear at any point. The idea is to go from the king in all directions as before. If we found own piece (say bishop in this case) in e.g. diagonal direction we keep going more times and see if we find an enemy bishop or queen in that direction. If we do, our bishop is pinned. We save the piece and also in what direction it was found (pinned pieces can still move, towards and away from the king if it is a bishop as in this case).
Here is the code for generating legal moves and also for finding pins and checks:
# Get all moves considering checks and pins
def get_valid_moves(self):
king_pos = self.white_king_location if self.is_white_turn else self.black_king_location
# Find if is in check and all the possible pinned pieces
self.is_in_check, self.pins, self.checks = self.check_for_pins_and_checks(king_pos)
# If we are in check we can only take the piece, move the king, or put own piece in the way
if self.is_in_check:
if len(self.checks) == 1: # Single check
moves = self.get_all_possible_moves()
check = self.checks[0]
checking_piece_pos = check[0]
piece_checking = self.board[check[0]] # Enemy piece that is causing the check
valid_squares = [] # Valid squares the piece can move to
if piece_checking[1] == 'N': # Knight check, must capture knight or move king
valid_squares = [checking_piece_pos]
else:
for i in range(1, 8):
valid_square = (king_pos + check[1] * i) # Look in the direction of checking piece
valid_squares.append(valid_square)
if valid_square == checking_piece_pos: # If finding the checking piece, look no further
break
# Filter to only keep moves that are valid during check
moves = list(filter(lambda x: x[0] == king_pos or x[1] in valid_squares or
(self.board[x[0]][1] == 'p' and x[1] == self.enpassant_square and piece_checking[1] == 'p'), moves))
else: # Double check, only king can move
moves = []
self.get_king_moves(king_pos, moves, False)
# If not in check, we find all moves (with respect to pins)
else:
moves = self.get_all_possible_moves()
return moves
# Checks if there are any pinned pieces or current checks
def check_for_pins_and_checks(self, square):
pins, checks = [], []
is_in_check = False
enemy_color, friendly_color = ('b', 'w') if self.is_white_turn else ('w', 'b')
# Check out from all directions from the king
for i in range(8):
d = s.directions[i]
possible_pin = False
for j in range(8): # Check the entire row/column in that direction
end_square = square + d*j
piece_color, piece_type = self.board[end_square][0], self.board[end_square][1]
if is_on_board(end_square):
if piece_color == friendly_color and piece_type != 'K':
if not possible_pin: # First own piece, we found a possible pin
possible_pin = (end_square, d)
else: # 2nd friendly piece, it wasn't a pin
break
elif piece_color == enemy_color:
# 5 different cases as before:
if (0 <= i <= 3 and piece_type == 'R') or \
(4 <= i <= 7 and piece_type == 'B') or \
(j == 1 and piece_type == 'p' and ((enemy_color == 'w' and 6 <= i <= 7) or (enemy_color == 'b' and 4 <= i <= 5))) or \
(piece_type == 'Q') or \
(j == 1 and piece_type == 'K'):
if not possible_pin: # No friendly piece is blocking -> is check
is_in_check = True
checks.append((end_square, d))
break
else: # Friendly piece is blocking -> we found a pinned piece
pins.append(possible_pin)
break
else: # Enemy piece that is not applying check or pin
break
else: # i, j is off board
break
# Check for knight checks
for d in s.knight_moves:
end_square = square + d
end_piece = self.board[end_square]
if is_on_board(end_square):
if end_piece[0] == enemy_color and end_piece[1] == 'N': # Enemy knight attacking king
is_in_check = True
checks.append((end_square, d))
return is_in_check, pins, checks
So now we need to apply our pinned information to our generate move functions. I will use the queen as example again. The only thing we need to do is to find if the piece is pinned (first extra chunk of code) and then right before we append the move we need to check that piece is not pinned OR that the pin direction allows us to move the piece there (e.g. move the queen towards or away from king).
def get_queen_moves(square):
# Loop through our pins and see if our piece is pinned. Remove it from our pinned piece list since we don't need the information any more.
pin_direction = ()
for i in range(len(self.pins)-1, -1, -1):
if self.pins[i][0] == square:
piece_pinned = True
pin_direction = (self.pins[i][1])
self.pins.remove(self.pins[i])
break
# Up, left, down, right, up/left, up/right, down/left, down/right
for d in [-10, -1, 10, 1, -11, -9, 9, 11]:
for i in range(1, 8): # At most 7 squares in each direction
end_square = square + d*i
end_piece = self.board[end_square]
# If square is enemy piece or empty square, append move
if end_piece in [enemy_pieces, empty_square]:
# Here we check if piece is pinned or if the direction allows us to add the piece anyway.
if not piece_pinned or pin_direction in (d, -d):
moves.append(square, end_square)
# If enemy piece, then break the direction since we can't go further here
if end_piece in enemy_pieces:
break
# Found own piece, can't move here so move on to next direction
else:
break
That should be it, please ask if you have any further questions :)
Upvotes: 2