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

Tags:

Python

Chess