"""Standard chess rules, 0x88 board, legal SAN and canonical position keys.
No runtime dependencies; used only for library indexing, never for UI latency.
"""
import hashlib
import re
START = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
OFFSETS = {'n': (-33, -31, -18, -14, 14, 18, 31, 33),
'b': (-17, -15, 15, 17), 'r': (-16, -1, 1, 16),
'q': (-17, -16, -15, -1, 1, 15, 16, 17),
'k': (-17, -16, -15, -1, 1, 15, 16, 17)}
[docs]
def square(name):
return (8 - int(name[1])) * 16 + ord(name[0]) - 97
[docs]
def name(sq):
return chr(97 + (sq & 7)) + str(8 - (sq >> 4))
[docs]
def color(piece):
return 'w' if piece.isupper() else 'b'
[docs]
class Chess:
def __init__(self, fen=START):
fields = fen.split()
if len(fields) != 6:
raise ValueError('FEN needs six fields')
self.board = [''] * 128
rows = fields[0].split('/')
if len(rows) != 8:
raise ValueError('FEN needs eight ranks')
for r, row in enumerate(rows):
f = 0
for p in row:
if p.isdigit():
f += int(p)
elif p in 'pnbrqkPNBRQK' and f < 8:
self.board[r * 16 + f] = p
f += 1
else:
raise ValueError('Invalid FEN placement')
if f != 8:
raise ValueError('Invalid FEN rank')
self.turn, self.castling = fields[1:3]
self.ep = square(fields[3]) if fields[3] != '-' else None
self.half, self.full = map(int, fields[4:])
if self.turn not in ('w', 'b'):
raise ValueError('Invalid FEN turn')
[docs]
def fen(self):
rows = []
for r in range(8):
row, empty = '', 0
for f in range(8):
p = self.board[r * 16 + f]
if not p:
empty += 1
else:
row += (str(empty) if empty else '') + p
empty = 0
rows.append(row + (str(empty) if empty else ''))
return ' '.join(['/'.join(rows), self.turn, self.castling or '-',
name(self.ep) if self.ep is not None else '-', str(self.half), str(self.full)])
[docs]
def attacked(self, target, by):
for src in range(128):
if src & 0x88:
continue
p = self.board[src]
if not p or color(p) != by:
continue
kind = p.lower()
offsets = ((-17, -15) if by == 'w' else (15, 17)) if kind == 'p' else OFFSETS[kind]
for step in offsets:
dst = src + step
while not dst & 0x88:
if dst == target:
return True
if self.board[dst] or kind in 'pnk':
break
dst += step
return False
[docs]
def check(self, side=None):
side = side or self.turn
king = 'K' if side == 'w' else 'k'
try:
sq = self.board.index(king)
except ValueError:
return True
return self.attacked(sq, 'b' if side == 'w' else 'w')
[docs]
def pseudo(self):
result = []
for src in range(128):
if src & 0x88:
continue
p = self.board[src]
if not p or color(p) != self.turn:
continue
kind = p.lower()
if kind == 'p':
step = -16 if self.turn == 'w' else 16
targets = []
dst = src + step
if not dst & 0x88 and not self.board[dst]:
targets.append(dst)
if src >> 4 == (6 if self.turn == 'w' else 1) and not self.board[dst + step]:
targets.append(dst + step)
for dst in (src + step - 1, src + step + 1):
if not dst & 0x88 and ((self.board[dst] and color(self.board[dst]) != self.turn) or
(dst == self.ep and self.board[dst - step] == ('p' if self.turn == 'w' else 'P'))):
targets.append(dst)
for dst in targets:
for promotion in ('qrbn' if dst >> 4 in (0, 7) else ['']):
result.append((src, dst, promotion))
continue
for step in OFFSETS[kind]:
dst = src + step
while not dst & 0x88:
other = self.board[dst]
if other and color(other) == self.turn:
break
result.append((src, dst, ''))
if other or kind in 'nk':
break
dst += step
if kind == 'k' and src == (116 if self.turn == 'w' else 4) and not self.check():
enemy = 'b' if self.turn == 'w' else 'w'
for right, delta, rook, between in [('K', 2, 3, [1, 2]), ('Q', -2, -4, [-1, -2, -3])]:
if self.turn == 'b':
right = right.lower()
if right in self.castling and self.board[src + rook] == ('R' if self.turn == 'w' else 'r'):
if all(not self.board[src + d] for d in between) and all(
not self.attacked(src + d, enemy) for d in [delta // 2, delta]):
result.append((src, src + delta, ''))
return result
[docs]
def push(self, move):
src, dst, promotion = move
p, capture = self.board[src], self.board[dst]
old_ep = self.ep
self.ep = None
if p.lower() == 'p':
if dst == old_ep and not capture and (src & 7) != (dst & 7):
self.board[dst + (16 if self.turn == 'w' else -16)] = ''
if abs(dst - src) == 32:
self.ep = (src + dst) // 2
if p.lower() == 'k':
for right in ('KQ' if self.turn == 'w' else 'kq'):
self.castling = self.castling.replace(right, '')
if abs(dst - src) == 2:
rook_src, rook_dst = (src + 3, src + 1) if dst > src else (src - 4, src - 1)
self.board[rook_dst], self.board[rook_src] = self.board[rook_src], ''
for sq, right in [(0, 'q'), (7, 'k'), (112, 'Q'), (119, 'K')]:
if sq in (src, dst):
self.castling = self.castling.replace(right, '')
self.board[src] = ''
self.board[dst] = (promotion.upper() if self.turn == 'w' else promotion) if promotion else p
self.half = 0 if capture or p.lower() == 'p' else self.half + 1
self.full += self.turn == 'b'
self.turn = 'b' if self.turn == 'w' else 'w'
[docs]
def snapshot(self):
return self.board[:], self.turn, self.castling, self.ep, self.half, self.full
[docs]
def restore(self, state):
self.board, self.turn, self.castling, self.ep, self.half, self.full = state
[docs]
def legal(self):
result = []
for move in self.pseudo():
state = self.snapshot()
side = self.turn
self.push(move)
if not self.check(side):
result.append(move)
self.restore(state)
return result
[docs]
def san(self, move, legal=None):
src, dst, promo = move
p = self.board[src].lower()
if p == 'k' and abs(dst - src) == 2:
text = 'O-O' if dst > src else 'O-O-O'
else:
capture = bool(self.board[dst]) or (p == 'p' and dst == self.ep)
text = '' if p == 'p' else p.upper()
others = [m for m in (legal if legal is not None else self.legal())
if m[0] != src and m[1] == dst and self.board[m[0]].lower() == p]
if p == 'p' and capture:
text += name(src)[0]
elif others:
same_file = any(m[0] & 7 == src & 7 for m in others)
same_rank = any(m[0] >> 4 == src >> 4 for m in others)
text += name(src) if same_file and same_rank else name(src)[1 if same_file else 0]
text += ('x' if capture else '') + name(dst) + ('=' + promo.upper() if promo else '')
state = self.snapshot()
self.push(move)
if self.check():
text += '+' if self.legal() else '#'
self.restore(state)
return text
[docs]
def move(self, text):
clean = re.sub(r'[+#?!]', '', text).replace('0', 'O').replace('=', '')
legal = self.legal()
for move in legal:
uci = name(move[0]) + name(move[1]) + move[2]
san = self.san(move, legal)
if clean in (uci, re.sub(r'[+#]', '', san).replace('=', '')):
self.push(move)
return san
raise ValueError('Illegal or ambiguous move: ' + text)
[docs]
def key(self):
fields = self.fen().split()[:4]
if self.ep is not None and not any(self.board[m[0]].lower() == 'p' and m[1] == self.ep
for m in self.legal()):
fields[3] = '-'
return hashlib.sha256(' '.join(fields).encode()).hexdigest()[:32]
[docs]
def perft(self, depth):
if depth == 0:
return 1
count = 0
for move in self.legal():
state = self.snapshot()
self.push(move)
count += self.perft(depth - 1)
self.restore(state)
return count