Closed purvi1508 closed 1 year ago
Yes, you could probably do that! Just add a tracker counting the number of checks given so far to the Board object.
I have did the necessary changes but faced an error, could you suggest to me where I am going wrong?
import chess import chess.engine import chess.variant
import time, math from itertools import count from collections import namedtuple, defaultdict
from functools import partial print = partial(print, flush=True)
version = "___"
piece = {"P": 100, "N": 280, "B": 320, "R": 479, "Q": 929, "K": 60000} pst = { 'P': ( 0, 0, 0, 0, 0, 0, 0, 0, 78, 83, 86, 73, 102, 82, 85, 90, 7, 29, 21, 44, 40, 31, 44, 7, -17, 16, -2, 15, 14, 0, 15, -13, -26, 3, 10, 9, 6, 1, 0, -23, -22, 9, 5, -11, -10, -2, 3, -19, -31, 8, -7, -37, -36, -14, 3, -31, 0, 0, 0, 0, 0, 0, 0, 0), 'N': ( -66, -53, -75, -75, -10, -55, -58, -70, -3, -6, 100, -36, 4, 62, -4, -14, 10, 67, 1, 74, 73, 27, 62, -2, 24, 24, 45, 37, 33, 41, 25, 17, -1, 5, 31, 21, 22, 35, 2, 0, -18, 10, 13, 22, 18, 15, 11, -14, -23, -15, 2, 0, 2, 0, -23, -20, -74, -23, -26, -24, -19, -35, -22, -69), 'B': ( -59, -78, -82, -76, -23,-107, -37, -50, -11, 20, 35, -42, -39, 31, 2, -22, -9, 39, -32, 41, 52, -10, 28, -14, 25, 17, 20, 34, 26, 25, 15, 10, 13, 10, 17, 23, 17, 16, 0, 7, 14, 25, 24, 15, 8, 25, 20, 15, 19, 20, 11, 6, 7, 6, 20, 16, -7, 2, -15, -12, -14, -15, -10, -10), 'R': ( 35, 29, 33, 4, 37, 33, 56, 50, 55, 29, 56, 67, 55, 62, 34, 60, 19, 35, 28, 33, 45, 27, 25, 15, 0, 5, 16, 13, 18, -4, -9, -6, -28, -35, -16, -21, -13, -29, -46, -30, -42, -28, -42, -25, -25, -35, -26, -46, -53, -38, -31, -26, -29, -43, -44, -53, -30, -24, -18, 5, -2, -18, -31, -32), 'Q': ( 6, 1, -8,-104, 69, 24, 88, 26, 14, 32, 60, -10, 20, 76, 57, 24, -2, 43, 32, 60, 72, 63, 43, 2, 1, -16, 22, 17, 25, 20, -13, -6, -14, -15, -2, -5, -1, -10, -20, -22, -30, -6, -13, -11, -16, -11, -16, -27, -36, -18, 0, -19, -15, -15, -21, -38, -39, -30, -31, -13, -31, -36, -34, -42), 'K': ( 4, 54, 47, -99, -99, 60, 83, -62, -32, 10, 55, 56, 56, 55, 10, 3, -62, 12, -57, 44, -67, 28, 37, -31, -55, 50, 11, -4, -19, 13, 0, -49, -55, -43, -52, -28, -51, -47, -8, -50, -47, -42, -43, -79, -64, -32, -29, -32, -4, 3, -14, -50, -57, -18, 13, 4, 17, 30, -3, -14, 6, -1, 40, 18), }
for k, table in pst.items(): padrow = lambda row: (0,) + tuple(x + piece[k] for x in row) + (0,) pst[k] = sum((padrow(table[i 8 : i 8 + 8]) for i in range(8)), ()) pst[k] = (0,) 20 + pst[k] + (0,) 20
N, E, S, W = -10, 1, 10, -1 directions = { "P": (N, N+W, N+E), "N": (N+N+E, E+N+E, E+S+E, S+S+E, S+S+W, W+S+W, W+N+W, N+N+W), "B": (N+E, S+E, S+W, N+W), "R": (N, E, S, W), "Q": (N, E, S, W, N+E, S+E, S+W, N+W), "K": (N, E, S, W, N+E, S+E, S+W, N+W) }
MATE_LOWER = piece["K"] - 10 piece["Q"] MATE_UPPER = piece["K"] + 10 piece["Q"]
QS = 35 EVAL_ROUGHNESS = 15
opt_ranges = dict( QS = (0, 300), EVAL_ROUGHNESS = (0, 50), )
import sys
INF = sys.maxsize
import chess import chess.variant from collections import namedtuple from itertools import count board = chess.variant.ThreeCheckBoard() Move = namedtuple("Move", "i j prom")
class Position(namedtuple("Position", "board score kp checks1 checks2")): """A state of a chess game board -- a ThreeCheckBoard representation of the board score -- the board evaluation kp - the king passant square checks1 - number of checks inflicted by player 1 checks2 - number of checks inflicted by player 2 """
def is_check(self, move):
i, j, prom = move
p, q = self.board.piece_at(i), self.board.piece_at(j)
# Copy variables and reset kp and checks
board = self.board.copy()
kp = None
checks1, checks2 = self.checks1, self.checks2
# Simulate the move
board.remove_piece_at(i)
board.set_piece_at(j, p)
# Check if the move gives a check
if q and q.piece_type == chess.KING:
if p.piece_type == chess.PAWN:
checks1 += 1
if checks1 == 3:
self.score = -chess.INFINITY
else:
checks2 += 1
if checks2 == 3:
self.score = chess.INFINITY
return True
return False
def is_game_over(self):
# Check if repeated 3 position
if self.board.is_repetition(3):
return True, "Draw"
# Check for three-check
if self.is_three_check():
return True, "Three-check"
else:
return False, None
def gen_moves(self):
self.history.append(self.board.fen())
for i, p in self.board.piece_map().items():
if p.color != self.board.turn:
continue
for d in p.piece_type.directions:
for j in count(i + d, d):
q = self.board.piece_at(j)
# Stay inside the board, and off friendly pieces
if q is None or q.color == p.color:
break
# Pawn move, double move and capture
if p.piece_type == chess.PAWN:
if d in (chess.WEST, chess.EAST) and q:
break
if (
d in (chess.WEST + chess.NORTH, chess.EAST + chess.NORTH)
and not q
and j not in (self.board.ep_square, self.board.ep_square - 1, self.board.ep_square + 1)
and j != abs(j - self.board.ep_square) >= 2
):
break
if d in (chess.WEST + chess.NORTH, chess.EAST + chess.NORTH) and not q:
if j not in (self.board.ep_square, self.board.ep_square - 1, self.board.ep_square + 1) or j != self.board.ep_square and abs(j - self.board.ep_square) >= 2:
break
else:
yield chess.Move(i, j)
if d == chess.NORTH and not q:
if j + chess.NORTH * self.board.turn in self.board.occupied_co[chess.WHITE if self.board.turn == chess.BLACK else chess.BLACK]:
break
elif j + chess.NORTH * self.board.turn not in self.board.occupied:
if j < 16 or j >= 48:
for promotion in (chess.QUEEN, chess.ROOK, chess.BISHOP, chess.KNIGHT):
yield chess.Move(i, j, promotion=promotion)
else:
yield chess.Move(i, j)
# Other pieces
else:
if j in chess.SQUARES:
if q is None:
yield chess.Move(i, j)
elif q.color != p.color:
yield chess.Move(i, j)
break
else:
break
def move(self, move):
i, j, prom = move.i, move.j, move.prom
p = self.board.piece_at(i)
# Copy variables and reset kp and checks
board = self.board.copy()
kp = None
checks1, checks2 = self.checks1, self.checks2
# Simulate the move
board.remove_piece_at(i)
if prom:
board.set_piece_at(j, chess.Piece(prom, p.color))
else:
board.set_piece_at(j, p)
if p.piece_type == chess.KING:
if abs(i-j) == 2:
kp = chess.square((i+j)//2, j if j<i else i)
elif p.piece_type == chess.PAWN and abs(i-j) == 16:
kp = i + (j-i)//2
# Count number of checks
checks1, checks2 = 0, 0
opp_king = chess.KING | (chess.BLACK if p.color == chess.WHITE else chess.WHITE)
for k in board.pieces(opp_king, p.color):
for l in board.attacks_mask(k) & board.occupied_co[p.color]:
checks2 += 1 if board.gives_check(chess.Move(l, k)) else 0
opp_king = chess.KING | (chess.WHITE if p.color == chess.WHITE else chess.BLACK)
for k in board.pieces(opp_king, not p.color):
for l in board.attacks_mask(k) & board.occupied_co[not p.color]:
checks1 += 1 if board.gives_check(chess.Move(l, k)) else 0
return Position(board, self.score, kp, checks1, checks2)
def is_three_check(self):
return self.checks1 >= 3 or self.checks2 >= 3
def evaluate(self):
# Check for checkmate
if self.board.is_checkmate():
return -chess.INFINITY if self.board.turn == chess.WHITE else chess.INFINITY
# Check for stalemate
if self.board.is_stalemate() or self.board.is_insufficient_material():
return 0
# Evaluate based on material and positional advantage
score = 0
for i in range(64):
p = self.board.piece_at(i)
if p is None:
continue
score += (chess.piece_value[p.piece_type] if p.color == chess.WHITE else -chess.piece_value[p.piece_type])
if p.color == chess.WHITE:
if p.piece_type == chess.PAWN and i+8 in self.board.occupied:
score -= 5
if p.piece_type == chess.KING and i in (chess.C1, chess.C8, chess.G1, chess.G8):
score -= 25
else:
if p.piece_type == chess.PAWN and i-8 in self.board.occupied:
score += 5
if p.piece_type == chess.KING and i in (chess.C1, chess.C8, chess.G1, chess.G8):
score += 25
return score
def value(self, move):
i, j, prom = move.from_square, move.to_square, move.promotion
p = self.piece_type_at(i)
q = self.piece_type_at(j)
# Actual move
score = pst[p][j] - pst[p][i]
# Capture
if q is not None and q != chess.PAWN:
score += pst[q][j]
# Special pawn stuff
if p == chess.PAWN:
if chess.square_rank(j) in [0, 7]:
score += pst[prom][j] - pst[chess.PAWN][j]
return score
Entry = namedtuple("Entry", "lower upper", defaults=(-MATE_UPPER, MATE_UPPER))
class Searcher: def init(self): self.tp_score = defaultdict(Entry) self.tp_move = {} self.history = set() self.nodes = 0
def bound(self, pos, gamma, depth, can_null=True):
""" Let s* be the "true" score of the sub-tree we are searching.
The method returns r, where
if gamma > s* then s* <= r < gamma (A better upper bound)
if gamma <= s* then gamma <= r <= s* (A better lower bound) """
self.nodes += 1
depth = max(depth, 0)
if pos.score <= -MATE_LOWER:
return -MATE_UPPER
entry = self.tp_score[pos, depth, can_null]
if entry.lower >= gamma: return entry.lower
if entry.upper < gamma: return entry.upper
if can_null and depth > 0 and pos in self.history:
return 0
# Generator of moves to search in order.
# This allows us to define the moves, but only calculate them if needed.
def moves():
# First try not moving at all. We only do this if there is at least one major
# piece left on the board, since otherwise zugzwangs are too dangerous.
if depth > 2 and can_null and any(c in pos.board for c in "RBNQ"):
yield None, -self.bound(pos.rotate(nullmove=True), 1 - gamma, depth - 3)
# For QSearch we have a different kind of null-move, namely we can just stop
# and not capture anything else.
if depth == 0:
yield None, pos.score
# Look for the strongest ove from last time, the hash-move.
killer = self.tp_move.get(pos)
# If there isn't one, try to find one with a more shallow search.
if not killer and depth > 2:
self.bound(pos, gamma, depth - 3, can_null=False)
killer = self.tp_move.get(pos)
# If depth == 0 we only try moves with high intrinsic score (captures and
# promotions). Otherwise we do all moves. This is called quiescent search.
val_lower = QS if depth == 0 else -MATE_LOWER
# Only play the move if it would be included at the current val-limit,
# since otherwise we'd get search instability.
# We will search it again in the main loop below, but the tp will fix
# things for us.
if killer and pos.value(killer) >= val_lower:
yield killer, -self.bound(pos.move(killer), 1 - gamma, depth - 1)
# Then all the other moves
for val, move in sorted(((pos.value(m), m) for m in pos.gen_moves()), reverse=True):
# Quiescent search
if val < val_lower:
break
if depth <= 1 and pos.score + val < gamma:
# Need special case for MATE, since it would normally be caught
# before standing pat.
yield move, pos.score + val if val < MATE_LOWER else MATE_UPPER
# We can also break, since we have ordered the moves by value,
# so it can't get any better than this.
break
yield move, -self.bound(pos.move(move), 1 - gamma, depth - 1)
# Run through the moves, shortcutting when possible
best = -MATE_UPPER
for move, score in moves():
best = max(best, score)
if best >= gamma:
# Save the move for pv construction and killer heuristic
if move is not None:
self.tp_move[pos] = move
break
if depth > 0 and best == -MATE_UPPER:
flipped = pos.rotate(nullmove=True)
# Hopefully this is already in the TT because of null-move
in_check = self.bound(flipped, MATE_UPPER, 0) == MATE_UPPER
best = -MATE_LOWER if in_check else 0
# Table part 2
if best >= gamma:
self.tp_score[pos, depth, can_null] = Entry(best, entry.upper)
if best < gamma:
self.tp_score[pos, depth, can_null] = Entry(entry.lower, best)
return best
def search(self, history):
"""Iterative deepening MTD-bi search"""
self.nodes = 0
self.history = set(history)
self.tp_score.clear()
gamma = 0
# In finished games, we could potentially go far enough to cause a recursion
# limit exception. Hence we bound the ply. We also can't start at 0, since
# that's quiscent search, and we don't always play legal moves there.
for depth in range(1, 1000):
# The inner loop is a binary search on the score of the position.
# Inv: lower <= score <= upper
# 'while lower != upper' would work, but it's too much effort to spend
# on what's probably not going to change the move played.
lower, upper = -MATE_LOWER, MATE_LOWER
while lower < upper - EVAL_ROUGHNESS:
score = self.bound(history[-1], gamma, depth, can_null=False)
if score >= gamma:
lower = score
if score < gamma:
upper = score
yield depth, gamma, score, self.tp_move.get(history[-1])
gamma = (lower + upper + 1) // 2
A1, H1, A8, H8 = 91, 98, 21, 28
A1, H1, A8, H8 = 91, 98, 21, 28 initial = ( " \n" # 0 - 9 " \n" # 10 - 19 " rnbqkbnr\n" # 20 - 29 " pppppppp\n" # 30 - 39 " ........\n" # 40 - 49 " ........\n" # 50 - 59 " ........\n" # 60 - 69 " ........\n" # 70 - 79 " PPPPPPPP\n" # 80 - 89 " RNBQKBNR\n" # 90 - 99 " \n" # 100 -109 " \n" # 110 -119 )
def parse(c): fil, rank = ord(c[0]) - ord("a"), int(c[1]) - 1 return A1 + fil - 10 * rank
def render(i): rank, fil = divmod(i - A1, 10) return chr(fil + ord("a")) + str(-rank + 1)
hist = [Position(initial, 0, 0,0,0)]
searcher = Searcher() while True: args = input().split() if args[0] == "uci": print(f"id name {version}") print("uciok")
elif args[0] == "isready":
print("readyok")
elif args[0] == "quit":
break
elif args[:2] == ["position", "startpos"]:
del hist[1:]
for ply, move in enumerate(args[3:]):
i, j, prom = parse(move[:2]), parse(move[2:4]), move[4:].upper()
if ply % 2 == 1:
i, j = 119 - i, 119 - j
hist.append(hist[-1].move(Move(i, j, prom)))
elif args[0] == "go":
wtime, btime, winc, binc = [int(a) / 1000 for a in args[2::2]]
if len(hist) % 2 == 0:
wtime, winc = btime, binc
think = min(wtime / 40 + winc, wtime / 2 - 1)
start = time.time()
move_str = None
for depth, gamma, score, move in Searcher().search(hist):
# The only way we can be sure to have the real move in tp_move,
# is if we have just failed high.
if score >= gamma:
i, j = move.i, move.j
if len(hist) % 2 == 0:
i, j = 119 - i, 119 - j
move_str = render(i) + render(j) + move.prom.lower()
print(f"info depth {depth} score cp {score} pv {move_str}")
if move_str and time.time() - start > think * 0.8:
break
print("bestmove", move_str or '(none)')
How could we make changes in code to make it suitable for three check chess variant