Detecting checks more efficiently (Chess)
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).
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