Download browser/entry.js from burrowdweller/mini-chessformer-micro: direct link, hf CLI and curl.
- Browser
- Download file 53.2 kB
-
https://huggingface.co/burrowdweller/mini-chessformer-micro/resolve/main/browser/entry.js
- Command line
-
hf download hf://burrowdweller/mini-chessformer-micro/browser/entry.js
-
curl -L -o entry.js https://huggingface.co/burrowdweller/mini-chessformer-micro/resolve/main/browser/entry.js
53.2 kB
| const SEQ_LEN=69;const VOCAB_SIZE=53;const PIECE_TYPE={p:1,n:2,b:3,r:4,q:5,k:6};function fenToTokens(fen){const tokens=new Array(SEQ_LEN).fill(0);const[placement,activeColor,castling,epSquare,halfmoveStr]=fen.split(" ");const ranks=placement.split("/");if(ranks.length!==8){throw new Error(`fenToTokens: expected 8 ranks in placement, got ${ranks.length}`)}for(let ri=0;ri<8;ri++){const rank=7-ri;let file=0;for(const ch of ranks[ri]){if(ch>="1"&&ch<="8"){file+=Number(ch);continue}const lower=ch.toLowerCase();const pieceType=PIECE_TYPE[lower];if(pieceType===void 0){throw new Error(`fenToTokens: bad piece char '${ch}' in placement`)}const isWhite=ch===ch.toUpperCase();const sq=rank*8+file;tokens[sq]=isWhite?pieceType:pieceType+6;file+=1}if(file!==8){throw new Error(`fenToTokens: rank ${8-ri} did not sum to 8 files (got ${file})`)}}tokens[64]=activeColor==="w"?13:14;let castlingMask=0;if(castling.includes("K"))castlingMask|=1;if(castling.includes("Q"))castlingMask|=2;if(castling.includes("k"))castlingMask|=4;if(castling.includes("q"))castlingMask|=8;tokens[65]=15+castlingMask;if(epSquare&&epSquare!=="-"){const file=epSquare.charCodeAt(0)-"a".charCodeAt(0);tokens[66]=31+file}else{tokens[66]=39}const halfmove=Number(halfmoveStr??0);tokens[67]=40+Math.min(Math.floor(halfmove/5),9);tokens[68]=50+0;return tokens}const PROMO_PIECES=[2,3,4,5];const PROMO_LETTER_TO_INT={n:2,b:3,r:4,q:5};function squareRank(sq){return Math.floor(sq/8)}function squareFile(sq){return sq%8}function buildMoveIndex(){const moves=[];for(let frm=0;frm<64;frm++){for(let to=0;to<64;to++){moves.push([frm,to,null])}}for(let frm=0;frm<64;frm++){const fr=squareRank(frm);const ff=squareFile(frm);for(let to=0;to<64;to++){const tr=squareRank(to);const tf=squareFile(to);const whitePromo=fr===6&&tr===7;const blackPromo=fr===1&&tr===0;if((whitePromo||blackPromo)&&Math.abs(ff-tf)<=1){for(const p of PROMO_PIECES){moves.push([frm,to,p])}}}}const idx=new Map;moves.forEach((m,i)=>idx.set(m.join(","),i));return{moves,idx}}const{moves:MOVES_LIST,idx:MOVE_TO_IDX}=buildMoveIndex();const MOVE_SPACE=MOVES_LIST.length;function squareNameToIndex(name){const file=name.charCodeAt(0)-"a".charCodeAt(0);const rank=name.charCodeAt(1)-"1".charCodeAt(0);return rank*8+file}function moveToIndex(fromName,toName,promoLetter){const frm=squareNameToIndex(fromName);const to=squareNameToIndex(toName);const promo=promoLetter?PROMO_LETTER_TO_INT[promoLetter]:null;if(promoLetter&&promo===void 0){throw new Error(`moveToIndex: unrecognized promotion letter '${promoLetter}'`)}const key=[frm,to,promo].join(",");const i=MOVE_TO_IDX.get(key);if(i===void 0){throw new Error(`moveToIndex: no index for move ${fromName}${toName}${promoLetter||""}`)}return i}const PAWN=1,KNIGHT=2,BISHOP=3,ROOK=4,QUEEN=5,KING=6;const WHITE=0,BLACK=1;const PIECE_CHAR={[PAWN]:"p",[KNIGHT]:"n",[BISHOP]:"b",[ROOK]:"r",[QUEEN]:"q",[KING]:"k"};const CHAR_PIECE=Object.fromEntries(Object.entries(PIECE_CHAR).map(([k,v])=>[v,Number(k)]));const PROMO_LETTER={[KNIGHT]:"n",[BISHOP]:"b",[ROOK]:"r",[QUEEN]:"q"};const LETTER_PROMO={n:KNIGHT,b:BISHOP,r:ROOK,q:QUEEN};function sqIndex(name){const file=name.charCodeAt(0)-97;const rank=name.charCodeAt(1)-49;return rank*8+file}function sqName(idx){const file=idx&7;const rank=idx>>3;return String.fromCharCode(97+file)+String.fromCharCode(49+rank)}const fileOf=sq=>sq&7;const rankOf=sq=>sq>>3;const START_FEN="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";const KNIGHT_OFFSETS=[[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];const KING_OFFSETS=[[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]];const BISHOP_DIRS=[[1,1],[1,-1],[-1,1],[-1,-1]];const ROOK_DIRS=[[1,0],[-1,0],[0,1],[0,-1]];const QUEEN_DIRS=[...BISHOP_DIRS,...ROOK_DIRS];function inBounds(file,rank){return file>=0&&file<8&&rank>=0&&rank<8}function pieceTypeAt(board,sq){const p=board[sq];return p===0?0:p<=6?p:p-6}function colorAt(board,sq){const p=board[sq];return p===0?-1:p<=6?WHITE:BLACK}function isAttacked(board,sq,byColor){const sf=fileOf(sq),sr=rankOf(sq);for(const[df,dr]of KNIGHT_OFFSETS){const f=sf+df,r=sr+dr;if(!inBounds(f,r))continue;const t=r*8+f;if(colorAt(board,t)===byColor&&pieceTypeAt(board,t)===KNIGHT)return true}for(const[df,dr]of KING_OFFSETS){const f=sf+df,r=sr+dr;if(!inBounds(f,r))continue;const t=r*8+f;if(colorAt(board,t)===byColor&&pieceTypeAt(board,t)===KING)return true}{const dr=byColor===WHITE?-1:1;for(const df of[-1,1]){const f=sf+df,r=sr+dr;if(!inBounds(f,r))continue;const t=r*8+f;if(colorAt(board,t)===byColor&&pieceTypeAt(board,t)===PAWN)return true}}for(const[df,dr]of BISHOP_DIRS){let f=sf+df,r=sr+dr;while(inBounds(f,r)){const t=r*8+f;const c=colorAt(board,t);if(c!==-1){if(c===byColor){const pt=pieceTypeAt(board,t);if(pt===BISHOP||pt===QUEEN)return true}break}f+=df;r+=dr}}for(const[df,dr]of ROOK_DIRS){let f=sf+df,r=sr+dr;while(inBounds(f,r)){const t=r*8+f;const c=colorAt(board,t);if(c!==-1){if(c===byColor){const pt=pieceTypeAt(board,t);if(pt===ROOK||pt===QUEEN)return true}break}f+=df;r+=dr}}return false}function kingDangerSquares(board,kingSq,enemyColor){const scratch=board.slice();scratch[kingSq]=0;const kingColor=enemyColor===WHITE?BLACK:WHITE;const kf=fileOf(kingSq),kr=rankOf(kingSq);const candidates=new Set;for(const[df,dr]of KING_OFFSETS){const f=kf+df,r=kr+dr;if(inBounds(f,r))candidates.add(r*8+f)}for(const spec of CASTLING_SPECS){if(spec.color!==kingColor)continue;for(const sq of spec.kingPath)candidates.add(sq)}const danger=new Set;for(const sq of candidates){if(isAttacked(scratch,sq,enemyColor))danger.add(sq)}return danger}function computeCheckState(board,kingSq,kingColor){const enemyColor=kingColor===WHITE?BLACK:WHITE;const kf=fileOf(kingSq),kr=rankOf(kingSq);const checkers=[];for(const[df,dr]of KNIGHT_OFFSETS){const f=kf+df,r=kr+dr;if(!inBounds(f,r))continue;const t=r*8+f;if(colorAt(board,t)===enemyColor&&pieceTypeAt(board,t)===KNIGHT)checkers.push(t)}{const dr=kingColor===WHITE?1:-1;for(const df of[-1,1]){const f=kf+df,r=kr+dr;if(!inBounds(f,r))continue;const t=r*8+f;if(colorAt(board,t)===enemyColor&&pieceTypeAt(board,t)===PAWN)checkers.push(t)}}const pinned=new Map;for(const[df,dr]of QUEEN_DIRS){const isDiagonal=df!==0&&dr!==0;let f=kf+df,r=kr+dr;let ownBlocker=-1;while(inBounds(f,r)){const t=r*8+f;const c=colorAt(board,t);if(c===-1){f+=df;r+=dr;continue}if(c===kingColor){if(ownBlocker===-1){ownBlocker=t;f+=df;r+=dr;continue}break}const pt=pieceTypeAt(board,t);const attacksThisRay=isDiagonal?pt===BISHOP||pt===QUEEN:pt===ROOK||pt===QUEEN;if(ownBlocker===-1){if(attacksThisRay)checkers.push(t);break}if(attacksThisRay){const ray=new Set;let rf=kf+df,rr=kr+dr;while(true){const rsq=rr*8+rf;ray.add(rsq);if(rsq===t)break;rf+=df;rr+=dr}pinned.set(ownBlocker,ray)}break}}return{checkers,pinned}}function checkResponseMask(board,kingSq,checkers){if(checkers.length===0)return null;if(checkers.length>=2)return new Set;const checkerSq=checkers[0];const pt=pieceTypeAt(board,checkerSq);const mask=new Set([checkerSq]);if(pt===BISHOP||pt===ROOK||pt===QUEEN){const kf=fileOf(kingSq),kr=rankOf(kingSq);const cf=fileOf(checkerSq),cr=rankOf(checkerSq);const df=Math.sign(cf-kf),dr=Math.sign(cr-kr);let f=kf+df,r=kr+dr;while(!(f===cf&&r===cr)){mask.add(r*8+f);f+=df;r+=dr}}return mask}function inMask(mask,sq){return mask===null||mask.has(sq)}function _genSliderMoves(board,color,checkMask,pinned){const moves=[];for(let sq=0;sq<64;sq++){if(colorAt(board,sq)!==color)continue;const pt=pieceTypeAt(board,sq);if(pt!==BISHOP&&pt!==ROOK&&pt!==QUEEN)continue;const dirs=pt===BISHOP?BISHOP_DIRS:pt===ROOK?ROOK_DIRS:QUEEN_DIRS;const pinRay=pinned.get(sq)??null;const sf=fileOf(sq),sr=rankOf(sq);for(const[df,dr]of dirs){let f=sf+df,r=sr+dr;while(inBounds(f,r)){const t=r*8+f;const c=colorAt(board,t);if(c===color)break;if(inMask(checkMask,t)&&(pinRay===null||pinRay.has(t))){moves.push({from:sq,to:t,promotion:null})}if(c!==-1)break;f+=df;r+=dr}}}return moves}function _genKnightMoves(board,color,checkMask,pinned){const moves=[];for(let sq=0;sq<64;sq++){if(colorAt(board,sq)!==color||pieceTypeAt(board,sq)!==KNIGHT)continue;if(pinned.has(sq))continue;const sf=fileOf(sq),sr=rankOf(sq);for(const[df,dr]of KNIGHT_OFFSETS){const f=sf+df,r=sr+dr;if(!inBounds(f,r))continue;const t=r*8+f;if(colorAt(board,t)===color)continue;if(!inMask(checkMask,t))continue;moves.push({from:sq,to:t,promotion:null})}}return moves}function _genKingMoves(board,color,kingSq,kingDanger){const moves=[];const sf=fileOf(kingSq),sr=rankOf(kingSq);for(const[df,dr]of KING_OFFSETS){const f=sf+df,r=sr+dr;if(!inBounds(f,r))continue;const t=r*8+f;if(colorAt(board,t)===color)continue;if(kingDanger.has(t))continue;moves.push({from:kingSq,to:t,promotion:null})}return moves}function findKing(board,color){for(let sq=0;sq<64;sq++){if(colorAt(board,sq)===color&&pieceTypeAt(board,sq)===KING)return sq}throw new Error("no king on board for color "+color)}function legalMovesNoPawnsNoCastling(board,color){const kingSq=findKing(board,color);const enemyColor=color===WHITE?BLACK:WHITE;const{checkers,pinned}=computeCheckState(board,kingSq,color);const checkMask=checkResponseMask(board,kingSq,checkers);const kingDanger=kingDangerSquares(board,kingSq,enemyColor);const moves=_genKingMoves(board,color,kingSq,kingDanger);if(checkers.length>=2)return moves;return moves.concat(_genSliderMoves(board,color,checkMask,pinned),_genKnightMoves(board,color,checkMask,pinned))}const PROMOTION_PIECE_TYPES=[KNIGHT,BISHOP,ROOK,QUEEN];function epExposesDiscoveredCheck(board,fromSq,capturedPawnSq,color,kingSq){if(rankOf(kingSq)!==rankOf(fromSq))return false;const scratch=board.slice();scratch[fromSq]=0;scratch[capturedPawnSq]=0;const enemyColor=color===WHITE?BLACK:WHITE;return isAttacked(scratch,kingSq,enemyColor)}function _genPawnMoves(board,color,checkMask,pinned,epSquare,kingSq){const moves=[];const dir=color===WHITE?1:-1;const startRank=color===WHITE?1:6;const promoRank=color===WHITE?7:0;for(let sq=0;sq<64;sq++){if(colorAt(board,sq)!==color||pieceTypeAt(board,sq)!==PAWN)continue;const f=fileOf(sq),r=rankOf(sq);const pinRay=pinned.get(sq)??null;const addMove=(to,promo=null)=>{if(!inMask(checkMask,to))return;if(pinRay!==null&&!pinRay.has(to))return;if(r+dir===promoRank){for(const p of PROMOTION_PIECE_TYPES)moves.push({from:sq,to,promotion:p})}else{moves.push({from:sq,to,promotion:promo})}};const oneAheadRank=r+dir;if(inBounds(f,oneAheadRank)){const oneAhead=oneAheadRank*8+f;if(colorAt(board,oneAhead)===-1){addMove(oneAhead);if(r===startRank){const twoAheadRank=r+2*dir;const twoAhead=twoAheadRank*8+f;if(colorAt(board,twoAhead)===-1)addMove(twoAhead)}}}for(const df of[-1,1]){const cf=f+df,cr=r+dir;if(!inBounds(cf,cr))continue;const capSq=cr*8+cf;if(colorAt(board,capSq)===(color===WHITE?BLACK:WHITE)){addMove(capSq)}else if(capSq===epSquare){const capturedPawnSq=r*8+cf;if(epExposesDiscoveredCheck(board,sq,capturedPawnSq,color,kingSq))continue;if(checkMask!==null&&!checkMask.has(epSquare)&&!checkMask.has(capturedPawnSq))continue;if(pinRay!==null&&!pinRay.has(epSquare))continue;moves.push({from:sq,to:epSquare,promotion:null})}}}return moves}function pawnMovesForTest(board,color,epSquare){const kingSq=findKing(board,color);const{checkers,pinned}=computeCheckState(board,kingSq,color);const checkMask=checkResponseMask(board,kingSq,checkers);if(checkers.length>=2)return[];return _genPawnMoves(board,color,checkMask,pinned,epSquare,kingSq)}const CASTLING_SPECS=[{color:WHITE,side:"k",kingFrom:4,kingTo:6,rookFrom:7,clear:[5,6],kingPath:[4,5,6],rightsBit:1},{color:WHITE,side:"q",kingFrom:4,kingTo:2,rookFrom:0,clear:[1,2,3],kingPath:[4,3,2],rightsBit:2},{color:BLACK,side:"k",kingFrom:60,kingTo:62,rookFrom:63,clear:[61,62],kingPath:[60,61,62],rightsBit:4},{color:BLACK,side:"q",kingFrom:60,kingTo:58,rookFrom:56,clear:[57,58,59],kingPath:[60,59,58],rightsBit:8}];function _genCastling(board,color,castlingRights,checkersCount,kingDanger){if(checkersCount>0)return[];const moves=[];for(const spec of CASTLING_SPECS){if(spec.color!==color)continue;if((castlingRights&spec.rightsBit)===0)continue;if(spec.clear.some(sq=>colorAt(board,sq)!==-1))continue;if(spec.kingPath.some(sq=>kingDanger.has(sq)))continue;moves.push({from:spec.kingFrom,to:spec.kingTo,promotion:null})}return moves}function castlingMovesForTest(board,color,castlingRights){const kingSq=findKing(board,color);const enemyColor=color===WHITE?BLACK:WHITE;const{checkers}=computeCheckState(board,kingSq,color);const kingDanger=kingDangerSquares(board,kingSq,enemyColor);return _genCastling(board,color,castlingRights,checkers.length,kingDanger)}class Chess{constructor(fen){this._loadFen(fen??START_FEN)}_loadFen(fen){const[placement,active,castling,ep,halfmove,fullmove]=fen.split(" ");this._board=new Uint8Array(64);const ranks=placement.split("/");if(ranks.length!==8)throw new Error(`bad FEN placement: ${placement}`);for(let ri=0;ri<8;ri++){const rank=7-ri;let file=0;for(const ch of ranks[ri]){if(ch>="1"&&ch<="8"){file+=Number(ch);continue}const lower=ch.toLowerCase();const type=CHAR_PIECE[lower];if(type===void 0)throw new Error(`bad FEN piece char: ${ch}`);const isWhite=ch===ch.toUpperCase();this._board[rank*8+file]=type+(isWhite?0:6);file+=1}if(file!==8)throw new Error(`bad FEN rank (not 8 files): ${ranks[ri]}`)}this._turn=active==="w"?WHITE:BLACK;let mask=0;if(castling.includes("K"))mask|=1;if(castling.includes("Q"))mask|=2;if(castling.includes("k"))mask|=4;if(castling.includes("q"))mask|=8;this._castling=mask;this._epSquare=ep==="-"?-1:sqIndex(ep);this._halfmove=Number(halfmove??0);this._fullmove=Number(fullmove??1);this._history=[];this._repCounts=new Map;this._seedRepCount()}turn(){return this._turn===WHITE?"w":"b"}fen(){let placement="";for(let rank=7;rank>=0;rank--){let empty=0;for(let file=0;file<8;file++){const piece=this._board[rank*8+file];if(piece===0){empty++;continue}if(empty>0){placement+=empty;empty=0}const isWhite=piece<=6;const type=isWhite?piece:piece-6;placement+=isWhite?PIECE_CHAR[type].toUpperCase():PIECE_CHAR[type]}if(empty>0)placement+=empty;if(rank>0)placement+="/"}let castling="";if(this._castling&1)castling+="K";if(this._castling&2)castling+="Q";if(this._castling&4)castling+="k";if(this._castling&8)castling+="q";if(castling==="")castling="-";const ep=this._hasLegalEnPassant()?sqName(this._epSquare):"-";return`${placement} ${this._turn===WHITE?"w":"b"} ${castling} ${ep} ${this._halfmove} ${this._fullmove}`}_hasLegalEnPassant(){if(this._epSquare===-1)return false;return this._legalMoves().some(m=>m.to===this._epSquare&&pieceTypeAt(this._board,m.from)===PAWN&&fileOf(m.from)!==fileOf(m.to))}_positionKey(){const ep=this._hasLegalEnPassant()?this._epSquare:-1;return this._board.join(",")+"|"+this._turn+"|"+this._castling+"|"+ep}_seedRepCount(){const key=this._positionKey();this._repCounts.set(key,(this._repCounts.get(key)??0)+1)}_legalMoves(){const kingSq=findKing(this._board,this._turn);const enemyColor=this._turn===WHITE?BLACK:WHITE;const{checkers,pinned}=computeCheckState(this._board,kingSq,this._turn);const checkMask=checkResponseMask(this._board,kingSq,checkers);const kingDanger=kingDangerSquares(this._board,kingSq,enemyColor);const moves=_genKingMoves(this._board,this._turn,kingSq,kingDanger);moves.push(..._genCastling(this._board,this._turn,this._castling,checkers.length,kingDanger));if(checkers.length<2){moves.push(..._genSliderMoves(this._board,this._turn,checkMask,pinned),..._genKnightMoves(this._board,this._turn,checkMask,pinned),..._genPawnMoves(this._board,this._turn,checkMask,pinned,this._epSquare,kingSq))}return moves}isCheck(){const kingSq=findKing(this._board,this._turn);return isAttacked(this._board,kingSq,this._turn===WHITE?BLACK:WHITE)}isCheckmate(){return this.isCheck()&&this._legalMoves().length===0}_isStalemate(){return!this.isCheck()&&this._legalMoves().length===0}moves(opts){const raw=this._legalMoves();if(opts&&opts.verbose){const computeSan=opts.computeSan!==false;return raw.map(m=>{const verbose={from:sqName(m.from),to:sqName(m.to),promotion:m.promotion?PROMO_LETTER[m.promotion]:void 0};if(computeSan)verbose.san=this.moveToSan(m);return verbose})}return raw.map(m=>this.moveToSan(m))}_applyMoveForTest(from,to,promotion){return this._applyMove({from,to,promotion:promotion??null})}_applyMove(move){const{from,to,promotion}=move;const board=this._board;const movingPiece=board[from];const movingType=pieceTypeAt(board,from);const isWhite=this._turn===WHITE;const capturedPiece=board[to];const record={from,to,promotion,movingPiece,capturedPiece,castling:this._castling,epSquare:this._epSquare,halfmove:this._halfmove,isEnPassant:false,capturedSq:to,rookFrom:-1,rookTo:-1};if(movingType===PAWN&&to===this._epSquare&&board[to]===0&&fileOf(from)!==fileOf(to)){const capturedSq=rankOf(from)*8+fileOf(to);record.isEnPassant=true;record.capturedSq=capturedSq;record.capturedPiece=board[capturedSq];board[capturedSq]=0}if(movingType===KING&&Math.abs(fileOf(to)-fileOf(from))===2){const spec=CASTLING_SPECS.find(s=>s.color===this._turn&&s.kingTo===to);board[spec.rookFrom]=0;const rookTo=spec.side==="k"?spec.kingTo-1:spec.kingTo+1;board[rookTo]=ROOK+(isWhite?0:6);record.rookFrom=spec.rookFrom;record.rookTo=rookTo}board[from]=0;board[to]=promotion!==null?promotion+(isWhite?0:6):movingPiece;if(movingType===KING){this._castling&=isWhite?~3:~12}for(const spec of CASTLING_SPECS){if(from===spec.rookFrom||to===spec.rookFrom)this._castling&=~spec.rightsBit}this._epSquare=-1;if(movingType===PAWN&&Math.abs(rankOf(to)-rankOf(from))===2){this._epSquare=(rankOf(from)+rankOf(to))/2*8+fileOf(from)}this._halfmove=movingType===PAWN||record.capturedPiece!==0?0:this._halfmove+1;if(this._turn===BLACK)this._fullmove+=1;this._turn=isWhite?BLACK:WHITE;this._history.push(record);this._recordRepetition()}undo(){const record=this._history.pop();if(!record)return;this._forgetRepetition();const board=this._board;const isWhite=this._turn===BLACK;board[record.from]=record.movingPiece;if(record.isEnPassant){board[record.to]=0;board[record.capturedSq]=record.capturedPiece}else if(record.capturedPiece!==0){board[record.to]=record.capturedPiece}else{board[record.to]=0}if(record.rookFrom!==-1){board[record.rookTo]=0;board[record.rookFrom]=ROOK+(isWhite?0:6)}this._castling=record.castling;this._epSquare=record.epSquare;this._halfmove=record.halfmove;if(this._turn===WHITE)this._fullmove-=1;this._turn=isWhite?WHITE:BLACK}history(opts){if(opts&&opts.verbose){return this._history.map(r=>({from:sqName(r.from),to:sqName(r.to),promotion:r.promotion?PROMO_LETTER[r.promotion]:void 0}))}return this._history.map(r=>sqName(r.from)+sqName(r.to))}_recordRepetition(){const key=this._positionKey();this._repCounts.set(key,(this._repCounts.get(key)??0)+1)}_forgetRepetition(){const key=this._positionKey();const count=this._repCounts.get(key)??0;if(count<=1)this._repCounts.delete(key);else this._repCounts.set(key,count-1)}_isThreefoldRepetition(){return(this._repCounts.get(this._positionKey())??0)>=3}_isInsufficientMaterial(){const pieces=[];for(let sq=0;sq<64;sq++){const p=this._board[sq];if(p===0)continue;const type=pieceTypeAt(this._board,sq);if(type===KING)continue;pieces.push({type,color:colorAt(this._board,sq),sq})}if(pieces.length===0)return true;if(pieces.length===1&&(pieces[0].type===KNIGHT||pieces[0].type===BISHOP))return true;if(pieces.length===2&&pieces.every(p=>p.type===BISHOP)&&pieces[0].color!==pieces[1].color){const colorOfSquare=sq=>(fileOf(sq)+rankOf(sq))%2;if(colorOfSquare(pieces[0].sq)===colorOfSquare(pieces[1].sq))return true}return false}isGameOver(){if(this.isCheckmate())return true;if(this._isStalemate())return true;if(this._isInsufficientMaterial())return true;if(this._halfmove>=100)return true;if(this._isThreefoldRepetition())return true;return false}move(input){const legal=this._legalMoves();let chosen;if(typeof input==="string"){chosen=this._resolveSan(input,legal);if(!chosen)throw new Error(`move() called with unrecognized SAN: ${input}`)}else{const fromIdx=sqIndex(input.from),toIdx=sqIndex(input.to);const promo=input.promotion?LETTER_PROMO[input.promotion]:null;chosen=legal.find(m=>m.from===fromIdx&&m.to===toIdx&&m.promotion===promo);if(!chosen)throw new Error(`move() called with illegal move: ${JSON.stringify(input)}`)}const san=this.moveToSan(chosen);this._applyMove(chosen);return{from:sqName(chosen.from),to:sqName(chosen.to),promotion:chosen.promotion?PROMO_LETTER[chosen.promotion]:void 0,san}}_resolveSan(san,legal){const clean=san.replace(/[+#]$/,"");if(clean==="O-O"||clean==="0-0"){return legal.find(m2=>pieceTypeAt(this._board,m2.from)===KING&&m2.to-m2.from===2)}if(clean==="O-O-O"||clean==="0-0-0"){return legal.find(m2=>pieceTypeAt(this._board,m2.from)===KING&&m2.from-m2.to===2)}const m=clean.match(/^([NBRQK]?)([a-h]?)([1-8]?)(x?)([a-h][1-8])(?:=([NBRQ]))?$/);if(!m)return null;const[,pieceLetter,disFile,disRank,,destName,promoLetter]=m;const pieceType=pieceLetter?CHAR_PIECE[pieceLetter.toLowerCase()]:PAWN;const dest=sqIndex(destName);const promo=promoLetter?LETTER_PROMO[promoLetter.toLowerCase()]:null;const candidates=legal.filter(cand=>{if(cand.to!==dest)return false;if(cand.promotion!==promo)return false;if(pieceTypeAt(this._board,cand.from)!==pieceType)return false;if(disFile&&fileOf(cand.from)!==disFile.charCodeAt(0)-97)return false;if(disRank&&rankOf(cand.from)!==disRank.charCodeAt(0)-49)return false;return true});return candidates.length===1?candidates[0]:null}moveToSan(move){const from=typeof move.from==="string"?sqIndex(move.from):move.from;const to=typeof move.to==="string"?sqIndex(move.to):move.to;const promotion=move.promotion==null?null:typeof move.promotion==="string"?LETTER_PROMO[move.promotion]:move.promotion;move={from,to,promotion};const movingType=pieceTypeAt(this._board,from);if(movingType===KING&&Math.abs(fileOf(to)-fileOf(from))===2){const base=to>from?"O-O":"O-O-O";return base+this._checkSuffix(move)}const isCapture=this._board[to]!==0||movingType===PAWN&&to===this._epSquare;let san="";if(movingType===PAWN){if(isCapture)san+=sqName(from)[0]+"x";san+=sqName(to);if(promotion)san+="="+PROMO_LETTER[promotion].toUpperCase()}else{san+=PIECE_CHAR[movingType].toUpperCase();san+=this._disambiguator(move);if(isCapture)san+="x";san+=sqName(to)}return san+this._checkSuffix(move)}_disambiguator(move){const{from,to,promotion}=move;const movingType=pieceTypeAt(this._board,from);const sharing=this._legalMoves().filter(m=>m.to===to&&m.promotion===promotion&&m.from!==from&&pieceTypeAt(this._board,m.from)===movingType);if(sharing.length===0)return"";const sameFile=sharing.some(m=>fileOf(m.from)===fileOf(from));const sameRank=sharing.some(m=>rankOf(m.from)===rankOf(from));if(!sameFile)return sqName(from)[0];if(!sameRank)return sqName(from)[1];return sqName(from)}_checkSuffix(move){this._applyMove(move);try{if(this.isCheck())return this._legalMoves().length===0?"#":"+";return""}finally{this.undo()}}}const PROMO_PIECE_ORDER=[KNIGHT,BISHOP,ROOK,QUEEN];const PROMO_MOVE_TABLE=(()=>{const table=[];for(let frm=0;frm<64;frm++){const fr=frm>>3,ff=frm&7;for(let to=0;to<64;to++){const tr=to>>3,tf=to&7;if((fr===6&&tr===7||fr===1&&tr===0)&&Math.abs(ff-tf)<=1){for(const p of PROMO_PIECE_ORDER)table.push({from:frm,to,promotion:p})}}}return table})();function decodeMoveIndex(idx){if(idx<4096)return{from:idx>>6,to:idx&63,promotion:null};return PROMO_MOVE_TABLE[idx-4096]}const ADAPTIVE_BATCH_SAFETY=1.25;const ADAPTIVE_BATCH_GUARD_MS=2;const ADAPTIVE_BATCH_EMA_ALPHA=.25;function clockMs(){if(typeof performance!=="undefined"&&typeof performance.now==="function"){return performance.now()}return Date.now()}function adaptiveBatchSizeForBudget(maxBatchSize,remainingMs,referenceBatchMs,referenceBatchSize=maxBatchSize){if(!Number.isInteger(maxBatchSize)||maxBatchSize<=0)return 0;if(remainingMs===Infinity)return maxBatchSize;if(!Number.isFinite(remainingMs)||remainingMs<=0)return 0;if(!Number.isFinite(referenceBatchMs)||referenceBatchMs<=0||!Number.isInteger(referenceBatchSize)||referenceBatchSize<=0){return maxBatchSize}for(let candidate=maxBatchSize;candidate>=1;candidate=Math.floor(candidate/2)){const estimatedMs=referenceBatchMs*(candidate/referenceBatchSize);const requiredMs=estimatedMs*ADAPTIVE_BATCH_SAFETY+ADAPTIVE_BATCH_GUARD_MS;if(remainingMs>=requiredMs)return candidate;if(candidate===1)break}return 0}class MCTSConfig{constructor({nSimulations=800,timeBudgetMs=null,cPuct=1.5,batchSize=1,reuseTree=true,deferredEval=false,deferredEvalMinVisits=2,lazyExpansion=false}={}){this.nSimulations=nSimulations;this.timeBudgetMs=timeBudgetMs;this.cPuct=cPuct;this.batchSize=batchSize;this.reuseTree=reuseTree;this.deferredEval=deferredEval;this.deferredEvalMinVisits=deferredEvalMinVisits;this.lazyExpansion=lazyExpansion}}class Node{constructor(prior){this.prior=prior;this.visitCount=0;this.valueSum=0;this.children=new Map;this.expanded=false;this.terminal=false;this.lazy=null;this.terminalValue=0}}function softmax(arr){const m=Math.max(...arr);const exps=arr.map(x=>Math.exp(x-m));const total=exps.reduce((a,b)=>a+b,0);if(total<=0)return arr.map(()=>1/arr.length);return exps.map(e=>e/total)}function wdlToScore(wdlProbs){return wdlProbs[0]+.5*wdlProbs[1]}function isDraw(chess){return chess.isGameOver()&&!chess.isCheckmate()}function rootSnapshot(root,simCount,chess){return{simCount,candidates:Array.from(root.children.values(),edge=>({san:chess.moveToSan({from:edge.from,to:edge.to,promotion:edge.promotion}),from:sqName(edge.from),to:sqName(edge.to),promotion:edge.promotion?PROMO_LETTER[edge.promotion]:void 0,visits:edge.node.visitCount,prior:edge.node.prior}))}}class MCTS{constructor(evaluator,config){this.evaluator=evaluator;this.config=config||new MCTSConfig;this.batchDurationMs=null;this.batchDurationSamples=0;this._root=null;this._rootHistory=null;this._rootFen=null}async search(chess,{timeBudgetMs,onRootUpdate}={}){const cfg=this.config;const effectiveTimeBudgetMs=timeBudgetMs!==void 0?timeBudgetMs:cfg.timeBudgetMs;const root=cfg.reuseTree&&this._acquireRoot(chess)||new Node(1);const notifyRoot=simCount=>{if(typeof onRootUpdate!=="function"||root.children.size===0)return;try{onRootUpdate(rootSnapshot(root,simCount,chess))}catch{}};const startTime=clockMs();let sim=0;if(cfg.batchSize<=1){while(sim<cfg.nSimulations){if(effectiveTimeBudgetMs!==null&&clockMs()-startTime>=effectiveTimeBudgetMs){break}await this._simulate(root,chess);sim++;if(sim===1||sim%16===0)notifyRoot(sim)}}else{if(sim<cfg.nSimulations&&(effectiveTimeBudgetMs===null||clockMs()-startTime<effectiveTimeBudgetMs)){const value=await this._expand(root,chess,true);this._backup([root],value);sim++;notifyRoot(sim)}while(sim<cfg.nSimulations){if(effectiveTimeBudgetMs!==null&&clockMs()-startTime>=effectiveTimeBudgetMs){break}const maxRoundBudget=Math.min(cfg.batchSize,cfg.nSimulations-sim);const remainingMs=effectiveTimeBudgetMs===null?Infinity:effectiveTimeBudgetMs-(clockMs()-startTime);const roundBudget=this._chooseAdaptiveBatchSize(remainingMs,maxRoundBudget);if(roundBudget===0)break;const completed=await this._runBatchedRound(root,chess,roundBudget,startTime,effectiveTimeBudgetMs);sim+=completed;if(completed>0)notifyRoot(sim);if(completed===0)break}}this.lastSimCount=sim;if(cfg.reuseTree){this._root=root;this._rootHistory=chess.history({verbose:true});this._rootFen=chess.fen()}if(root.children.size===0){const moves=chess.moves({verbose:true});if(moves.length===0){throw new Error("search() called on a position with no legal moves")}return chess.moveToSan({from:moves[0].from,to:moves[0].to,promotion:moves[0].promotion})}let bestEdge=null;let bestVisits=-1;for(const edge of root.children.values()){if(edge.node.visitCount>bestVisits){bestVisits=edge.node.visitCount;bestEdge=edge}}return chess.moveToSan({from:bestEdge.from,to:bestEdge.to,promotion:bestEdge.promotion})}_acquireRoot(chess){if(this._root===null||this._rootHistory===null||this._rootFen===null)return null;const oldHistory=this._rootHistory;const newHistory=chess.history({verbose:true});if(newHistory.length<oldHistory.length)return null;for(let i=0;i<oldHistory.length;i++){const oldMove=oldHistory[i];const newMove=newHistory[i];if(newMove.from!==oldMove.from||newMove.to!==oldMove.to||newMove.promotion!==oldMove.promotion){return null}}const elapsed=newHistory.slice(oldHistory.length);const replay=new Chess(this._rootFen);for(const move of elapsed){try{replay.move({from:move.from,to:move.to,promotion:move.promotion})}catch{return null}}if(replay.fen()!==chess.fen()){return null}let node=this._root;for(const move of elapsed){if(!node.expanded||node.terminal)return null;let moveIdx;try{moveIdx=moveToIndex(move.from,move.to,move.promotion)}catch{return null}const edge=node.children.get(moveIdx);if(edge===void 0)return null;node=edge.node}return node}_recordDefaultBatchTiming(elapsedMs){if(!Number.isFinite(elapsedMs)||elapsedMs<=0)return;if(this.batchDurationMs===null){this.batchDurationMs=elapsedMs}else{this.batchDurationMs=(1-ADAPTIVE_BATCH_EMA_ALPHA)*this.batchDurationMs+ADAPTIVE_BATCH_EMA_ALPHA*elapsedMs}this.batchDurationSamples++}_chooseAdaptiveBatchSize(remainingMs,maxBatchSize=this.config.batchSize){return adaptiveBatchSizeForBudget(maxBatchSize,remainingMs,this.batchDurationMs,this.config.batchSize)}_shouldDeferEval(node,path,currentVisitCounted){const cfg=this.config;if(!cfg.deferredEval)return false;if(path.length<=1)return false;const visits=currentVisitCounted?node.visitCount:node.visitCount+1;return visits<cfg.deferredEvalMinVisits}async _simulate(root,chess){let node=root;const path=[node];let pushed=0;while(node.expanded&&!node.terminal){if(this.config.lazyExpansion)this._maybeExpandLazy(node,chess);const moveIdx=this._selectPuct(node);const edge=node.children.get(moveIdx);chess._applyMove({from:edge.from,to:edge.to,promotion:edge.promotion});pushed++;node=edge.node;path.push(node)}let value;if(node.terminal){value=node.terminalValue}else if(this._shouldDeferEval(node,path,false)&&!chess.isCheckmate()&&!isDraw(chess)){value=0}else{value=await this._expand(node,chess,node===root)}this._backup(path,value);for(let i=0;i<pushed;i++){chess.undo()}}async _runBatchedRound(root,chess,roundBudget,startTime,timeBudgetMs){const roundStart=clockMs();const pending=new Map;const order=[];let completed=0;for(let i=0;i<roundBudget;i++){if(timeBudgetMs!==null&&clockMs()-startTime>=timeBudgetMs){break}const result=this._selectLeafVL(root,chess);if(result.kind==="fresh"){let entry=pending.get(result.node);if(entry===void 0){entry={node:result.node,chess:result.chess,simulations:[]};pending.set(result.node,entry);order.push(result.node)}entry.simulations.push(result.path)}else{this._finishImmediate(result);completed++}}if(pending.size>0){const entries=order.map(node=>pending.get(node));const boards=entries.map(entry=>entry.chess);const{policyLogits,wdlLogits}=await this.evaluator.evaluateBatch(boards);for(let i=0;i<entries.length;i++){completed+=this._finishFreshEntry(entries[i],policyLogits[i],wdlLogits[i])}}if(order.length===this.config.batchSize){this._recordDefaultBatchTiming(clockMs()-roundStart)}return completed}_selectLeafVL(root,chess){let node=root;const path=[node];let pushed=0;while(true){if(node.terminal){for(let i=0;i<pushed;i++)chess.undo();return{kind:"terminal",path,value:node.terminalValue}}if(!node.expanded){if(chess.isCheckmate()){node.terminal=true;node.expanded=true;node.terminalValue=-1;for(let i=0;i<pushed;i++)chess.undo();return{kind:"terminal",path,value:-1}}if(isDraw(chess)){node.terminal=true;node.expanded=true;node.terminalValue=0;for(let i=0;i<pushed;i++)chess.undo();return{kind:"terminal",path,value:0}}if(this._shouldDeferEval(node,path,true)){for(let i=0;i<pushed;i++)chess.undo();return{kind:"deferred",path,value:0}}const boardCopy=new Chess(chess.fen());for(let i=0;i<pushed;i++)chess.undo();return{kind:"fresh",path,node,chess:boardCopy}}if(this.config.lazyExpansion)this._maybeExpandLazy(node,chess);const moveIdx=this._selectPuct(node);const edge=node.children.get(moveIdx);chess._applyMove({from:edge.from,to:edge.to,promotion:edge.promotion});pushed++;edge.node.visitCount++;edge.node.valueSum+=1;node=edge.node;path.push(node)}}_finishImmediate(result){this._correctValue(result.path,result.value)}_finishFreshEntry(entry,policy,wdl){const value=this._buildChildren(entry.node,entry.chess,policy,wdl);for(const path of entry.simulations){this._correctValue(path,value)}return entry.simulations.length}_buildChildren(node,chess,policy,wdl,eager=false){if(!eager&&this.config.lazyExpansion===true){return this._buildChildrenLazy(node,chess,policy,wdl)}const legalMoves=chess._legalMoves();const moveIndices=legalMoves.map(m=>moveToIndex(sqName(m.from),sqName(m.to),m.promotion?PROMO_LETTER[m.promotion]:void 0));const legalLogits=moveIndices.map(idx=>policy[idx]);const priors=softmax(legalLogits);for(let i=0;i<legalMoves.length;i++){const m=legalMoves[i];node.children.set(moveIndices[i],{node:new Node(priors[i]),from:m.from,to:m.to,promotion:m.promotion})}node.expanded=true;const wdlProbs=softmax(Array.from(wdl));const score=wdlToScore(wdlProbs);return 2*score-1}_buildChildrenLazy(node,chess,policy,wdl){node.expanded=true;const board=chess._board;const usWhite=chess._turn===0;const candIdx=[];for(let frm=0;frm<64;frm++){const p=board[frm];if(p===0||p<=6!==usWhite)continue;const base=frm<<6;for(let to=0;to<64;to++){const idx=base|to;if(!this._candidateShapeLegal(chess,frm,to,null))continue;candIdx.push(idx)}}for(let k=0;k<PROMO_MOVE_TABLE.length;k++){const m=PROMO_MOVE_TABLE[k];if(!this._candidateShapeLegal(chess,m.from,m.to,m.promotion))continue;candIdx.push(4096+k)}node.lazy={candIdx,logits:policy,maxL:null,sumExp:0};const wdlProbs=softmax(Array.from(wdl));const score=wdlToScore(wdlProbs);return 2*score-1}_materializeNextLazyChild(node,chess){const lazy=node.lazy;const candIdx=lazy.candIdx;while(candIdx.length>0){let bi=0;let bestL=-Infinity;for(let i=0;i<candIdx.length;i++){const l=lazy.logits[candIdx[i]];if(l>bestL){bestL=l;bi=i}}const best=candIdx[bi];candIdx[bi]=candIdx[candIdx.length-1];candIdx.pop();const cand=decodeMoveIndex(best);if(!this._candidateLeavesKingSafe(chess,cand))continue;if(lazy.maxL===null){lazy.maxL=bestL}const weight=Math.exp(bestL-lazy.maxL);lazy.sumExp+=weight;node.children.set(best,{node:new Node(weight/lazy.sumExp),from:cand.from,to:cand.to,promotion:cand.promotion,weight});for(const edge of node.children.values()){edge.node.prior=edge.weight/lazy.sumExp}return true}return false}_maybeExpandLazy(node,chess){const lazy=node.lazy;if(lazy===null||lazy.candIdx.length===0)return;if(node.children.size>0){for(const edge of node.children.values()){if(edge.node.visitCount===0)return}}this._materializeNextLazyChild(node,chess)}_candidateLeavesKingSafe(chess,cand){const board=chess._board;const usWhite=chess._turn===0;const p=board[cand.from];const pt=usWhite?p:p-6;chess._applyMove(cand);let kingSq;if(pt===KING){kingSq=cand.to}else{kingSq=-1;for(let sq=0;sq<64;sq++){const b=board[sq];if(b!==0&&b<=6===usWhite&&(usWhite?b:b-6)===KING){kingSq=sq;break}}}const leavesKingInCheck=isAttacked(chess._board,kingSq,usWhite?1:0);chess.undo();return!leavesKingInCheck}_candidateShapeLegal(chess,from,to,promotion){const board=chess._board;const usWhite=chess._turn===0;const p=board[from];if(p===0||p<=6!==usWhite)return false;const pt=usWhite?p:p-6;if(pt!==PAWN&&promotion!==null)return false;const t=board[to];if(t!==0&&t<=6===usWhite)return false;const ff=from&7,fr=from>>3,tf=to&7,tr=to>>3;const df=tf-ff,dr=tr-fr;if(pt===PAWN){const dir=usWhite?1:-1;const startRank=usWhite?1:6;const lastRank=usWhite?7:0;if(df===0){if(t!==0)return false;if(dr===dir){}else if(dr===2*dir&&fr===startRank&&board[(fr+dir)*8+ff]===0){}else{return false}}else if(df===1||df===-1){if(dr!==dir)return false;const isEpCapture=t===0&&to===chess._epSquare;if(!isEpCapture&&t===0)return false}else{return false}if(tr===lastRank!==(promotion!==null))return false}else if(pt===KNIGHT){const adf=Math.abs(df),adr=Math.abs(dr);if(!(adf===1&&adr===2||adf===2&&adr===1))return false}else if(pt===BISHOP||pt===ROOK||pt===QUEEN){const diagonal=Math.abs(df)===Math.abs(dr);const straight=df===0||dr===0;if(pt===BISHOP&&!diagonal)return false;if(pt===ROOK&&!straight)return false;if(pt===QUEEN&&!(diagonal||straight))return false;const sf=Math.sign(df),sr=Math.sign(dr);let f=ff+sf,r=fr+sr;while(f!==tf||r!==tr){if(board[r*8+f]!==0)return false;f+=sf;r+=sr}}else if(pt===KING){if(Math.abs(df)<=1&&Math.abs(dr)<=1)return true;return this._castlingCandidateLegal(chess,from,to)}else{return false}return true}_castlingCandidateLegal(chess,from,to){const board=chess._board;const white=chess._turn===0;const rank=white?0:7;if(from!==rank*8+4)return false;const kingSide=to===rank*8+6;const queenSide=to===rank*8+2;if(!kingSide&&!queenSide)return false;const rightsBit=white?kingSide?1:2:kingSide?4:8;if((chess._castling&rightsBit)===0)return false;const rookSq=rank*8+(kingSide?7:0);const rook=board[rookSq];if(rook===0||rook<=6!==white||(white?rook:rook-6)!==ROOK)return false;if(kingSide){if(board[rank*8+5]!==0||board[rank*8+6]!==0)return false}else{for(let f=3;f>=1;f--){if(board[rank*8+f]!==0)return false}}const enemy=white?1:0;const danger=kingDangerSquares(board,from,enemy);const pathFiles=kingSide?[4,5,6]:[4,3,2];for(const f of pathFiles){if(danger.has(rank*8+f))return false}return true}async _expand(node,chess,eager=false){if(chess.isCheckmate()){node.terminal=true;node.expanded=true;node.terminalValue=-1;return node.terminalValue}if(isDraw(chess)){node.terminal=true;node.expanded=true;node.terminalValue=0;return node.terminalValue}const{policyLogits,wdlLogits}=await this.evaluator.evaluateBatch([chess]);return this._buildChildren(node,chess,policyLogits[0],wdlLogits[0],eager)}_selectPuct(node){const cfg=this.config;const sqrtN=Math.sqrt(Math.max(node.visitCount,1));let bestScore=-Infinity;let bestIdx=null;for(const[idx,edge]of node.children){const child=edge.node;const q=child.visitCount===0?0:-(child.valueSum/child.visitCount);const u=cfg.cPuct*child.prior*sqrtN/(1+child.visitCount);const score=q+u;if(score>bestScore){bestScore=score;bestIdx=idx}}return bestIdx}_correctValue(path,value){let v=value;const root=path[0];for(let i=path.length-1;i>=0;i--){const node=path[i];if(node===root){node.visitCount++;node.valueSum+=v}else{node.valueSum+=v-1}v=-v}}_backup(path,value){let v=value;for(let i=path.length-1;i>=0;i--){const node=path[i];node.visitCount++;node.valueSum+=v;v=-v}}}class OrtEvaluator{constructor(session,ort,{evalCacheMaxEntries=0}={}){this.session=session;this.ort=ort;this.evalCacheMaxEntries=evalCacheMaxEntries;this.evalCache=new Map;this.evalCacheHits=0;this.evalCacheMisses=0}async evaluateBatch(chessInstances){const n=chessInstances.length;const allTokens=new BigInt64Array(n*SEQ_LEN);const keys=new Array(n);for(let i=0;i<n;i++){const tokens=fenToTokens(chessInstances[i].fen());let key="";for(let j=0;j<SEQ_LEN;j++){const t=tokens[j];allTokens[i*SEQ_LEN+j]=BigInt(t);key+=String.fromCharCode(t)}keys[i]=key}if(this.evalCacheMaxEntries<=0){const inputTensor=new this.ort.Tensor("int64",allTokens,[n,SEQ_LEN]);const results2=await this.session.run({tokens:inputTensor});const policyLogits=[];const wdlLogits=[];for(let i=0;i<n;i++){policyLogits.push(results2.policy.data.subarray(i*MOVE_SPACE,(i+1)*MOVE_SPACE));wdlLogits.push(results2.wdl.data.subarray(i*3,(i+1)*3))}return{policyLogits,wdlLogits}}const results={policyLogits:new Array(n),wdlLogits:new Array(n)};const missSlots=new Map;for(let i=0;i<n;i++){const cached=this.evalCache.get(keys[i]);if(cached!==void 0){this.evalCache.delete(keys[i]);this.evalCache.set(keys[i],cached);this.evalCacheHits++;results.policyLogits[i]=cached.policy;results.wdlLogits[i]=cached.wdl}else{this.evalCacheMisses++;let slot=missSlots.get(keys[i]);if(slot===void 0){slot={instIdx:i,slots:[i]};missSlots.set(keys[i],slot)}else{slot.slots.push(i)}}}const m=missSlots.size;if(m>0){const missTokens=new BigInt64Array(m*SEQ_LEN);const keyToOutIdx=new Map;let out=0;for(const[key,slot]of missSlots){missTokens.set(allTokens.subarray(slot.instIdx*SEQ_LEN,(slot.instIdx+1)*SEQ_LEN),out*SEQ_LEN);keyToOutIdx.set(key,out);out++}const inputTensor=new this.ort.Tensor("int64",missTokens,[m,SEQ_LEN]);const runRes=await this.session.run({tokens:inputTensor});for(const[key,slot]of missSlots){const oi=keyToOutIdx.get(key);const policyCopy=Float32Array.from(runRes.policy.data.subarray(oi*MOVE_SPACE,(oi+1)*MOVE_SPACE));const wdlCopy=Float32Array.from(runRes.wdl.data.subarray(oi*3,(oi+1)*3));this.evalCache.set(key,{policy:policyCopy,wdl:wdlCopy});while(this.evalCache.size>this.evalCacheMaxEntries){const oldest=this.evalCache.keys().next().value;this.evalCache.delete(oldest)}for(let s=0;s<slot.slots.length;s++){const i=slot.slots[s];results.policyLogits[i]=policyCopy;results.wdlLogits[i]=wdlCopy}}}return results}}const STM_STATE_DIM=8;const STM_MOVE_SPACE=4272;const STM_PIECE_TYPE={p:1,n:2,b:3,r:4,q:5,k:6};const STM_PROMO_PIECES=[2,3,4,5];function stmMirrorSquare(square){return(7-Math.floor(square/8))*8+square%8}function stmMoveIndex(from,to,promotion=null){if(promotion===null||promotion===void 0)return from*64+to;let index=64*64;for(let candidateFrom=0;candidateFrom<64;candidateFrom++){const fromRank=Math.floor(candidateFrom/8);const fromFile=candidateFrom%8;for(let candidateTo=0;candidateTo<64;candidateTo++){const toRank=Math.floor(candidateTo/8);const toFile=candidateTo%8;const isPromotion=fromRank===6&&toRank===7||fromRank===1&&toRank===0;if(!isPromotion||Math.abs(fromFile-toFile)>1)continue;for(const piece of STM_PROMO_PIECES){if(candidateFrom===from&&candidateTo===to&&piece===promotion)return index;index++}}}throw new Error(`stmMoveIndex: invalid promotion tuple ${from},${to},${promotion}`)}const STM_MIRROR_INDEX=(()=>{const table=new Uint32Array(STM_MOVE_SPACE);let index=0;for(let from=0;from<64;from++){for(let to=0;to<64;to++){table[index++]=stmMoveIndex(stmMirrorSquare(from),stmMirrorSquare(to))}}for(let from=0;from<64;from++){const fromRank=Math.floor(from/8);const fromFile=from%8;for(let to=0;to<64;to++){const toRank=Math.floor(to/8);const toFile=to%8;const isPromotion=fromRank===6&&toRank===7||fromRank===1&&toRank===0;if(!isPromotion||Math.abs(fromFile-toFile)>1)continue;for(const promotion of STM_PROMO_PIECES){table[index++]=stmMoveIndex(stmMirrorSquare(from),stmMirrorSquare(to),promotion)}}}if(index!==STM_MOVE_SPACE)throw new Error(`STM move table has ${index} entries`);return table})();function stmParsePlacement(placement){const absolute=new Uint8Array(64);const ranks=placement.split("/");if(ranks.length!==8)throw new Error("stm encoder: invalid board placement");for(let row=0;row<8;row++){const rank=7-row;let file=0;for(const symbol of ranks[row]){if(symbol>="1"&&symbol<="8"){file+=Number(symbol);continue}const pieceType=STM_PIECE_TYPE[symbol.toLowerCase()];if(pieceType===void 0)throw new Error(`stm encoder: invalid piece ${symbol}`);if(file>=8)throw new Error("stm encoder: board rank is wider than eight files");absolute[rank*8+file]=symbol===symbol.toUpperCase()?pieceType:pieceType+6;file++}if(file!==8)throw new Error("stm encoder: board rank is not eight files wide")}return absolute}function stmRepetitionCount(chess){if(typeof chess?._positionKey==="function"&&chess._repCounts instanceof Map){return Number(chess._repCounts.get(chess._positionKey())??1)}return typeof chess?._isThreefoldRepetition==="function"&&chess._isThreefoldRepetition()?3:1}function stmBoardInputs(chess){return stmEncodeFen(chess.fen(),{repetitionCount:stmRepetitionCount(chess)})}function stmEncodeFen(fen,{repetitionCount=1}={}){const[placement,activeColor,castling,epSquare,halfmoveStr]=fen.split(" ");const absolute=stmParsePlacement(placement);const mirror=activeColor==="b";const squareIds=new BigInt64Array(64);for(let square=0;square<64;square++){let piece=absolute[square];if(mirror&&piece!==0)piece=piece<=6?piece+6:piece-6;const target=mirror?stmMirrorSquare(square):square;squareIds[target]=BigInt(piece)}const state=new Float32Array(STM_STATE_DIM);state[0]=1;if(mirror){state[1]=castling.includes("k")?1:0;state[2]=castling.includes("q")?1:0;state[3]=castling.includes("K")?1:0;state[4]=castling.includes("Q")?1:0}else{state[1]=castling.includes("K")?1:0;state[2]=castling.includes("Q")?1:0;state[3]=castling.includes("k")?1:0;state[4]=castling.includes("q")?1:0}state[5]=epSquare&&epSquare!=="-"?(epSquare.charCodeAt(0)-"a".charCodeAt(0))/7:-1;const halfmove=Number(halfmoveStr??0);state[6]=Math.min(Math.floor(halfmove/5),9)/9;const count=Number.isFinite(Number(repetitionCount))?Number(repetitionCount):1;state[7]=Math.min(Math.max(0,count-1),2)/2;return{squareIds,state,mirror}}function stmMirrorPolicyIndex(index){if(!Number.isInteger(index)||index<0||index>=STM_MOVE_SPACE){throw new Error(`stmMirrorPolicyIndex: index out of range: ${index}`)}return STM_MIRROR_INDEX[index]}function stmMirrorPolicyLogits(logits){if(!logits||logits.length!==STM_MOVE_SPACE){throw new Error(`stmMirrorPolicyLogits: expected ${STM_MOVE_SPACE} logits`)}const output=new Float32Array(STM_MOVE_SPACE);for(let absoluteIndex=0;absoluteIndex<STM_MOVE_SPACE;absoluteIndex++){output[absoluteIndex]=logits[STM_MIRROR_INDEX[absoluteIndex]]}return output}const CF_STATE_DIM=8;const CF_MOVE_SPACE=4272;const CF_PIECE_TYPE={p:1,n:2,b:3,r:4,q:5,k:6};const CF_DEFAULT_TIME_BUDGET_MS=9200;const CF_MOVE_TIME_LIMIT_MARGIN=.92;const CF_MAX_SIMULATIONS=1e5;const CF_MCTS_BATCH_SIZE=16;function cfBoardInputs(chess,stmCanonical=false){if(stmCanonical)return stmBoardInputs(chess);const fen=chess.fen();const[placement,activeColor,castling,epSquare,halfmoveStr]=fen.split(" ");const squareIds=new BigInt64Array(64);const ranks=placement.split("/");if(ranks.length!==8)throw new Error("invalid board placement");for(let row=0;row<8;row++){const rank=7-row;let file=0;for(const symbol of ranks[row]){if(symbol>="1"&&symbol<="8"){file+=Number(symbol);continue}const pieceType=CF_PIECE_TYPE[symbol.toLowerCase()];if(pieceType===void 0)throw new Error(`invalid piece ${symbol}`);if(file>=8)throw new Error("invalid board rank width");squareIds[rank*8+file]=BigInt(symbol===symbol.toUpperCase()?pieceType:pieceType+6);file++}if(file!==8)throw new Error("invalid board rank width")}const state=new Float32Array(CF_STATE_DIM);state[0]=activeColor==="w"?1:0;state[1]=castling.includes("K")?1:0;state[2]=castling.includes("Q")?1:0;state[3]=castling.includes("k")?1:0;state[4]=castling.includes("q")?1:0;state[5]=epSquare&&epSquare!=="-"?(epSquare.charCodeAt(0)-"a".charCodeAt(0))/7:-1;const halfmove=Number(halfmoveStr??0);state[6]=Math.min(Math.floor(halfmove/5),9)/9;if(typeof chess._positionKey==="function"&&chess._repCounts instanceof Map){const occurrences=Number(chess._repCounts.get(chess._positionKey())??0);const priorRepetitions=Math.max(0,occurrences-1);state[7]=Math.min(priorRepetitions,2)/2}else{state[7]=typeof chess._isThreefoldRepetition==="function"&&chess._isThreefoldRepetition()?1:0}return{squareIds,state,mirror:false}}export class CFLiteOrtEvaluator{constructor(session,ort,contempt=0,{evalCacheMaxEntries=0,stmCanonical=false}={}){this.session=session;this.ort=ort;this.contempt=Number.isFinite(Number(contempt))?Number(contempt):0;this.stmCanonical=stmCanonical===true;this.evalCacheMaxEntries=evalCacheMaxEntries;this.evalCache=new Map;this.evalCacheHits=0;this.evalCacheMisses=0}async evaluateBatch(chessInstances){const n=chessInstances.length;if(n===0)return{policyLogits:[],wdlLogits:[]};const allSquareIds=new BigInt64Array(n*64);const allState=new Float32Array(n*CF_STATE_DIM);const allContempt=new Float32Array(n);const mirrors=new Array(n);const keys=new Array(n);for(let i=0;i<n;i++){const inputs=cfBoardInputs(chessInstances[i],this.stmCanonical);allSquareIds.set(inputs.squareIds,i*64);allState.set(inputs.state,i*CF_STATE_DIM);allContempt[i]=this.contempt;mirrors[i]=this.stmCanonical&&inputs.mirror===true;keys[i]=cfEvalCacheKey(inputs.squareIds,inputs.state)}if(this.evalCacheMaxEntries<=0){const results2=await this.session.run({square_ids:new this.ort.Tensor("int64",allSquareIds,[n,64]),state_features:new this.ort.Tensor("float32",allState,[n,CF_STATE_DIM]),contempt:new this.ort.Tensor("float32",allContempt,[n])});return cfScatter(results2,n,mirrors)}const results={policyLogits:new Array(n),wdlLogits:new Array(n)};const missSlots=new Map;for(let i=0;i<n;i++){const cached=this.evalCache.get(keys[i]);if(cached!==void 0){this.evalCache.delete(keys[i]);this.evalCache.set(keys[i],cached);this.evalCacheHits++;results.policyLogits[i]=cfPolicyForBoard(cached.policy,mirrors[i]);results.wdlLogits[i]=cached.wdl}else{this.evalCacheMisses++;let slot=missSlots.get(keys[i]);if(slot===void 0){slot={instIdx:i,slots:[i]};missSlots.set(keys[i],slot)}else{slot.slots.push(i)}}}const m=missSlots.size;if(m>0){const mSquareIds=new BigInt64Array(m*64);const mState=new Float32Array(m*CF_STATE_DIM);const mContempt=new Float32Array(m);let out=0;const keyToOutIdx=new Map;for(const[key,slot]of missSlots){const bi=slot.instIdx;mSquareIds.set(allSquareIds.subarray(bi*64,(bi+1)*64),out*64);mState.set(allState.subarray(bi*CF_STATE_DIM,(bi+1)*CF_STATE_DIM),out*CF_STATE_DIM);mContempt[out]=this.contempt;keyToOutIdx.set(key,out);out++}const runRes=await this.session.run({square_ids:new this.ort.Tensor("int64",mSquareIds,[m,64]),state_features:new this.ort.Tensor("float32",mState,[m,CF_STATE_DIM]),contempt:new this.ort.Tensor("float32",mContempt,[m])});for(const[key,slot]of missSlots){const oi=keyToOutIdx.get(key);const policyCopy=Float32Array.from(runRes.policy.data.subarray(oi*CF_MOVE_SPACE,(oi+1)*CF_MOVE_SPACE));const wdlCopy=Float32Array.from(runRes.wdl.data.subarray(oi*3,(oi+1)*3));this.evalCache.set(key,{policy:policyCopy,wdl:wdlCopy});while(this.evalCache.size>this.evalCacheMaxEntries){const oldest=this.evalCache.keys().next().value;this.evalCache.delete(oldest)}for(const i of slot.slots){results.policyLogits[i]=cfPolicyForBoard(policyCopy,mirrors[i]);results.wdlLogits[i]=wdlCopy}}}return results}}function cfEvalCacheKey(squareIds,state){const bytes=new Uint8Array(squareIds.buffer,squareIds.byteOffset,squareIds.byteLength);const sbytes=new Uint8Array(state.buffer,state.byteOffset,state.byteLength);let key="";const all=[bytes,sbytes];for(const arr of all){for(let off=0;off<arr.length;off+=4096){key+=String.fromCharCode.apply(null,arr.subarray(off,off+4096))}key+="|"}return key}function cfPolicyForBoard(policy,mirror){return mirror?stmMirrorPolicyLogits(policy):policy}function cfScatter(results,n,mirrors=null){const policyLogits=[];const wdlLogits=[];for(let i=0;i<n;i++){const policy=results.policy.data.subarray(i*CF_MOVE_SPACE,(i+1)*CF_MOVE_SPACE);policyLogits.push(cfPolicyForBoard(policy,mirrors?.[i]===true));wdlLogits.push(results.wdl.data.subarray(i*3,(i+1)*3))}return{policyLogits,wdlLogits}}function cfResolveModelBytes(artifacts){if(artifacts instanceof Map)return artifacts.get("model");if(artifacts&&typeof artifacts==="object")return artifacts.model;return void 0}function cfClockMs(){return typeof performance!=="undefined"?performance.now():Date.now()}export async function loadPackage({artifacts,ort,config}){const modelBytes=cfResolveModelBytes(artifacts);if(modelBytes===void 0)throw new Error('missing "model" artifact');const packageConfig=config&&typeof config==="object"?config:{};const executionProviders=packageConfig.executionProviders||["webgpu","wasm"];const session=await ort.InferenceSession.create(modelBytes,{executionProviders});const contempt=Number.isFinite(Number(packageConfig.contempt))?Number(packageConfig.contempt):0;const stmCanonical=packageConfig.encoder==="stm-canonical";const evalCacheMaxEntries=Number.isFinite(Number(packageConfig.evalCacheMaxEntries))?Number(packageConfig.evalCacheMaxEntries):2048;const mctsBatchSize=Number.isFinite(Number(packageConfig.mctsBatchSize))?Number(packageConfig.mctsBatchSize):CF_MCTS_BATCH_SIZE;const warmupEvaluator=new CFLiteOrtEvaluator(session,ort,contempt,{stmCanonical});try{const warmupBoard=new Chess;await warmupEvaluator.evaluateBatch([warmupBoard]);await warmupEvaluator.evaluateBatch(new Array(mctsBatchSize).fill(warmupBoard))}catch{}const evaluator=new CFLiteOrtEvaluator(session,ort,contempt,{evalCacheMaxEntries,stmCanonical});const defaultTimeBudget=Number.isFinite(Number(packageConfig.timeBudgetMs))?Number(packageConfig.timeBudgetMs):CF_DEFAULT_TIME_BUDGET_MS;const nSimulations=Number.isFinite(Number(packageConfig.nSimulations))?Number(packageConfig.nSimulations):CF_MAX_SIMULATIONS;return{async newGame(){const mcts=new MCTS(evaluator,new MCTSConfig({nSimulations,timeBudgetMs:defaultTimeBudget,batchSize:mctsBatchSize}));return{async chooseMove({history,legalMoves,moveTimeLimitMs}){if(!Array.isArray(legalMoves)||legalMoves.length===0){throw new Error("chooseMove called without legalMoves")}const started=cfClockMs();try{const chess=new Chess;for(const san of history||[]){const moved=chess.move(san);if(!moved)throw new Error(`invalid SAN history: ${san}`)}const requested=Number(moveTimeLimitMs);const searchBudget=Number.isFinite(requested)&&requested>0?requested*CF_MOVE_TIME_LIMIT_MARGIN:defaultTimeBudget;const chosen=await mcts.search(chess,{timeBudgetMs:searchBudget});if(legalMoves.includes(chosen))return chosen;return legalMoves[0]}catch{return legalMoves[0]}finally{void started}},async dispose(){}}},async dispose(){if(typeof session.release==="function")await session.release()}}} | |