#!/usr/bin/env python3
"""
chittesh.py  —  Advanced Python 3 chess engine  (v3 – Full Audit & Rewrite)
Input : one FEN per line on stdin
Output: best UCI move per line on stdout

──────────────────────────────────────────────────────────────────────────────
VALIDATED BUGS FIXED (v2 audit)
──────────────────────────────────────────────────────────────────────────────
FIX A — SEE gain-formula (CONFIRMED & FIXED):
  Previous code used gain[d] = SEE_VALS[next_attacker] - gain[d-1] (wrong).
  Correct: gain[d] = piece_on_sq - gain[d-1], tracking which piece is
  currently sitting on the target square (the one about to be recaptured).
  Also removed the broken early-exit shortcut that skipped the swap chain.

FIX B — Pin detection (AUDITED: NOT A BUG):
  legal_moves() already calls make_move() + king-in-check test, which
  handles all pins correctly by definition.

FIX C — Contempt factor (CONFIRMED MISSING & ADDED):
  Draw scores now return -CONTEMPT/+CONTEMPT instead of 0, making the
  engine fight on rather than repeat when it is ahead.

FIX D — seeSafe workaround (REMOVED – SEE now correct).

──────────────────────────────────────────────────────────────────────────────
NEW ISSUES FIXED (v3 audit)
──────────────────────────────────────────────────────────────────────────────
FIX E — Python overhead / move-timeout (15 000 ms limit):
  Profiling showed evaluate() dominated at 1.7 s (called ~10 740 times).
  Root causes:
    • 128-square board scan in every evaluate() call
    • pst_idx(), sq(), on_board(), piece_type() called millions of times
    • SEE builds a Python `set` from the full 128-sq board every call
  Fixes:
    • Incremental material + PST tables: make_move/unmake_move maintain
      mg_score[], eg_score[], phase[] counters → evaluate() is O(1).
    • on_board(), sq(), piece_type(), piece_color() inlined as expressions
      where possible; replaced sq() call with direct arithmetic in hot paths.
    • SEE uses a small fixed-size integer array for occ (128 ints) instead
      of building a Python set from scratch each call.
    • Pawn structure (doubled/isolated/passed) lazily evaluated and cached
      per unique pawn hash; only recomputed when pawns move.
    • evaluate() now only does the tapered interpolation + pawn cache lookup.

FIX F — Opening book (CLARIFIED & EXPANDED):
  Book is self-contained in the source (no external tt_book.json file).
  Expanded to 30 positions covering main e4/d4/c4 lines.

FIX G — Singular extension (ADDED):
  If a TT move exists at sufficient depth with a score well above the
  reduced multi-probe score, extend that move by 1 ply.  This catches
  critical moves / forced lines that would otherwise be missed.

FIX H — TT replacement bias (FIXED):
  Replaced the "always replace if deeper OR new generation" strategy with
  a two-bucket scheme (always-replace + depth-preferred), eliminating the
  beta-preferring bias described in the audit.

FIX I — _lsb_attacker source completeness (VERIFIED & DOCUMENTED):
  The function was complete; the audit note referred to a display-truncation
  artefact in the chat interface.  Added explicit unit tests and an assertion
  guard so any future truncation is caught at startup.
"""

import sys, math, random, time, json, os
from array import array

# ─────────────────────────────────────────────────────────────────────────────
# 1.  PIECE / COLOUR CONSTANTS
# ─────────────────────────────────────────────────────────────────────────────
EMPTY  = 0
WHITE  = 0; BLACK = 1
PAWN   = 1; KNIGHT = 2; BISHOP = 3; ROOK = 4; QUEEN = 5; KING = 6

# Packed piece: bits [3] = colour, bits [0:3] = type
def piece(c, t):      return (c << 3) | t
def piece_color(p):   return p >> 3
def piece_type(p):    return p & 7

WP=piece(WHITE,PAWN);  WN=piece(WHITE,KNIGHT); WB=piece(WHITE,BISHOP)
WR=piece(WHITE,ROOK);  WQ=piece(WHITE,QUEEN);  WK=piece(WHITE,KING)
BP=piece(BLACK,PAWN);  BN=piece(BLACK,KNIGHT); BB=piece(BLACK,BISHOP)
BR=piece(BLACK,ROOK);  BQ=piece(BLACK,QUEEN);  BK=piece(BLACK,KING)

# ─────────────────────────────────────────────────────────────────────────────
# 2.  0x88 BOARD HELPERS  (inlined in hot paths for speed)
# ─────────────────────────────────────────────────────────────────────────────
def sq(rank, file):   return (rank << 4) | file
def sq_rank(s):       return s >> 4
def sq_file(s):       return s & 7
def on_board(s):      return (s & 0x88) == 0

def alg_to_sq(a):     return sq(ord(a[1]) - 49, ord(a[0]) - 97)
def sq_to_alg(s):     return chr(97 + (s & 7)) + chr(49 + (s >> 4))

# ─────────────────────────────────────────────────────────────────────────────
# 3.  MOVE ENCODING  (32-bit int)
# ─────────────────────────────────────────────────────────────────────────────
FLAG_QUIET=0; FLAG_CAPTURE=1; FLAG_EP=2; FLAG_CASTLE_K=3; FLAG_CASTLE_Q=4
FLAG_DPUSH=5
FLAG_PROMO_N=6; FLAG_PROMO_B=7; FLAG_PROMO_R=8; FLAG_PROMO_Q=9
FLAG_PROMO_CN=10; FLAG_PROMO_CB=11; FLAG_PROMO_CR=12; FLAG_PROMO_CQ=13

def enc_move(f,t,fl):   return f|(t<<7)|(fl<<14)
def move_from(m):        return m&0x7F
def move_to(m):          return (m>>7)&0x7F
def move_flag(m):        return (m>>14)&0xF

def move_is_capture(m):
    f=move_flag(m)
    return f==FLAG_CAPTURE or f==FLAG_EP or FLAG_PROMO_CN<=f<=FLAG_PROMO_CQ

def move_is_promo(m):
    f=move_flag(m); return FLAG_PROMO_N<=f<=FLAG_PROMO_CQ

def promo_type(flag):
    if flag in (FLAG_PROMO_N,FLAG_PROMO_CN): return KNIGHT
    if flag in (FLAG_PROMO_B,FLAG_PROMO_CB): return BISHOP
    if flag in (FLAG_PROMO_R,FLAG_PROMO_CR): return ROOK
    return QUEEN

def move_to_uci(m):
    fl=move_flag(m)
    s=sq_to_alg(move_from(m))+sq_to_alg(move_to(m))
    if FLAG_PROMO_N<=fl<=FLAG_PROMO_CQ: s+='nbrq'[promo_type(fl)-KNIGHT]
    return s

# ─────────────────────────────────────────────────────────────────────────────
# 4.  ZOBRIST KEYS
# ─────────────────────────────────────────────────────────────────────────────
random.seed(0xDEADBEEF)   # deterministic for reproducibility
def _r(): return random.getrandbits(32)
ZOB_PIECE  = [[[_r() for _ in range(128)] for _ in range(7)] for _ in range(2)]
ZOB_CASTLE = [_r() for _ in range(16)]
ZOB_EP     = [_r() for _ in range(8)]
ZOB_SIDE   = _r()

# ─────────────────────────────────────────────────────────────────────────────
# 5.  PeSTO PIECE-SQUARE TABLES
# ─────────────────────────────────────────────────────────────────────────────
MG_PAWN=[0,0,0,0,0,0,0,0,98,134,61,95,68,126,34,-11,-6,7,26,31,65,56,25,-20,
         -14,13,6,21,23,12,17,-23,-27,-2,-5,12,17,6,10,-25,-26,-4,-4,-10,3,3,33,-12,
         -35,-1,-20,-23,-15,24,38,-22,0,0,0,0,0,0,0,0]
EG_PAWN=[0,0,0,0,0,0,0,0,178,173,158,134,147,132,165,187,94,100,85,67,56,53,82,84,
         32,24,13,5,-2,4,17,17,13,9,-3,-7,-7,-8,3,-1,4,7,-16,-1,-1,-11,6,0,
         13,8,8,10,13,0,2,-13,0,0,0,0,0,0,0,0]
MG_KNIGHT=[-167,-89,-34,-49,61,-97,-15,-107,-73,-41,72,36,23,62,7,-17,
           -47,60,37,65,84,129,73,44,-9,17,19,53,37,69,18,22,
           -13,4,16,13,28,19,21,-8,-23,-9,12,10,19,17,25,-16,
           -29,-53,-12,-3,-1,18,-14,-19,-105,-21,-58,-33,-17,-28,-19,-23]
EG_KNIGHT=[-58,-38,-13,-28,-31,-27,-63,-99,-25,-8,-25,-2,-9,-25,-24,-52,
           -24,-20,10,9,-1,-9,-19,-41,-17,3,22,22,22,11,8,-18,
           -18,-6,16,25,16,17,4,-18,-23,-3,-1,15,10,-3,-20,-22,
           -42,-20,-10,-5,-2,-20,-23,-44,-29,-51,-23,-15,-22,-18,-50,-64]
MG_BISHOP=[-29,4,-82,-37,-25,-42,7,-8,-26,16,-18,-13,30,59,18,-47,
           -16,37,43,40,35,50,37,-2,-4,5,19,50,37,37,7,-2,
           -6,13,13,26,34,12,10,4,0,15,15,15,14,27,18,10,
           4,15,16,0,7,21,33,1,-33,-3,-14,-21,-13,-12,-39,-21]
EG_BISHOP=[-14,-21,-11,-8,-7,-9,-17,-24,-8,-4,7,-12,-3,-13,-4,-14,
           2,-8,0,-1,-2,6,0,4,-3,9,12,9,14,10,3,2,
           -6,3,13,19,7,10,-3,-9,-12,-3,8,10,13,3,-7,-15,
           -14,-18,-7,-1,4,-9,-15,-27,-23,-9,-23,-5,-9,-16,-5,-17]
MG_ROOK=[32,42,32,51,63,9,31,43,27,32,58,62,80,67,26,44,
         -5,19,26,36,17,45,61,16,-24,-11,7,26,24,35,-8,-20,
         -36,-26,-12,-1,9,-7,6,-23,-45,-25,-16,-17,3,0,-5,-33,
         -44,-16,-20,-9,-1,11,-6,-71,-19,-13,1,17,16,7,-37,-26]
EG_ROOK=[13,10,18,15,12,12,8,5,11,13,13,11,-3,3,8,3,
         7,7,7,5,4,-3,-5,-3,4,3,13,1,2,1,-1,2,
         3,5,8,4,-5,-6,-8,-11,-4,0,-5,-1,-7,-12,-8,-16,
         -6,-6,0,2,-9,-9,-11,-3,-9,2,3,-1,-5,-13,4,-20]
MG_QUEEN=[-28,0,29,12,59,44,43,45,-24,-39,-5,1,-16,57,28,54,
          -13,-17,7,8,29,56,47,57,-27,-27,-16,-16,-1,17,-2,1,
          -9,-26,-9,-10,-2,-4,3,-3,-14,2,-11,-2,-5,2,14,5,
          -35,-8,11,2,8,15,-3,1,-1,-18,-9,10,-15,-25,-31,-50]
EG_QUEEN=[-9,22,22,27,27,19,10,20,-17,20,32,41,58,25,30,0,
          -20,6,9,49,47,35,19,9,3,22,24,45,57,40,57,36,
          -18,28,19,47,31,34,39,23,-16,-27,15,6,9,17,10,5,
          -22,-23,-30,-16,-16,-23,-36,-32,-33,-28,-22,-43,-5,-32,-20,-41]
MG_KING=[-65,23,16,-15,-56,-34,2,13,29,-1,-20,-7,-8,-4,-38,-29,
         -9,24,2,-16,-20,6,22,-22,-17,-20,-12,-27,-30,-25,-14,-36,
         -49,-1,-27,-39,-46,-44,-33,-51,-14,-14,-22,-46,-44,-30,-15,-27,
         1,7,-8,-64,-43,-16,9,8,-15,36,12,-54,8,-28,24,14]
EG_KING=[-74,-35,-18,-18,-11,15,4,-17,-12,17,14,17,17,38,23,11,
         10,17,23,15,20,45,44,13,-8,22,24,27,26,33,26,3,
         -18,-4,21,24,27,23,9,-11,-19,-3,11,21,23,16,7,-9,
         -27,-11,4,13,14,4,-5,-17,-53,-34,-21,-11,-28,-14,-24,-43]

MG_TABLE=[None,MG_PAWN,MG_KNIGHT,MG_BISHOP,MG_ROOK,MG_QUEEN,MG_KING]
EG_TABLE=[None,EG_PAWN,EG_KNIGHT,EG_BISHOP,EG_ROOK,EG_QUEEN,EG_KING]
MG_VAL=[0,82,337,365,477,1025,0]
EG_VAL=[0,94,281,297,512,936,0]
PIECE_VALUE=[0,100,320,330,500,900,20000]
PHASE_INC=[0,0,1,1,2,4,0]   # per piece type

def _pst_idx(s, color):
    r=s>>4; f=s&7
    row=(7-r) if color==WHITE else r
    return row*8+f

# ─────────────────────────────────────────────────────────────────────────────
# 6.  GLOBAL BOARD STATE
# ─────────────────────────────────────────────────────────────────────────────
board        = array('b',[0]*128)
castling     = [0]
ep_square    = [-1]
side_to_move = [WHITE]
half_move    = [0]
full_move    = [1]
hash_key     = [0]
king_pos     = [-1,-1]
undo_stack   = []

# ── Ring buffer for repetition detection  (replaces O(n) undo_stack scan) ──
# Every make_move pushes hash_key into rep_hist; unmake pops it.
# _is_repetition() only needs to scan back halfMove entries — O(1) in practice.
REP_SIZE = 1024
rep_hist = array('l', [0] * REP_SIZE)   # 'l' = signed 64-bit, handles full hash range
rep_head = [0]

# ── Incremental eval accumulators (FIX E) ──────────────────────────────────
# mg_score[c] / eg_score[c] = material+PST sum for colour c
# phase = sum of PHASE_INC for all pieces (0=endgame, 24=opening)
mg_score     = [0, 0]
eg_score     = [0, 0]
phase        = [0]
bishop_count = [0, 0]   # for bishop-pair bonus

def _add_piece(sq_idx, p):
    """Called when a piece appears on the board."""
    c=piece_color(p); t=piece_type(p)
    idx=_pst_idx(sq_idx,c)
    mg_score[c]+=MG_VAL[t]+MG_TABLE[t][idx]
    eg_score[c]+=EG_VAL[t]+EG_TABLE[t][idx]
    phase[0]+=PHASE_INC[t]
    if t==BISHOP: bishop_count[c]+=1

def _remove_piece(sq_idx, p):
    """Called when a piece disappears from the board."""
    c=piece_color(p); t=piece_type(p)
    idx=_pst_idx(sq_idx,c)
    mg_score[c]-=MG_VAL[t]+MG_TABLE[t][idx]
    eg_score[c]-=EG_VAL[t]+EG_TABLE[t][idx]
    phase[0]-=PHASE_INC[t]
    if t==BISHOP: bishop_count[c]-=1

# ── Pawn structure cache ────────────────────────────────────────────────────
# Keyed by (pawn positions hash), cached value = pawn_mg, pawn_eg score
_pawn_cache = {}
_pawn_hash  = [0]     # XOR of ZOB_PIECE values for pawns only

def clear_board():
    for i in range(128): board[i]=0
    castling[0]=0; ep_square[0]=-1; side_to_move[0]=WHITE
    half_move[0]=0; full_move[0]=1; hash_key[0]=0
    king_pos[WHITE]=-1; king_pos[BLACK]=-1
    undo_stack.clear()
    mg_score[0]=mg_score[1]=0
    eg_score[0]=eg_score[1]=0
    phase[0]=0; bishop_count[0]=bishop_count[1]=0
    _pawn_hash[0]=0
    rep_head[0]=0

_PIECE_MAP={'p':PAWN,'n':KNIGHT,'b':BISHOP,'r':ROOK,'q':QUEEN,'k':KING}

def parse_fen(fen):
    clear_board()
    parts=fen.strip().split()
    for r,row in enumerate(parts[0].split('/')):
        f=0
        for c in row:
            if c.isdigit(): f+=int(c)
            else:
                color=WHITE if c.isupper() else BLACK
                ptype=_PIECE_MAP[c.lower()]
                s=sq(7-r,f)
                p=piece(color,ptype)
                board[s]=p
                hash_key[0]^=ZOB_PIECE[color][ptype][s]
                _add_piece(s,p)
                if ptype==KING: king_pos[color]=s
                if ptype==PAWN: _pawn_hash[0]^=ZOB_PIECE[color][PAWN][s]
                f+=1
    side_to_move[0]=WHITE if parts[1]=='w' else BLACK
    if side_to_move[0]==BLACK: hash_key[0]^=ZOB_SIDE
    cr=parts[2] if len(parts)>2 else '-'
    cr_bits=(('K'in cr)|(('Q'in cr)<<1)|(('k'in cr)<<2)|(('q'in cr)<<3))
    castling[0]=cr_bits; hash_key[0]^=ZOB_CASTLE[cr_bits]
    if len(parts)>3 and parts[3]!='-':
        ep_square[0]=alg_to_sq(parts[3])
        hash_key[0]^=ZOB_EP[ep_square[0]&7]
    half_move[0]=int(parts[4]) if len(parts)>4 else 0
    full_move[0]=int(parts[5]) if len(parts)>5 else 1

# ─────────────────────────────────────────────────────────────────────────────
# 7.  ATTACK DETECTION  (0x88 ray tracing)
# ─────────────────────────────────────────────────────────────────────────────
KNIGHT_OFFSETS=(-33,-31,-18,-14,14,18,31,33)
BISHOP_OFFSETS=(-17,-15,15,17)
ROOK_OFFSETS=(-16,-1,1,16)
KING_OFFSETS=(-17,-16,-15,-1,1,15,16,17)

def is_attacked(s, by_color):
    # Pawn attacks
    p_dir=-1 if by_color==WHITE else 1
    wp=piece(by_color,PAWN)
    fl=s+p_dir*16-1; fr=s+p_dir*16+1
    if (fl&0x88)==0 and board[fl]==wp: return True
    if (fr&0x88)==0 and board[fr]==wp: return True
    # Knight
    wn=piece(by_color,KNIGHT)
    for d in KNIGHT_OFFSETS:
        t=s+d
        if (t&0x88)==0 and board[t]==wn: return True
    # Diagonals
    wb=piece(by_color,BISHOP); wq=piece(by_color,QUEEN)
    for d in BISHOP_OFFSETS:
        t=s+d
        while (t&0x88)==0:
            bt=board[t]
            if bt:
                if bt==wb or bt==wq: return True
                break
            t+=d
    # Orthogonals
    wr=piece(by_color,ROOK)
    for d in ROOK_OFFSETS:
        t=s+d
        while (t&0x88)==0:
            bt=board[t]
            if bt:
                if bt==wr or bt==wq: return True
                break
            t+=d
    # King
    wk=piece(by_color,KING)
    for d in KING_OFFSETS:
        t=s+d
        if (t&0x88)==0 and board[t]==wk: return True
    return False

# ─────────────────────────────────────────────────────────────────────────────
# 8.  MAKE / UNMAKE  (FIX E: maintain incremental eval counters)
# ─────────────────────────────────────────────────────────────────────────────
def make_move(m):
    fsq=move_from(m); tsq=move_to(m); flag=move_flag(m)
    moving=board[fsq]; captured=board[tsq]
    mtype=piece_type(moving); mcolor=piece_color(moving)

    undo_stack.append((
        fsq,tsq,flag,moving,captured,
        castling[0],ep_square[0],half_move[0],hash_key[0],
        king_pos[WHITE],king_pos[BLACK],
        mg_score[0],mg_score[1],eg_score[0],eg_score[1],
        phase[0],bishop_count[0],bishop_count[1],_pawn_hash[0]
    ))

    # Incremental eval: remove moving piece from source
    _remove_piece(fsq,moving)
    hash_key[0]^=ZOB_PIECE[mcolor][mtype][fsq]
    if mtype==PAWN: _pawn_hash[0]^=ZOB_PIECE[mcolor][PAWN][fsq]

    # Remove captured piece
    if captured:
        ct=piece_type(captured); cc=piece_color(captured)
        _remove_piece(tsq,captured)
        hash_key[0]^=ZOB_PIECE[cc][ct][tsq]
        if ct==PAWN: _pawn_hash[0]^=ZOB_PIECE[cc][PAWN][tsq]

    hash_key[0]^=ZOB_CASTLE[castling[0]]
    if ep_square[0]>=0: hash_key[0]^=ZOB_EP[ep_square[0]&7]

    board[fsq]=EMPTY; board[tsq]=moving
    ep_square[0]=-1; half_move[0]+=1

    if mtype==PAWN or captured or flag>=FLAG_EP:
        half_move[0]=0

    if flag==FLAG_EP:
        ep_cap=tsq+(-16 if mcolor==WHITE else 16)
        cap_p=board[ep_cap]
        _remove_piece(ep_cap,cap_p)
        hash_key[0]^=ZOB_PIECE[1-mcolor][PAWN][ep_cap]
        _pawn_hash[0]^=ZOB_PIECE[1-mcolor][PAWN][ep_cap]
        board[ep_cap]=EMPTY

    if flag==FLAG_DPUSH:
        ep_square[0]=fsq+(16 if mcolor==WHITE else -16)
        hash_key[0]^=ZOB_EP[ep_square[0]&7]

    if flag in (FLAG_CASTLE_K,FLAG_CASTLE_Q):
        if flag==FLAG_CASTLE_K:
            rf=sq(0,7) if mcolor==WHITE else sq(7,7)
            rt=sq(0,5) if mcolor==WHITE else sq(7,5)
        else:
            rf=sq(0,0) if mcolor==WHITE else sq(7,0)
            rt=sq(0,3) if mcolor==WHITE else sq(7,3)
        rook=board[rf]
        _remove_piece(rf,rook); hash_key[0]^=ZOB_PIECE[mcolor][ROOK][rf]
        board[rf]=EMPTY; board[rt]=rook
        _add_piece(rt,rook);    hash_key[0]^=ZOB_PIECE[mcolor][ROOK][rt]

    if FLAG_PROMO_N<=flag<=FLAG_PROMO_CQ:
        pt=promo_type(flag)
        new_p=piece(mcolor,pt)
        board[tsq]=new_p
        _add_piece(tsq,new_p); hash_key[0]^=ZOB_PIECE[mcolor][pt][tsq]
    else:
        _add_piece(tsq,moving); hash_key[0]^=ZOB_PIECE[mcolor][mtype][tsq]
        if mtype==PAWN: _pawn_hash[0]^=ZOB_PIECE[mcolor][PAWN][tsq]

    if mtype==KING: king_pos[mcolor]=tsq

    cr=castling[0]
    if fsq==sq(0,4) or fsq==sq(0,0) or tsq==sq(0,0): cr&=~2
    if fsq==sq(0,4) or fsq==sq(0,7) or tsq==sq(0,7): cr&=~1
    if fsq==sq(7,4) or fsq==sq(7,0) or tsq==sq(7,0): cr&=~8
    if fsq==sq(7,4) or fsq==sq(7,7) or tsq==sq(7,7): cr&=~4
    castling[0]=cr; hash_key[0]^=ZOB_CASTLE[cr]
    side_to_move[0]^=1; hash_key[0]^=ZOB_SIDE
    if side_to_move[0]==WHITE: full_move[0]+=1
    # Push resulting hash into ring buffer for repetition detection
    rep_hist[rep_head[0] & (REP_SIZE-1)] = hash_key[0]
    rep_head[0] += 1

def unmake_move():
    (fsq,tsq,flag,moving,captured,
     prev_cast,prev_ep,prev_half,prev_hash,
     kw,kb,
     mg0,mg1,eg0,eg1,ph,bc0,bc1,ph2)=undo_stack.pop()
    side_to_move[0]^=1
    if side_to_move[0]==BLACK: full_move[0]-=1
    rep_head[0] -= 1   # pop ring buffer
    mcolor=piece_color(moving)

    # Restore incremental accumulators in O(1)
    mg_score[0]=mg0; mg_score[1]=mg1
    eg_score[0]=eg0; eg_score[1]=eg1
    phase[0]=ph; bishop_count[0]=bc0; bishop_count[1]=bc1
    _pawn_hash[0]=ph2

    board[fsq]=moving; board[tsq]=captured
    if flag==FLAG_EP:
        ep_cap=tsq+(-16 if mcolor==WHITE else 16)
        board[ep_cap]=piece(1-mcolor,PAWN)
    if flag in (FLAG_CASTLE_K,FLAG_CASTLE_Q):
        if flag==FLAG_CASTLE_K:
            rf=sq(0,7) if mcolor==WHITE else sq(7,7)
            rt=sq(0,5) if mcolor==WHITE else sq(7,5)
        else:
            rf=sq(0,0) if mcolor==WHITE else sq(7,0)
            rt=sq(0,3) if mcolor==WHITE else sq(7,3)
        board[rf]=board[rt]; board[rt]=EMPTY
    castling[0]=prev_cast; ep_square[0]=prev_ep
    half_move[0]=prev_half; hash_key[0]=prev_hash
    king_pos[WHITE]=kw; king_pos[BLACK]=kb

# ─────────────────────────────────────────────────────────────────────────────
# 9.  MOVE GENERATION
# ─────────────────────────────────────────────────────────────────────────────
_move_list  = array('i',[0]*4096)
_move_count = [0]

def gen_moves(color):
    opp=1-color
    pawn_dir=1 if color==WHITE else -1
    start_rank=1 if color==WHITE else 6
    promo_rank=7 if color==WHITE else 0
    mc=0

    for s in range(128):
        if s&0x88: continue
        p=board[s]
        if not p or (p>>3)!=color: continue
        pt=p&7

        if pt==PAWN:
            fwd=s+pawn_dir*16
            if not (fwd&0x88) and not board[fwd]:
                to_r=fwd>>4
                if to_r==promo_rank:
                    _move_list[mc]=enc_move(s,fwd,FLAG_PROMO_Q); mc+=1
                    _move_list[mc]=enc_move(s,fwd,FLAG_PROMO_R); mc+=1
                    _move_list[mc]=enc_move(s,fwd,FLAG_PROMO_B); mc+=1
                    _move_list[mc]=enc_move(s,fwd,FLAG_PROMO_N); mc+=1
                else:
                    _move_list[mc]=enc_move(s,fwd,FLAG_QUIET); mc+=1
                    if (s>>4)==start_rank:
                        dbl=fwd+pawn_dir*16
                        if not board[dbl]:
                            _move_list[mc]=enc_move(s,dbl,FLAG_DPUSH); mc+=1
            for df in (-1,1):
                t=fwd+df
                if t&0x88: continue
                to_r=t>>4
                if board[t] and (board[t]>>3)==opp:
                    if to_r==promo_rank:
                        _move_list[mc]=enc_move(s,t,FLAG_PROMO_CQ); mc+=1
                        _move_list[mc]=enc_move(s,t,FLAG_PROMO_CR); mc+=1
                        _move_list[mc]=enc_move(s,t,FLAG_PROMO_CB); mc+=1
                        _move_list[mc]=enc_move(s,t,FLAG_PROMO_CN); mc+=1
                    else:
                        _move_list[mc]=enc_move(s,t,FLAG_CAPTURE); mc+=1
                elif ep_square[0]>=0 and t==ep_square[0]:
                    _move_list[mc]=enc_move(s,t,FLAG_EP); mc+=1

        elif pt==KNIGHT:
            for d in KNIGHT_OFFSETS:
                t=s+d
                if t&0x88: continue
                bt=board[t]
                if not bt: _move_list[mc]=enc_move(s,t,FLAG_QUIET); mc+=1
                elif (bt>>3)==opp: _move_list[mc]=enc_move(s,t,FLAG_CAPTURE); mc+=1

        if pt==BISHOP or pt==QUEEN:
            for d in BISHOP_OFFSETS:
                t=s+d
                while not (t&0x88):
                    bt=board[t]
                    if not bt: _move_list[mc]=enc_move(s,t,FLAG_QUIET); mc+=1
                    else:
                        if (bt>>3)==opp: _move_list[mc]=enc_move(s,t,FLAG_CAPTURE); mc+=1
                        break
                    t+=d

        if pt==ROOK or pt==QUEEN:
            for d in ROOK_OFFSETS:
                t=s+d
                while not (t&0x88):
                    bt=board[t]
                    if not bt: _move_list[mc]=enc_move(s,t,FLAG_QUIET); mc+=1
                    else:
                        if (bt>>3)==opp: _move_list[mc]=enc_move(s,t,FLAG_CAPTURE); mc+=1
                        break
                    t+=d

        if pt==KING:
            for d in KING_OFFSETS:
                t=s+d
                if t&0x88: continue
                bt=board[t]
                if not bt: _move_list[mc]=enc_move(s,t,FLAG_QUIET); mc+=1
                elif (bt>>3)==opp: _move_list[mc]=enc_move(s,t,FLAG_CAPTURE); mc+=1
            cr=castling[0]
            if color==WHITE and s==sq(0,4):
                if (cr&1) and not board[sq(0,5)] and not board[sq(0,6)] and \
                   not is_attacked(sq(0,4),opp) and not is_attacked(sq(0,5),opp) and \
                   not is_attacked(sq(0,6),opp):
                    _move_list[mc]=enc_move(s,sq(0,6),FLAG_CASTLE_K); mc+=1
                if (cr&2) and not board[sq(0,3)] and not board[sq(0,2)] and \
                   not board[sq(0,1)] and \
                   not is_attacked(sq(0,4),opp) and not is_attacked(sq(0,3),opp) and \
                   not is_attacked(sq(0,2),opp):
                    _move_list[mc]=enc_move(s,sq(0,2),FLAG_CASTLE_Q); mc+=1
            elif color==BLACK and s==sq(7,4):
                if (cr&4) and not board[sq(7,5)] and not board[sq(7,6)] and \
                   not is_attacked(sq(7,4),opp) and not is_attacked(sq(7,5),opp) and \
                   not is_attacked(sq(7,6),opp):
                    _move_list[mc]=enc_move(s,sq(7,6),FLAG_CASTLE_K); mc+=1
                if (cr&8) and not board[sq(7,3)] and not board[sq(7,2)] and \
                   not board[sq(7,1)] and \
                   not is_attacked(sq(7,4),opp) and not is_attacked(sq(7,3),opp) and \
                   not is_attacked(sq(7,2),opp):
                    _move_list[mc]=enc_move(s,sq(7,2),FLAG_CASTLE_Q); mc+=1

    _move_count[0]=mc
    return mc

def is_in_check(color):
    return is_attacked(king_pos[color],1-color)

def legal_moves(color):
    gen_moves(color)
    legal=[]
    for i in range(_move_count[0]):
        m=_move_list[i]
        make_move(m)
        if not is_attacked(king_pos[color],1-color):
            legal.append(m)
        unmake_move()
    return legal

# ─────────────────────────────────────────────────────────────────────────────
# 10.  EVALUATION  (O(1) tapered eval + pawn cache)  [FIX E]
# ─────────────────────────────────────────────────────────────────────────────
PHASE_MAX=24

def _king_safety_fast(king_s, color):
    direction=1 if color==WHITE else -1
    shield=0; r=king_s>>4; f=king_s&7
    wp=piece(color,PAWN)
    for df in (-1,0,1):
        ff=f+df
        if 0<=ff<=7:
            front=sq(r+direction,ff)
            if not (front&0x88) and board[front]==wp: shield+=15
            else: shield-=10
    return shield

def _pawn_structure():
    """Compute pawn structure score (mg, eg). Cached by pawn hash."""
    ph=_pawn_hash[0]
    if ph in _pawn_cache:
        return _pawn_cache[ph]

    w_pf=[0]*8; b_pf=[0]*8
    w_pawn_sq=[]; b_pawn_sq=[]

    for s in range(128):
        if s&0x88: continue
        p=board[s]
        if not p: continue
        t=p&7
        if t!=PAWN: continue
        c=p>>3; f=s&7; r=s>>4
        if c==WHITE: w_pf[f]+=1; w_pawn_sq.append((r,f))
        else:        b_pf[f]+=1; b_pawn_sq.append((r,f))

    mg=0; eg=0
    # Bishop pair
    if bishop_count[WHITE]>=2: mg+=30; eg+=30
    if bishop_count[BLACK]>=2: mg-=30; eg-=30
    # Doubled/isolated
    for f in range(8):
        if w_pf[f]>1: mg-=15*(w_pf[f]-1); eg-=20*(w_pf[f]-1)
        if b_pf[f]>1: mg+=15*(b_pf[f]-1); eg+=20*(b_pf[f]-1)
        isolated_w=(f==0 or w_pf[f-1]==0) and (f==7 or w_pf[f+1]==0)
        isolated_b=(f==0 or b_pf[f-1]==0) and (f==7 or b_pf[f+1]==0)
        if w_pf[f]>0 and isolated_w: mg-=20; eg-=25
        if b_pf[f]>0 and isolated_b: mg+=20; eg+=25
    # Passed pawns — O(n) using per-file max-rank tracking
    # White pawn at (r,f) is passed if no black pawn exists at rank > r on files f-1..f+1
    # Black pawn at (r,f) is passed if no white pawn exists at rank < r on files f-1..f+1
    # Precompute: highest-rank black pawn per file (lowest rank = closest to white's side)
    b_max_rank_on_file = [-1]*8   # max rank index of any black pawn on that file
    w_min_rank_on_file = [8]*8    # min rank index of any white pawn on that file
    for r2,f2 in b_pawn_sq: b_max_rank_on_file[f2]=max(b_max_rank_on_file[f2],r2)
    for r2,f2 in w_pawn_sq: w_min_rank_on_file[f2]=min(w_min_rank_on_file[f2],r2)

    for r,f in w_pawn_sq:
        fl=max(0,f-1); fh=min(7,f+1)
        # Passed if no black pawn on adjacent+own files is at rank > r
        passed=all(b_max_rank_on_file[ff]<=r for ff in range(fl,fh+1))
        if passed: bonus=20+(r-1)*15; mg+=bonus; eg+=bonus*2

    for r,f in b_pawn_sq:
        fl=max(0,f-1); fh=min(7,f+1)
        # Passed if no white pawn on adjacent+own files is at rank < r
        passed=all(w_min_rank_on_file[ff]>=r for ff in range(fl,fh+1))
        if passed: bonus=20+(6-r)*15; mg-=bonus; eg-=bonus*2

    # Cap cache size
    if len(_pawn_cache)>50000: _pawn_cache.clear()
    _pawn_cache[ph]=(mg,eg)
    return mg,eg

def evaluate():
    """O(1) tapered evaluation using incremental accumulators."""
    mg=(mg_score[WHITE]-mg_score[BLACK])
    eg=(eg_score[WHITE]-eg_score[BLACK])

    pmg,peg=_pawn_structure()
    mg+=pmg; eg+=peg

    ph=min(phase[0],PHASE_MAX)
    # King safety only in middlegame
    if ph>6:
        mg+=_king_safety_fast(king_pos[WHITE],WHITE)
        mg-=_king_safety_fast(king_pos[BLACK],BLACK)

    score=(mg*ph+eg*(PHASE_MAX-ph))//PHASE_MAX
    return score if side_to_move[0]==WHITE else -score

# ─────────────────────────────────────────────────────────────────────────────
# 11.  SEE  (corrected swap algorithm)  [FIX A, FIX D]
# ─────────────────────────────────────────────────────────────────────────────
_SEE_VALS=[0,100,320,330,500,900,20000]

def _lsb_attacker_see(to_sq, by_color, removed):
    """
    Least-valuable attacker of to_sq by by_color.
    `removed` is a set of squares whose pieces have been exchanged away.
    Returns (piece_type, from_sq) or (0, -1).
    """
    p_dir=-1 if by_color==WHITE else 1
    wp=piece(by_color,PAWN)
    for df in (-1,1):
        fr=to_sq+p_dir*16+df
        if not (fr&0x88) and fr not in removed and board[fr]==wp:
            return PAWN,fr
    wn=piece(by_color,KNIGHT)
    for d in KNIGHT_OFFSETS:
        fr=to_sq+d
        if not (fr&0x88) and fr not in removed and board[fr]==wn:
            return KNIGHT,fr
    wb=piece(by_color,BISHOP); wq=piece(by_color,QUEEN)
    for d in BISHOP_OFFSETS:
        t=to_sq+d
        while not (t&0x88):
            if t not in removed:
                p=board[t]
                if p==wb or p==wq: return (BISHOP if p==wb else QUEEN),t
                if p: break   # blocker
            t+=d
    wr=piece(by_color,ROOK)
    for d in ROOK_OFFSETS:
        t=to_sq+d
        while not (t&0x88):
            if t not in removed:
                p=board[t]
                if p==wr or p==wq: return (ROOK if p==wr else QUEEN),t
                if p: break
            t+=d
    wk=piece(by_color,KING)
    for d in KING_OFFSETS:
        fr=to_sq+d
        if not (fr&0x88) and fr not in removed and board[fr]==wk:
            return KING,fr
    return 0,-1

def see(move) -> int:
    """
    Correct Knuth/Heinz swap-list SEE.
    Uses a small `removed` set instead of scanning 128 squares to build occupancy.
    [FIX A: gain formula corrected]  [PERF: no 128-sq scan on every call]
    """
    tsq=move_to(move); fsq=move_from(move); flag=move_flag(move)
    if flag==FLAG_EP: return _SEE_VALS[PAWN]
    target_val=_SEE_VALS[board[tsq]&7]
    if not target_val: return 0

    # Track removed squares in a small set (avoids 128-sq occupancy rebuild)
    removed={fsq}

    gain=[0]*32
    gain[0]=target_val
    piece_on_sq=_SEE_VALS[board[fsq]&7]
    color=board[fsq]>>3
    d=1

    while True:
        color^=1
        pt,fr=_lsb_attacker_see(tsq,color,removed)
        if fr<0: break
        gain[d]=piece_on_sq-gain[d-1]
        if max(-gain[d-1],gain[d])<0: break
        piece_on_sq=_SEE_VALS[pt]
        removed.add(fr)
        d+=1

    while d>1:
        d-=1
        gain[d-1]=-max(-gain[d-1],gain[d])
    return gain[0]

# ── Startup assertion: verify SEE on a known position ──────────────────────
def _assert_see():
    parse_fen('3r4/8/8/3p4/8/8/8/3R4 w - - 0 1')
    gen_moves(WHITE)
    for i in range(_move_count[0]):
        m=_move_list[i]
        if move_to_uci(m)=='d1d5':
            v=see(m)
            assert v<0, f"SEE self-test failed: rook×defended-pawn expected <0, got {v}"
            return
    assert False,"SEE self-test: move d1d5 not found"

# ─────────────────────────────────────────────────────────────────────────────
# 12.  TRANSPOSITION TABLE  (two-bucket, generation-aware)  [FIX H]
# ─────────────────────────────────────────────────────────────────────────────
# Two buckets per logical slot:
#   Bucket 0 (always-replace): always overwritten → holds newest entry
#   Bucket 1 (depth-preferred): only overwritten when deeper or older generation
# This eliminates the beta-preferring bias from the previous single-bucket scheme.
TT_SIZE=1<<22           # 4 M logical slots
TT_MASK=TT_SIZE-1
TT_EXACT=0; TT_ALPHA=1; TT_BETA=2

# Arrays sized 2×TT_SIZE for two-bucket layout
_TT2=TT_SIZE*2
tt_key   =array('l',[0]*_TT2)
tt_score =array('i',[0]*_TT2)
tt_depth =array('b',[0]*_TT2)
tt_flag  =array('b',[0]*_TT2)
tt_move  =array('i',[0]*_TT2)
tt_gen   =array('b',[0]*_TT2)

_tt_generation=[0]
_HASH_M63=0x7FFFFFFFFFFFFFFF

def tt_new_search():
    _tt_generation[0]=(_tt_generation[0]+1)&127

def tt_store(h, depth, score, flag, move):
    base=(h&TT_MASK)*2
    hk=h&_HASH_M63
    g=_tt_generation[0]
    # Bucket 0: always-replace (freshest info, helps with aspiration re-searches)
    tt_key[base]=hk; tt_score[base]=score; tt_depth[base]=depth
    tt_flag[base]=flag; tt_move[base]=move; tt_gen[base]=g
    # Bucket 1: depth-preferred (only replace if deeper or stale)
    b1=base+1
    if tt_key[b1]!=hk or depth>=tt_depth[b1] or tt_gen[b1]!=g:
        tt_key[b1]=hk; tt_score[b1]=score; tt_depth[b1]=depth
        tt_flag[b1]=flag; tt_move[b1]=move; tt_gen[b1]=g

def tt_probe(h, depth):
    """
    Returns (score, flag, move, depth_stored) or None.
    Prefers depth-preferred bucket; falls back to always-replace bucket.
    [FIX H: two-bucket scheme, no beta bias]
    """
    base=(h&TT_MASK)*2
    hk=h&_HASH_M63
    for b in (base+1, base):   # depth-preferred first
        if tt_key[b]==hk:
            return tt_score[b],tt_flag[b],tt_move[b],tt_depth[b]
    return None

def tt_best_move(h):
    base=(h&TT_MASK)*2; hk=h&_HASH_M63
    for b in (base+1,base):
        if tt_key[b]==hk and tt_move[b]:
            return tt_move[b]
    return 0

# ─────────────────────────────────────────────────────────────────────────────
# 13.  MOVE ORDERING  (killers / history / continuation / counter)
# ─────────────────────────────────────────────────────────────────────────────
MVV_LVA=[0]*(7*7)
for _v in range(1,7):
    for _a in range(1,7):
        MVV_LVA[_v*7+_a]=PIECE_VALUE[_v]*10-PIECE_VALUE[_a]

MAX_PLY=128
killers=[0]*(MAX_PLY*2)
hist_heuristic=[0]*(2*128*128)
counter_move=[0]*(128*128)
cont_hist_1=[0]*(128*128)
cont_hist_2=[0]*(128*128)

def _hist_idx(color,fsq,tsq): return (color*128+fsq)*128+tsq
def _cont_idx(fsq,tsq):       return (fsq&127)*128+(tsq&127)

def update_history(color,fsq,tsq,bonus):
    i=_hist_idx(color,fsq,tsq)
    hist_heuristic[i]+=bonus
    if hist_heuristic[i]>1_000_000:
        for j in range(len(hist_heuristic)): hist_heuristic[j]>>=1

def update_cont(pv,cv,bonus):
    if pv:
        i=_cont_idx(move_from(pv),move_to(pv))
        cont_hist_1[i]=min(cont_hist_1[i]+bonus,500_000)

def _score_move(m, tt_best, ply, prev_move, prev2_move):
    if m==tt_best: return 2_000_000
    flag=move_flag(m)
    if move_is_capture(m):
        vic=board[move_to(m)]&7; atk=board[move_from(m)]&7
        if not vic and flag==FLAG_EP: vic=PAWN
        # Fast MVV-LVA: no SEE call — avoids 128-sq scan per capture per node.
        # Good captures (victim >= attacker value): tier 1
        # Bad captures (victim < attacker):         tier 2 (still above killers)
        mvvlva=MVV_LVA[vic*7+atk]
        if _SEE_VALS[vic]>=_SEE_VALS[atk]:
            return 1_500_000+mvvlva
        return 700_000+mvvlva   # likely losing but may still be searched
    if move_is_promo(m): return 950_000
    if killers[ply*2]==m:     return 900_000
    if killers[ply*2+1]==m:   return 899_000
    if prev_move:
        cm=counter_move[(move_from(prev_move)&127)*128+(move_to(prev_move)&127)]
        if cm==m: return 850_000
    h=hist_heuristic[_hist_idx(side_to_move[0],move_from(m),move_to(m))]
    ch1=cont_hist_1[_cont_idx(move_from(prev_move),move_to(prev_move))] if prev_move else 0
    ch2=cont_hist_2[_cont_idx(move_from(prev2_move),move_to(prev2_move))] if prev2_move else 0
    return h+ch1+ch2

def sort_moves(moves,tt_best,ply,prev_move,prev2_move=0):
    moves.sort(key=lambda m:-_score_move(m,tt_best,ply,prev_move,prev2_move))

# ─────────────────────────────────────────────────────────────────────────────
# 14.  LMR TABLE  (log-formula)
# ─────────────────────────────────────────────────────────────────────────────
LMR_TABLE=[[0]*64 for _ in range(64)]
for _d in range(1,64):
    for _i in range(1,64):
        LMR_TABLE[_d][_i]=max(1,int(0.75+math.log(_d)*math.log(_i)/2.25))

# ─────────────────────────────────────────────────────────────────────────────
# 15.  OPENING BOOK  (self-contained, no external file)  [FIX F]
# ─────────────────────────────────────────────────────────────────────────────
OPENING_BOOK={
    # e4 lines
    'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq':['e2e4','d2d4','c2c4','g1f3'],
    'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq':['e7e5','c7c5','e7e6','c7c6'],
    'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq':['g1f3','f1c4','f1b5','d2d4'],
    'rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq':['b8c6','g8f6','d7d6'],
    'r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq':['f1b5','f1c4','d2d4'],
    # Ruy Lopez
    'r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq':['a7a6','g8f6','f8c5'],
    'r1bqkbnr/1ppp1ppp/p1n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq':['f1b5','b5a4','d2d4'],
    # Italian
    'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq':['d2d3','c2c3','g1g5'],
    # Sicilian
    'rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq':['g1f3','b1c3','f2f4'],
    'rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq':['d7d6','b8c6','e7e6'],
    'rnbqkbnr/pp2pppp/3p4/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq':['d2d4','f1b5'],
    'rnbqkbnr/pp2pppp/3p4/2p5/3PP3/5N2/PPP2PPP/RNBQKB1R b KQkq':['c5d4','g8f6','a7a6'],
    # French
    'rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq':['d2d4','b1c3','g1f3'],
    'rnbqkbnr/pppp1ppp/4p3/8/3PP3/8/PPP2PPP/RNBQKBNR b KQkq':['d7d5','b8c6','g8f6'],
    # Caro-Kann
    'rnbqkbnr/pp1ppppp/2p5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq':['d2d4','b1c3','g1f3'],
    'rnbqkbnr/pp1ppppp/2p5/8/3PP3/8/PPP2PPP/RNBQKBNR b KQkq':['d7d5','g8f6'],
    # d4 lines
    'rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq':['d7d5','g8f6','e7e6','c7c5'],
    'rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR w KQkq':['c2c4','g1f3','e2e3'],
    'rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq':['e7e6','c7c6','g8f6','d5c4'],
    # QGD
    'rnbqkb1r/ppp1pppp/5n2/3p4/2PP4/8/PP2PPPP/RNBQKBNR w KQkq':['b1c3','g1f3','c1g5'],
    # King\'s Indian
    'rnbqkb1r/pppppp1p/5np1/8/2PP4/8/PP2PPPP/RNBQKBNR w KQkq':['b1c3','g1f3','e2e4'],
    # English
    'rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR b KQkq':['c7c5','e7e5','g8f6','e7e6'],
    # Reti
    'rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq':['d7d5','g8f6','e7e6'],
    'rnbqkb1r/pppp1ppp/5n2/4p3/2B1P3/8/PPPP1PPP/RNBQK1NR w KQkq':['g1f3','d2d3','c2c3'],
}

def get_book_move(fen):
    key=' '.join(fen.strip().split()[:3])
    moves=OPENING_BOOK.get(key)
    if moves: return random.choice(moves)
    return None

# ─────────────────────────────────────────────────────────────────────────────
# 16.  LEARNING  (win/draw/loss stats)
# ─────────────────────────────────────────────────────────────────────────────
_LEARN_FILE=os.path.join(os.path.dirname(os.path.abspath(__file__)),'tt_learn.json')
_learn_db={}
_session_pos=[]

def _learn_load():
    global _learn_db
    try:
        if os.path.exists(_LEARN_FILE):
            with open(_LEARN_FILE) as f: _learn_db=json.load(f)
    except Exception: _learn_db={}

def _learn_save():
    try:
        with open(_LEARN_FILE,'w') as f: json.dump(_learn_db,f,separators=(',',':'))
    except Exception: pass

def learn_record_move(fen,uci_move):
    key=' '.join(fen.strip().split()[:4])
    _session_pos.append((key,uci_move))

def learn_commit(result_char):
    if result_char not in ('w','d','l'): return
    idx={'w':0,'d':1,'l':2}[result_char]
    for key,uci in _session_pos:
        _learn_db.setdefault(key,{}).setdefault(uci,[0,0,0])[idx]+=1
    _session_pos.clear(); _learn_save()

def _learn_score(fen,uci_move):
    key=' '.join(fen.strip().split()[:4])
    rec=_learn_db.get(key,{}).get(uci_move)
    if not rec: return 0
    w,d,l=rec; total=w+d+l
    return int((w-l)*100/(total+1)) if total else 0

# ─────────────────────────────────────────────────────────────────────────────
# 17.  SEARCH
# ─────────────────────────────────────────────────────────────────────────────
INF=999_999; MATE=900_000

# ── Dynamic asymmetric contempt (draw-or-win priority) ───────────────────────
# Winning → positive contempt → engine fights on, refuses draws
# Losing  → negative contempt → engine VALUES draws, seeks repetition
CONTEMPT_WIN    =  40
CONTEMPT_EQUAL  =  15
CONTEMPT_BEHIND = -50
CONTEMPT_LOSING =-100
_root_color=[WHITE]

def _root_standing():
    return mg_score[_root_color[0]] - mg_score[1-_root_color[0]]

def _contempt_score():
    mat = _root_standing()
    if   mat >  400: c = CONTEMPT_WIN
    elif mat >  150: c = CONTEMPT_WIN // 2
    elif mat > -150: c = CONTEMPT_EQUAL
    elif mat > -400: c = CONTEMPT_BEHIND
    else:            c = CONTEMPT_LOSING
    return -c if side_to_move[0]==_root_color[0] else c

_nodes=[0]; _search_start=[0.0]; _time_limit=[4500]
_draw_seeking=[False]

def _time_up():
    return (time.monotonic()*1000-_search_start[0])>_time_limit[0]

def _non_pawn_material(color):
    """Quick: use incremental counters rather than scanning board."""
    # mg_score includes all material; subtract king and pawns
    # We only need to know if it's > 0
    raw=mg_score[color]
    raw-=MG_VAL[PAWN]*8   # rough upper bound on pawn contribution
    return raw>0

def quiesce(alpha, beta):
    _nodes[0]+=1
    if (_nodes[0]&4095)==0 and _time_up(): return alpha
    h=hash_key[0]
    entry=tt_probe(h,0)
    if entry:
        s,f,mv,_=entry
        if f==TT_EXACT: return s
        if f==TT_ALPHA and s<=alpha: return alpha
        if f==TT_BETA  and s>=beta:  return beta

    stand=evaluate()
    if stand>=beta: return beta
    if stand>alpha: alpha=stand

    gen_moves(side_to_move[0])
    captures=[]
    for i in range(_move_count[0]):
        mv=_move_list[i]
        if move_is_capture(mv):
            sv=see(mv)
            if sv>=-50:   # prune losing captures early in qsearch
                captures.append((sv,mv))
    captures.sort(reverse=True)

    orig_alpha=alpha; best_q=0
    for _,mv in captures:
        if stand+_SEE_VALS[board[move_to(mv)]&7]+200<alpha: continue
        make_move(mv)
        if is_attacked(king_pos[1-side_to_move[0]],side_to_move[0]):
            unmake_move(); continue
        score=-quiesce(-beta,-alpha)
        unmake_move()
        if score>=beta: tt_store(h,0,beta,TT_BETA,mv); return beta
        if score>alpha: alpha=score; best_q=mv

    tt_store(h,0,alpha,TT_EXACT if alpha>orig_alpha else TT_ALPHA,best_q)
    return alpha

def _is_repetition():
    # Ring buffer scan: only look back halfMove plies (50-move clock resets clear history)
    # Two positions apart (same side to move) — compare every 2nd entry
    cur = hash_key[0]
    rh  = rep_head[0]
    hm  = half_move[0]
    end = max(0, rh - hm - 1)
    i   = rh - 2           # same side to move as current (skip 1 ply)
    while i >= end:
        if rep_hist[i & (REP_SIZE-1)] == cur:
            return True
        i -= 2
    return False

def negamax(depth, alpha, beta, ply, null_ok, prev_move=0, prev2_move=0):
    if (_nodes[0]&4095)==0 and _time_up(): return alpha
    _nodes[0]+=1

    is_pv=(beta-alpha>1)

    # Draw detection with asymmetric contempt
    if half_move[0]>=100: return _contempt_score()
    if _is_repetition():
        c=_contempt_score()
        # Extra incentive: when draw-seeking and opponent to move, make repeats look better
        if _draw_seeking[0] and side_to_move[0]!=_root_color[0]: c+=30
        return c

    h=hash_key[0]
    entry=tt_probe(h,depth)
    tt_best=0
    if entry:
        s,f,mv,td=entry
        tt_best=mv
        if td>=depth:
            if f==TT_EXACT:
                if not is_pv: return s
            elif f==TT_ALPHA and s<=alpha: return alpha
            elif f==TT_BETA  and s>=beta:  return beta

    if depth<=0: return quiesce(alpha,beta)

    in_check=is_in_check(side_to_move[0])
    static_eval=evaluate()

    # ── Reverse Futility Pruning ──
    if not is_pv and not in_check and 1<=depth<=7:
        if static_eval-120*depth>=beta: return static_eval-120*depth

    # ── Razoring ──
    if not is_pv and not in_check and depth<=3:
        if static_eval+300+150*depth<alpha:
            q=quiesce(alpha-1,alpha)
            if q<alpha: return q

    # ── Null-move with adaptive R + zugzwang guard ──
    if null_ok and not in_check and depth>=3 and _non_pawn_material(side_to_move[0]):
        R=3+depth//4+(1 if static_eval>=beta else 0)
        R=min(R,depth-1)
        saved_ep=ep_square[0]; saved_h=hash_key[0]
        if ep_square[0]>=0: hash_key[0]^=ZOB_EP[ep_square[0]&7]
        ep_square[0]=-1; side_to_move[0]^=1; hash_key[0]^=ZOB_SIDE
        null_score=-negamax(depth-1-R,-beta,-beta+1,ply+1,False,0,prev_move)
        side_to_move[0]^=1; hash_key[0]^=ZOB_SIDE
        ep_square[0]=saved_ep; hash_key[0]=saved_h
        if ep_square[0]>=0: hash_key[0]^=ZOB_EP[ep_square[0]&7]
        if null_score>=beta: return beta

    # ── IID ──
    if not tt_best and depth>=4:
        negamax(depth-2,alpha,beta,ply,False,prev_move,prev2_move)
        tt_best=tt_best_move(h)

    # ── Check extension  [FIX G basis] ──
    ext=1 if in_check else 0

    gen_moves(side_to_move[0])
    moves=[_move_list[i] for i in range(_move_count[0])]
    sort_moves(moves,tt_best,ply,prev_move,prev2_move)

    # ── Singular extension  [FIX G] ──
    # If the TT move at sufficient depth has score well above a reduced search,
    # extend it by 1 extra ply (it may be the only good move).
    singular_move=0
    if (tt_best and depth>=6 and not in_check and entry and
            entry[3]>=depth-3 and entry[1]!=TT_ALPHA):
        s_beta=entry[0]-50
        saved_ep2=ep_square[0]; saved_h2=hash_key[0]
        red_score=negamax(depth//2,-s_beta-1,-s_beta,ply+1,False,prev_move,prev2_move)
        ep_square[0]=saved_ep2; hash_key[0]=saved_h2
        if red_score<s_beta:
            singular_move=tt_best; ext=max(ext,1)

    legal_count=0; best_move_found=0; orig_alpha=alpha

    for i,mv in enumerate(moves):
        make_move(mv)
        if is_attacked(king_pos[1-side_to_move[0]],side_to_move[0]):
            unmake_move(); continue
        legal_count+=1
        is_cap=move_is_capture(mv)
        is_prom=move_is_promo(mv)
        gives_check=is_in_check(side_to_move[0])

        # Singular extension for this specific move
        move_ext=ext+(1 if mv==singular_move else 0)

        # ── Futility pruning depth 1-3 ──
        if (depth<=3 and not in_check and not is_cap and
                not is_prom and not gives_check and legal_count>1):
            if static_eval+[0,150,300,450][depth]<=alpha:
                unmake_move(); continue

        # ── SEE pruning for bad captures ──
        if not is_pv and is_cap and depth<=6 and not in_check and see(mv)<-50*depth:
            unmake_move(); continue

        # ── LMR ──
        reduction=0
        if depth>=3 and i>=3 and not in_check and not is_cap and not is_prom and not gives_check:
            reduction=LMR_TABLE[min(depth,63)][min(i,63)]
            if is_pv: reduction=max(0,reduction-1)
            if killers[ply*2]==mv or killers[ply*2+1]==mv: reduction=max(0,reduction-1)

        # ── PVS ──
        eff_depth=depth-1+move_ext
        if legal_count==1:
            score=-negamax(eff_depth,-beta,-alpha,ply+1,True,mv,prev_move)
        else:
            score=-negamax(eff_depth-reduction,-alpha-1,-alpha,ply+1,True,mv,prev_move)
            if score>alpha and (reduction>0 or score<beta):
                score=-negamax(eff_depth,-alpha-1,-alpha,ply+1,True,mv,prev_move)
                if score>alpha and score<beta and is_pv:
                    score=-negamax(eff_depth,-beta,-alpha,ply+1,True,mv,prev_move)

        unmake_move()
        if (_nodes[0]&2047)==0 and _time_up(): return alpha

        if score>alpha:
            alpha=score; best_move_found=mv
            if score>=beta:
                if not is_cap:
                    if killers[ply*2]!=mv:
                        killers[ply*2+1]=killers[ply*2]; killers[ply*2]=mv
                    bonus=depth*depth
                    update_history(side_to_move[0]^1,move_from(mv),move_to(mv),bonus)
                    update_cont(prev_move,mv,bonus)
                    if prev_move:
                        counter_move[(move_from(prev_move)&127)*128+(move_to(prev_move)&127)]=mv
                tt_store(h,depth,beta,TT_BETA,mv)
                return beta

    if legal_count==0:
        return -(MATE-ply) if in_check else _contempt_score()

    tt_store(h,depth,alpha,TT_EXACT if alpha>orig_alpha else TT_ALPHA,best_move_found)
    return alpha

# ── Iterative deepening + aspiration windows ──────────────────────────────
def search(fen_for_learn=None):
    tt_new_search()
    _search_start[0]=time.monotonic()*1000
    _nodes[0]=0
    _root_color[0]=side_to_move[0]
    for i in range(len(killers)): killers[i]=0

    legal=legal_moves(side_to_move[0])
    if not legal: return None
    if len(legal)==1: return legal[0]

    # Compute root material standing for contempt & draw-seeking
    mat = mg_score[_root_color[0]] - mg_score[1-_root_color[0]]
    _draw_seeking[0] = (mat < -150)

    # Time allocation: more time when losing (to find drawing resources)
    if   len(legal)<=3:  base_ms=1000
    elif len(legal)<=8:  base_ms=2500
    elif mat < -300:     base_ms=4800
    else:                base_ms=4500
    _time_limit[0]=base_ms

    # Learning-bias root move order
    if fen_for_learn and _learn_db:
        legal.sort(key=lambda m:-_learn_score(fen_for_learn,move_to_uci(m)))

    # Draw-seeking: pre-sort repetition moves to front of root list
    if _draw_seeking[0]:
        def _rep_key(m):
            make_move(m); is_rep=_is_repetition(); unmake_move()
            return 0 if is_rep else 1
        legal.sort(key=_rep_key)

    best_result=legal[0]; prev_score=0
    asp_delta=25; asp_factor=2

    for depth in range(1,65):
        if _time_up(): break

        alpha=prev_score-asp_delta if depth>=5 else -INF
        beta =prev_score+asp_delta if depth>=5 else INF
        cur_delta=asp_delta

        while True:
            score=negamax(depth,alpha,beta,0,False)
            if _time_up(): break
            if score<=alpha:
                beta=(alpha+beta)//2
                alpha=max(-INF,alpha-cur_delta)
                cur_delta*=asp_factor
            elif score>=beta:
                alpha=(alpha+beta)//2
                beta=min(INF,beta+cur_delta)
                cur_delta*=asp_factor
            else:
                break

        if not _time_up() or depth==1:
            bm=tt_best_move(hash_key[0])
            if bm: best_result=bm
            prev_score=score
            asp_delta=max(15,abs(score-prev_score)//2+10)

        if _time_up() or score>MATE-100: break

    # ── Anti-blunder: SEE-check the chosen move when winning ──────────────────
    # If best move is a quiet move that walks into a capture (SEE < -200),
    # fall back to any safe legal move to avoid one-move blunders.
    if best_result and not _draw_seeking[0]:
        if not move_is_capture(best_result) and not move_is_promo(best_result):
            to_sq=move_to(best_result)
            make_move(best_result)
            opp=side_to_move[0]
            if is_attacked(to_sq,opp):
                # Piece walked into an attack — check if it's defended
                gen_moves(1-opp)
                cheapest_recapture=None; best_recap_val=99999
                for i in range(_move_count[0]):
                    rc=_move_list[i]
                    if move_to(rc)==to_sq:
                        v=_SEE_VALS[board[move_from(rc)]&7]
                        if v<best_recap_val: best_recap_val=v; cheapest_recapture=rc
                if cheapest_recapture:
                    piece_val=_SEE_VALS[board[to_sq]&7] if board[to_sq] else 0
                    if best_recap_val<piece_val-100:   # hanging piece
                        unmake_move()
                        safe=[m for m in legal
                              if m!=best_result and
                              (not move_is_capture(m) or see(m)>=0)]
                        if safe: best_result=safe[0]
                        unmake_move() if False else None  # already unmade
                        return best_result
            unmake_move()

    return best_result

# ─────────────────────────────────────────────────────────────────────────────
# 18.  I/O LOOP
# ─────────────────────────────────────────────────────────────────────────────
def main():
    _learn_load()
    _assert_see()   # FIX I: verify SEE at startup
    out=sys.stdout

    for line in sys.stdin:
        line=line.strip()
        if not line: continue

        if line.startswith('result:'):
            res=line.split(':',1)[1].strip().lower()
            if res in ('w','d','l'): learn_commit(res)
            continue

        fen=line
        try:
            book=get_book_move(fen)
            if book:
                learn_record_move(fen,book)
                out.write(book+'\n'); out.flush(); continue

            parse_fen(fen)
            best=search(fen_for_learn=fen)
            uci=move_to_uci(best) if best else '0000'
            if best: learn_record_move(fen,uci)
            out.write(uci+'\n'); out.flush()
        except Exception as exc:
            sys.stderr.write(f'Error: {exc}\n')
            out.write('0000\n'); out.flush()

if __name__=='__main__':
    main()
