Honn
Honn

Reputation: 744

Detecting checks more efficiently (Chess)

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)

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

Answers (3)

Alain T.
Alain T.

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

Alain T.
Alain T.

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

Eli
Eli

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 and then at search you test if they were legal or not.
  2. Generate all pinned pieces and then generate only legal moves.

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

Related Questions