<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>国际象棋|离线进化版</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:Arial}
body{display:flex;flex-direction:column;align-items:center;padding:16px;background:#222;color:#fff}
#panel{margin-bottom:10px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center}
button{padding:6px 12px;font-size:14px;cursor:pointer}
#info{font-size:20px;min-height:28px}
#boardWrap{position:relative}
#board{display:grid;grid-template-columns:repeat(8,64px);grid-template-rows:repeat(8,64px);border:4px solid #111}
.square{width:64px;height:64px;cursor:pointer}
.light{background:#f0d9b5}
.dark{background:#b58863}
.possible{background:rgba(0,170,0,0.4)}
.possible-capture{background:rgba(200,0,0,0.4)}
#pieceLayer{position:absolute;top:0;left:0;width:512px;height:512px;pointer-events:none}
#animLayer{position:absolute;top:0;left:0;width:512px;height:512px;pointer-events:none}
.piece{position:absolute;width:64px;height:64px;display:flex;align-items:center;justify-content:center;font-size:52px;transform:translate(0,-2px);text-align:center;line-height:1;backface-visibility:hidden;-webkit-font-smoothing:antialiased;transition:transform .3s ease}
#pgnBox{margin-top:12px;width:512px;height:100px;color:#000;background:#fff;padding:8px;overflow:auto}
#promoModal{display:none;position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,.6);align-items:center;justify-content:center;z-index:100}
.promoPanel{background:white;padding:12px;border-radius:8px;display:flex;gap:8px;cursor:pointer}
#expInfo{font-size:12px;color:#888}
select{padding:4px;font-size:13px}
</style>
</head>
<body>
<div id="panel">
    <div id="info"></div>
    <select id="skillSel">
        <option value="3">轻松</option>
        <option value="4" selected>标准</option>
        <option value="5">困难</option>    </select>
    <button id="exportPgnBtn">导出PGN</button>
    <button id="modeBtn">切换双人/人机</button>
    <button id="clearExpBtn" title="清除 AI 从历史对局中学到的偏好">清除AI经验</button>
</div>
<div id="boardWrap">
    <div id="board"></div>
    <div id="pieceLayer"></div>
    <div id="animLayer"></div>
</div>
<textarea id="pgnBox" readonly placeholder="PGN棋谱"></textarea>
<div id="expInfo"></div>
<div id="promoModal">
    <div class="promoPanel" data-p="Q">♕</div>
    <div class="promoPanel" data-p="R">♖</div>
    <div class="promoPanel" data-p="B">♗</div>
    <div class="promoPanel" data-p="N">♘</div>
</div>
<script>
const pieceChar={'K':'♔','Q':'♕','R':'♖','B':'♗','N':'♘','P':'♙','k':'♚','q':'♛','r':'♜','b':'♝','n':'♞','p':'♟','':''};
const startBoard=[['r','n','b','q','k','b','n','r'],['p','p','p','p','p','p','p','p'],['','','','','','','',''],['','','','','','','',''],['','','','','','','',''],['','','','','','','',''],['P','P','P','P','P','P','P','P'],['R','N','B','Q','K','B','N','R']];
// 开局库:key=已走UCI序列, value=黑方应法列表
const openingBook={
    "e2e4":["e7e5","c7c5","e7e6","c7c6"],
    "d2d4":["d7d5","g8f6","e7e6"],
    "c2c4":["e7e5","d7d5"],
    "e2e4e7e5":["g8f6","b8c6"],
    "e2e4c7c5":["g8f6","b8c6","d7d6"],
    "e2e4e7e6":["d7d5"],
    "e2e4c7c6":["d7d5"],
    "d2d4g8f6":["e7e6","g7g6"],
    "d2d4d7d5":["e7e6","g8f6","c7c6"],
    "e2e4e7e5g1f3":["b8c6","g8f6","g8e7"],
    "e2e4c7c5g1f3":["d7d6","b8c6","e7e6"],
    "e2e4e7e5g1f3b8c6":["f8c5","g8f6","g8e7"],
    "e2e4c7c5g1f3d7d6":["b8c6","g8f6"],
    "e2e4e7e6d2d4":["d7d5"],
    "d2d4d7d5c2c4":["e7e6","g8f6","c7c6"],
    "e2e4e7e5d2d4":["e5d4"],
};
const pieceValue={p:100,n:320,b:330,r:500,q:900,k:20000};
const posTable={
p:[[0,0,0,0,0,0,0,0],[50,50,50,50,50,50,50,50],[10,10,20,30,30,20,10,10],[5,5,10,25,25,10,5,5],[0,0,0,20,20,0,0,0],[5,-5,-10,0,0,-10,-5,5],[5,10,10,-20,-20,10,10,5],[0,0,0,0,0,0,0,0]],
n:[[-50,-40,-30,-30,-30,-30,-40,-50],[-40,-20,0,0,0,0,-20,-40],[-30,0,10,15,15,10,0,-30],[-30,5,15,20,20,15,5,-30],[-30,0,15,20,20,15,0,-30],[-30,5,10,15,15,10,5,-30],[-40,-20,0,5,5,0,-20,-40],[-50,-40,-30,-30,-30,-30,-40,-50]],
b:[[-20,-10,-10,-10,-10,-10,-10,-20],[-10,0,0,0,0,0,0,-10],[-10,0,5,10,10,5,0,-10],[-10,5,5,10,10,5,5,-10],[-10,0,10,10,10,10,0,-10],[-10,10,10,10,10,10,10,-10],[-10,5,0,0,0,0,5,-10],[-20,-10,-10,-10,-10,-10,-10,-20]],
r:[[0,0,0,0,0,0,0,0],[5,10,10,10,10,10,10,5],[-5,0,0,0,0,0,0,-5],[-5,0,0,0,0,0,0,-5],[-5,0,0,0,0,0,0,-5],[-5,0,0,0,0,0,0,-5],[-5,0,0,0,0,0,0,-5],[0,0,0,5,5,0,0,0]],
q:[[-20,-10,-10,-5,-5,-10,-10,-20],[-10,0,0,0,0,0,0,-10],[-10,0,5,5,5,5,0,-10],[-5,0,5,5,5,5,0,-5],[0,0,5,5,5,5,0,-5],[-10,5,5,5,5,5,0,-10],[-10,0,5,0,0,0,0,-10],[-20,-10,-10,-5,-5,-10,-10,-20]],
k:[[-50,-50,-50,-50,-50,-50,-50,-50],[-50,-50,-50,-50,-50,-50,-50,-50],[-30,-30,-30,-30,-30,-30,-30,-30],[-20,-20,-20,-20,-20,-20,-20,-20],[-10,-10,-10,-10,-10,-10,-10,-10],[20,20,20,20,20,20,20,20],[40,40,40,40,40,40,40,40],[60,60,60,60,60,60,60,60]]};

let board=null,turnWhite=true,selected=null,pgnMoves=[],uciMoves=[],gameOver=false;
let state={enPassantTarget:null,castlingRights:{K:true,Q:true,k:true,q:true}};
let playVsAI=true,promoPending=null,aiThinking=false;
let historyTable=new Int16Array(4096); // 走法历史启发式
let positionHistory=new Map(); // 局面重复计数
let aiMoveLog=[]; // 本局 AI 走的 UCI 串,结束后用于学习
const $board=document.getElementById('board'),$info=document.getElementById('info'),$pgnBox=document.getElementById('pgnBox'),$promoModal=document.getElementById('promoModal'),$pieceLayer=document.getElementById('pieceLayer'),$animLayer=document.getElementById('animLayer'),$expInfo=document.getElementById('expInfo');

// ===== 经验持久化(AI 从历史对局学习)=====
const EXP_KEY='chess_ai_experience_v1';
let experience={};
try{experience=JSON.parse(localStorage.getItem(EXP_KEY)||'{}')}catch(e){experience={}}
function saveExperience(){try{localStorage.setItem(EXP_KEY,JSON.stringify(experience))}catch(e){}}
function expBonus(uci){
    const v=experience[uci];
    if(!v)return 0;
    // 限制影响幅度:最多 ±40 分,不影响主搜索,只打破均势震荡
    return Math.max(-40,Math.min(40,v));
}
function expLearn(winner){
    // winner: 'w' 白赢 / 'b' 黑赢 / 'd' 和棋
    // ai 是黑方
    for(const uci of aiMoveLog){
        if(winner==='b')experience[uci]=(experience[uci]||0)+8;
        else if(winner==='w')experience[uci]=(experience[uci]||0)-12;
        else experience[uci]=(experience[uci]||0)+1;
    }
    saveExperience();
    updateExpInfo();
}
function updateExpInfo(){
    const keys=Object.keys(experience);
    let wins=0,losses=0;
    for(const k of keys){if(experience[k]>0)wins++;else losses++}
    $expInfo.textContent=`已从 ${aiMoveLog.length} 步本局 + ${keys.length} 条历史走法经验中学习(偏好${wins} / 回避${losses})`;
}

document.getElementById('exportPgnBtn').onclick=exportPGN;
document.getElementById('modeBtn').onclick=()=>{playVsAI=!playVsAI;alert(playVsAI?"人机对战:你白方,AI黑方":"双人对战");render()};
document.getElementById('clearExpBtn').onclick=()=>{if(confirm('确定清除 AI 从所有历史对局学到的经验?')){experience={};saveExperience();updateExpInfo();alert('已清除')}};
document.querySelectorAll('.promoPanel').forEach(b=>{b.onclick=()=>{const pc=b.dataset.p,{sr,sc,tr,tc,w}=promoPending;board[tr][tc]=w?pc:pc.toLowerCase();promoPending=null;$promoModal.style.display='none';finishMove(sr,sc,tr,tc)}});

function isWhite(p){return p&&p===p.toUpperCase()}
function isBlack(p){return p&&p===p.toLowerCase()}
function copyBoard(b){return JSON.parse(JSON.stringify(b))}
function cloneState(s){return{enPassantTarget:s.enPassantTarget?{...s.enPassantTarget}:null,castlingRights:{...s.castlingRights}}}

function resetGame(){
    board=copyBoard(startBoard);
    state={enPassantTarget:null,castlingRights:{K:true,Q:true,k:true,q:true}};
    turnWhite=true;selected=null;pgnMoves=[];uciMoves=[];gameOver=false;promoPending=null;aiThinking=false;
    historyTable=new Int16Array(4096);
    positionHistory=new Map();
    aiMoveLog=[];
    render();updateExpInfo();
}

function findKing(b,white){
    for(let r=0;r<8;r++)for(let c=0;c<8;c++){const p=b[r][c];if(p&&p===(white?'K':'k'))return{r,c}}
    return null;
}
function isSquareAttacked(b,r,c,byWhite){
    for(let i=0;i<8;i++)for(let j=0;j<8;j++){
        const p=b[i][j];if(!p)continue;
        if(byWhite?!isWhite(p):isWhite(p))continue;
        if(getRawMoves(b,i,j).some(([tr,tc])=>tr===r&&tc===c))return true;
    }
    return false;
}
function getRawMoves(b,r,c){
    const p=b[r][c];if(!p)return[];
    const moves=[],w=isWhite(p),type=p.toLowerCase();
    function canGo(tr,tc){
        if(tr<0||tr>=8||tc<0||tc>=8)return false;
        const t=b[tr][tc];if(!t)return true;
        return!(w?isWhite(t):isBlack(t));
    }
    function addLine(dr,dc){
        let tr=r+dr,tc=c+dc;
        while(tr>=0&&tr<8&&tc>=0&&tc<8){
            const t=b[tr][tc];
            if(!t)moves.push([tr,tc]);
            else{if(w?isBlack(t):isWhite(t))moves.push([tr,tc]);break}
            tr+=dr;tc+=dc;
        }
    }
    if(type==='p'){
        const dir=w?-1:1,startR=w?6:1;
        if(b[r+dir]?.[c]===''){
            moves.push([r+dir,c]);
            if(r===startR&&b[r+2*dir][c]==='')moves.push([r+2*dir,c]);
        }
        for(const dc of[-1,1]){
            const tr=r+dir,tc=c+dc;
            if(tr>=0&&tr<8&&tc>=0&&tc<8){const t=b[tr][tc];if(t&&(w?isBlack(t):isWhite(t)))moves.push([tr,tc])}
        }
    }else if(type==='n'){
        const dirs=[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
        for(const[dr,dc]of dirs)if(canGo(r+dr,c+dc))moves.push([r+dr,c+dc]);
    }else if(type==='b')addLine(-1,-1),addLine(-1,1),addLine(1,-1),addLine(1,1);
    else if(type==='r')addLine(-1,0),addLine(1,0),addLine(0,-1),addLine(0,1);
    else if(type==='q'){addLine(-1,-1),addLine(-1,1),addLine(1,-1),addLine(1,1);addLine(-1,0),addLine(1,0),addLine(0,-1),addLine(0,1)}
    else{
        const dirs=[[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];
        for(const[dr,dc]of dirs)if(canGo(r+dr,c+dc))moves.push([r+dr,c+dc]);
    }
    return moves;
}
function getLegalMoves(b,r,c,st){
    st=st||state;
    const p=b[r][c],w=isWhite(p),raw=getRawMoves(b,r,c),legal=[];
    for(const[tr,tc]of raw){
        const captured=b[tr][tc];
        b[tr][tc]=p;b[r][c]='';
        // 过路兵模拟(简化:raw moves 里兵斜吃只可能吃真子,这里跳过过路兵检测)
        const kp=findKing(b,w);
        const safe=kp?!isSquareAttacked(b,kp.r,kp.c,!w):true;
        b[r][c]=p;b[tr][tc]=captured;
        if(safe)legal.push([tr,tc]);
    }
    if(p.toLowerCase()==='k'&&r===(w?7:0)){
        const kr=w?'K':'k',qr=w?'Q':'q';
        if(st.castlingRights[kr]&&b[r][5]===''&&b[r][6]===''){
            if(!isSquareAttacked(b,r,4,!w)&&!isSquareAttacked(b,r,5,!w)&&!isSquareAttacked(b,r,6,!w))legal.push([r,6]);
        }
        if(st.castlingRights[qr]&&b[r][1]===''&&b[r][2]===''&&b[r][3]===''){
            if(!isSquareAttacked(b,r,4,!w)&&!isSquareAttacked(b,r,3,!w)&&!isSquareAttacked(b,r,2,!w))legal.push([r,2]);
        }
    }
    if(p.toLowerCase()==='p'&&st.enPassantTarget){
        const epR=st.enPassantTarget.r,epC=st.enPassantTarget.c,dir=w?-1:1;
        if(r+dir===epR&&Math.abs(c-epC)===1)legal.push([epR,epC]);
    }
    return legal;
}
function getAllLegalMoves(isWhiteTurn,b,st){
    b=b||board;st=st||state;
    const moves=[];
    for(let r=0;r<8;r++)for(let c=0;c<8;c++){
        const p=b[r][c];if(!p)continue;
        if(isWhiteTurn?isWhite(p):isBlack(p)){
            const lm=getLegalMoves(b,r,c,st);
            for(const m of lm)moves.push({from:[r,c],to:m});
        }
    }
    return moves;
}
function inCheck(whiteTurn,b){
    b=b||board;
    const kp=findKing(b,whiteTurn);
    return kp?isSquareAttacked(b,kp.r,kp.c,!whiteTurn):false;
}

// ===== 增量走子(doMove / undoMove),避免每节点深拷贝 =====
function doMove(m){
    const sr=m.from[0],sc=m.from[1],tr=m.to[0],tc=m.to[1];
    const piece=board[sr][sc],w=isWhite(piece),type=piece.toLowerCase();
    const captured=board[tr][tc];
    let epCap=null,epCapR=-1;
    if(type==='p'&&!captured&&sc!==tc){
        const epR=w?tr+1:tr-1;
        if(board[epR]&&board[epR][tc]){epCap=board[epR][tc];epCapR=epR;board[epR][tc]=''}
    }
    board[tr][tc]=piece;board[sr][sc]='';
    let promoted=false;
    if(type==='p'&&(tr===0||tr===7)){board[tr][tc]=w?'Q':'q';promoted=true}
    let rookFrom=-1,rookTo=-1;
    if(type==='k'&&Math.abs(sc-tc)===2){
        if(tc===6){board[sr][5]=board[sr][7];board[sr][7]='';rookFrom=7;rookTo=5}
        else if(tc===2){board[sr][3]=board[sr][0];board[sr][0]='';rookFrom=0;rookTo=3}
    }
    const oldEp=state.enPassantTarget;
    state.enPassantTarget=(type==='p'&&Math.abs(sr-tr)===2)?{r:(sr+tr)/2,c:sc}:null;
    const oldCastle=Object.assign({},state.castlingRights);
    if(type==='k'){state.castlingRights[w?'K':'k']=false;state.castlingRights[w?'Q':'q']=false}
    if(type==='r'){
        if(sr===7&&sc===0)state.castlingRights.Q=false;
        if(sr===7&&sc===7)state.castlingRights.K=false;
        if(sr===0&&sc===0)state.castlingRights.q=false;
        if(sr===0&&sc===7)state.castlingRights.k=false;
    }
    if(captured==='r'){
        if(tr===7&&tc===0)state.castlingRights.q=false;
        if(tr===7&&tc===7)state.castlingRights.k=false;
    }
    if(captured==='R'){
        if(tr===0&&tc===0)state.castlingRights.Q=false;
        if(tr===0&&tc===7)state.castlingRights.K=false;
    }
    return{sr,sc,tr,tc,piece,captured,epCap,epCapR,oldEp,oldCastle,promoted,rookFrom,rookTo,w};
}
function undoMove(u){
    board[u.sr][u.sc]=u.piece;
    board[u.tr][u.tc]=u.captured||'';
    if(u.epCapR>=0)board[u.epCapR][u.tc]=u.epCap;
    if(u.rookFrom>=0){board[u.sr][u.rookFrom]=board[u.sr][u.rookTo];board[u.sr][u.rookTo]=''}
    state.enPassantTarget=u.oldEp;
    state.castlingRights=u.oldCastle;
}

// ===== 局面指纹(用于重复局面检测,防 AI 来回晃)=====
function boardFingerprint(){
    let s='';
    for(let r=0;r<8;r++)s+=board[r].join('')+'/';
    s+=turnWhite?'w':'b';
    s+=JSON.stringify(state.castlingRights)+JSON.stringify(state.enPassantTarget);
    return s;
}

// ===== 评估(轻量高速版)=====
function evaluate(b){
    let score=0;
    const wp=Array(8).fill(0),bp=Array(8).fill(0);
    const wpr=Array(8).fill(-1),bpr=Array(8).fill(-1); // 每列最前兵的行
    let endgame=true,wNonPawn=0,bNonPawn=0,wBishops=0,bBishops=0;
    let wRooks=[],bRooks=[];
    let wk=null,bk=null;
    for(let r=0;r<8;r++)for(let c=0;c<8;c++){
        const p=b[r][c];if(!p)continue;
        const tp=p.toLowerCase(),w=isWhite(p);
        let v=pieceValue[tp],ps=w?posTable[tp][r][c]:posTable[tp][7-r][c];
        if(!w){v=-v;ps=-ps}
        score+=v+ps;
        if(tp==='k'){if(w)wk={r,c};else bk={r,c};continue}
        if(tp!=='p'){endgame=false;w?wNonPawn++:bNonPawn++}
        if(tp==='p'){
            w?wp[c]++:bp[c]++;
            // 白兵越靠上(r越小)越前;黑兵越靠下(r越大)越前
            if(w&&(wpr[c]<0||r<wpr[c]))wpr[c]=r;
            if(!w&&(bpr[c]<0||r>bpr[c]))bpr[c]=r;
        }
        if(tp==='b'){w?wBishops++:bBishops++}
        if(tp==='r'){if(w)wRooks.push(c);else bRooks.push(c)}
        if((c===3||c===4)&&(r===3||r===4))score+=w?14:-14;
    }
    if(wBishops>=2)score+=30;
    if(bBishops>=2)score-=30;
    // 叠兵/孤立兵
    for(let c=0;c<8;c++){
        if(wp[c]>1)score-=16*(wp[c]-1);
        if(bp[c]>1)score+=16*(bp[c]-1);
        if(wp[c]>0&&!wp[c-1]&&!wp[c+1])score-=12;
        if(bp[c]>0&&!bp[c-1]&&!bp[c+1])score+=12;
    }
    // 通路兵(轻量:不用 findIndex)
    for(let c=0;c<8;c++){
        if(wp[c]>0){
            let passed=true;
            for(let dc=-1;dc<=1;dc++){
                const cc=c+dc;if(cc<0||cc>7)continue;
                if(bp[cc]>0){passed=false;break}
            }
            if(passed){
                const rank=7-wpr[c]; // 白兵距升变还有几步
                score+=15+rank*rank*6;
            }
        }
        if(bp[c]>0){
            let passed=true;
            for(let dc=-1;dc<=1;dc++){
                const cc=c+dc;if(cc<0||cc>7)continue;
                if(wp[cc]>0){passed=false;break}
            }
            if(passed){
                const rank=bpr[c]; // 黑兵距升变还有几步
                score-=15+rank*rank*6;
            }
        }
    }
    // 开放线车
    for(const c of wRooks){
        if(wp[c]===0&&bp[c]===0)score+=35;
        else if(wp[c]===0)score+=18;
    }
    for(const c of bRooks){
        if(wp[c]===0&&bp[c]===0)score-=35;
        else if(bp[c]===0)score-=18;
    }
    // 王盾 + 王暴露
    if(wk){
        let shield=0;
        for(const dc of[-1,0,1]){
            const c=wk.c+dc;
            if(c>=0&&c<8&&wk.r+1<8&&b[wk.r+1][c]==='P')shield++;
        }
        if(!endgame)score-=shield*8;
        if(!endgame&&(wk.c===3||wk.c===4)&&wk.r>=6)score-=20;
    }
    if(bk){
        let shield=0;
        for(const dc of[-1,0,1]){
            const c=bk.c+dc;
            if(c>=0&&c<8&&bk.r-1>=0&&b[bk.r-1][c]==='p')shield++;
        }
        if(!endgame)score+=shield*8;
        if(!endgame&&(bk.c===3||bk.c===4)&&bk.r<=1)score+=20;
    }
    // 残局王中心化
    if(endgame||(wNonPawn+bNonPawn)<=6){
        if(wk)score-=(Math.abs(3.5-wk.r)+Math.abs(3.5-wk.c))*6;
        if(bk)score+=(Math.abs(3.5-bk.r)+Math.abs(3.5-bk.c))*6;
    }
    return score;
}

// 走法排序:MVV-LVA + 历史启发式 + 经验
function orderMoves(moves,whiteToMove){
    return moves.map(m=>{
        const vic=board[m.to[0]][m.to[1]],att=board[m.from[0]][m.from[1]];
        let s=0;
        if(vic)s=10*pieceValue[vic.toLowerCase()]-pieceValue[att.toLowerCase()];
        const uci=colToFile(m.from[1])+(8-m.from[0])+colToFile(m.to[1])+(8-m.to[0]);
        const hIdx=(m.from[0]*8+m.from[1])*64+(m.to[0]*8+m.to[1]);
        s+=historyTable[hIdx]>>4;
        if(!whiteToMove)s+=expBonus(uci); // 只有黑方(AI)走时用经验
        return{m,s};
    }).sort((a,b)=>b.s-a.s).map(x=>x.m);
}

// 静态搜索:只搜吃子,消除地平线效应(送子的核心修复)
function quiescence(alpha,beta,isMax,qd){
    if(qd===undefined)qd=6;
    const stand=evaluate(board);
    if(isMax){
        if(stand>=beta)return beta;
        if(stand>alpha)alpha=stand;
    }else{
        if(stand<=alpha)return alpha;
        if(stand<beta)beta=stand;
    }
    if(qd<=0)return stand;
    let caps=getAllLegalMoves(isMax).filter(m=>board[m.to[0]][m.to[1]]);
    caps=orderMoves(caps,isMax);
    if(isMax){
        let best=-Infinity;
        for(const m of caps){
            const u=doMove(m);
            const sc=quiescence(alpha,beta,false,qd-1);
            undoMove(u);
            if(sc>=beta)return beta;
            if(sc>best)best=sc;
            if(sc>alpha)alpha=sc;
        }
        return best===-Infinity?stand:best;
    }else{
        let best=Infinity;
        for(const m of caps){
            const u=doMove(m);
            const sc=quiescence(alpha,beta,true,qd-1);
            undoMove(u);
            if(sc<=alpha)return alpha;
            if(sc<best)best=sc;
            if(sc<beta)beta=sc;
        }
        return best===Infinity?stand:best;
    }
}

// 主搜索
let searchDeadline=0;
function search(depth,alpha,beta,isMax,maxDepth){
    if(Date.now()>searchDeadline)return 0;
    if(depth===0)return quiescence(alpha,beta,isMax);
    let moves=getAllLegalMoves(isMax);
    if(moves.length===0){
        return inCheck(isMax)?(isMax?-100000-(maxDepth-depth):100000+(maxDepth-depth)):0;
    }
    // 空着裁剪(null move pruning):非必兑局面让对手"多走一步",仍占优则剪枝
    if(depth>=3&&!inCheck(isMax)&&!gameOver){
        const savedTurn=turnWhite;
        turnWhite=!isMax; // 跳过当前方
        const nm=search(depth-3,alpha,beta,!isMax,maxDepth);
        turnWhite=savedTurn;
        if(isMax?nm>=beta:nm<=alpha)return isMax?beta:alpha;
    }
    moves=orderMoves(moves,isMax);
    if(isMax){
        let best=-Infinity;
        for(const m of moves){
            const u=doMove(m);
            const sc=search(depth-1,alpha,beta,false,maxDepth);
            undoMove(u);
            if(sc>best)best=sc;
            if(sc>alpha)alpha=sc;
            if(beta<=alpha){
                const hIdx=(m.from[0]*8+m.from[1])*64+(m.to[0]*8+m.to[1]);
                historyTable[hIdx]+=depth*depth;
                break;
            }
        }
        return best;
    }else{
        let best=Infinity;
        for(const m of moves){
            const u=doMove(m);
            const sc=search(depth-1,alpha,beta,true,maxDepth);
            undoMove(u);
            if(sc<best)best=sc;
            if(sc<beta)beta=sc;
            if(beta<=alpha){
                const hIdx=(m.from[0]*8+m.from[1])*64+(m.to[0]*8+m.to[1]);
                historyTable[hIdx]+=depth*depth;
                break;
            }
        }
        return best;
    }
}

function pickAIMove(){
    const moves=getAllLegalMoves(false);
    if(moves.length===0)return null;
    // 开局库优先(前 5 步)
    if(uciMoves.length<=8){
        const seq=uciMoves.join('');
        const book=openingBook[seq];
        if(book){
            for(const resp of book){
                const found=moves.find(m=>{
                    const u=colToFile(m.from[1])+(8-m.from[0])+colToFile(m.to[1])+(8-m.to[0]);
                    return u===resp;
                });
                if(found)return found;
            }
        }
    }
    const depth=parseInt(document.getElementById('skillSel').value);
    searchDeadline=Date.now()+1500; // 最多搜 1.5 秒
    const ordered=orderMoves(moves,false);
    let best=null,bestScore=Infinity;
    for(const m of ordered){
        if(Date.now()>searchDeadline)break;
        const u=doMove(m);
        const sc=search(depth-1,-Infinity,Infinity,true,depth);
        undoMove(u);
        if(sc<bestScore){bestScore=sc;best=m}
    }
    return best;
}

async function aiMove(){
    if(gameOver||turnWhite||aiThinking)return;
    aiThinking=true;
    $info.textContent="AI思考中…";
    await sleep(30);
    const best=pickAIMove();
    aiThinking=false;
    if(best)await executeMove(best.from[0],best.from[1],best.to[0],best.to[1]);
}

function colToFile(c){return String.fromCharCode(97+c)}
function moveToUci(sr,sc,tr,tc){return colToFile(sc)+(8-sr)+colToFile(tc)+(8-tr)}

function finishGame(msg,result){
    gameOver=true;
    $info.textContent=msg;
    if(playVsAI)expLearn(result);
    // 4 秒后自动开新局
    setTimeout(()=>{resetGame()},4000);
}

function updateGameStatus(){
    const all=getAllLegalMoves(turnWhite);
    const check=inCheck(turnWhite);
    // 三次重复局面判和
    let repeated=false;
    for(const v of positionHistory.values()){if(v>=3){repeated=true;break}}
    if(repeated){
        finishGame("三次重复局面,和棋",'d');
    }else if(all.length===0){
        if(check){
            finishGame(turnWhite?"黑方将死白方!":"白方将死黑方!",turnWhite?'b':'w');
        }else{
            finishGame("逼和(无子可动),和棋",'d');
        }
    }else{
        $info.textContent=turnWhite?(check?"白方回合,将军!":"白方回合"):(check?"黑方回合,将军!":"黑方回合");
    }
}

function moveToPGN(sr,sc,tr,tc){
    const piece=board[sr][sc],type=piece.toLowerCase();
    const f=colToFile(sc),r=8-sr,tf=colToFile(tc),trank=8-tr;
    let notation='';
    if(type==='p'){
        if(board[tr][tc]||(state.enPassantTarget&&tr===state.enPassantTarget.r&&tc===state.enPassantTarget.c))notation+=f;
        notation+=tf+trank;
        const pc=board[tr][tc].toLowerCase();
        if(pc==='q'||pc==='r'||pc==='b'||pc==='n')notation+='='+board[tr][tc].toUpperCase();
    }else{
        notation+=piece.toUpperCase()+tf+trank;
    }
    return notation;
}

async function executeMove(sr,sc,tr,tc){
    const piece=board[sr][sc],w=isWhite(piece),type=piece.toLowerCase();
    const animPiece=document.createElement("div");
    animPiece.className="piece";
    animPiece.innerText=pieceChar[piece];
    $animLayer.appendChild(animPiece);
    animPiece.style.transform=`translate(${sc*64}px,${sr*64}px)`;
    animPiece.offsetHeight;
    animPiece.style.transform=`translate(${tc*64}px,${tr*64}px)`;
    await sleep(300);
    if(type==='k'&&Math.abs(sc-tc)===2){
        if(tc===6){board[sr][5]=board[sr][7];board[sr][7]=''}
        else if(tc===2){board[sr][3]=board[sr][0];board[sr][0]=''}
    }
    if(type==='p'&&state.enPassantTarget&&tr===state.enPassantTarget.r&&tc===state.enPassantTarget.c&&!board[tr][tc]){
        const capR=w?tr+1:tr-1;
        board[capR][tc]='';
    }
    if(type==='p'&&(tr===0||tr===7)){
        promoPending={sr,sc,tr,tc,w};
        $promoModal.style.display='flex';
        animPiece.remove();board[sr][sc]='';render();return;
    }
    board[tr][tc]=piece;board[sr][sc]='';
    animPiece.remove();
    finishMove(sr,sc,tr,tc);
}

function finishMove(sr,sc,tr,tc){
    const pc=board[tr][tc],type=pc.toLowerCase(),w=isWhite(pc);
    state.enPassantTarget=null;
    if(type==='p'&&Math.abs(sr-tr)===2)state.enPassantTarget={r:(sr+tr)/2,c:sc};
    if(type==='k'){state.castlingRights[w?'K':'k']=false;state.castlingRights[w?'Q':'q']=false}
    if(type==='r'){
        if(sr===7&&sc===0)state.castlingRights.Q=false;
        if(sr===7&&sc===7)state.castlingRights.K=false;
        if(sr===0&&sc===0)state.castlingRights.q=false;
        if(sr===0&&sc===7)state.castlingRights.k=false;
    }
    const pgnStr=moveToPGN(sr,sc,tr,tc);
    pgnMoves.push(pgnStr);
    const uciStr=moveToUci(sr,sc,tr,tc);
    uciMoves.push(uciStr);
    // 记录 AI(黑方)走的步,结束后用于学习
    if(!w)aiMoveLog.push(uciStr);
    // 记录局面
    const fp=boardFingerprint();
    positionHistory.set(fp,(positionHistory.get(fp)||0)+1);
    turnWhite=!turnWhite;selected=null;
    render();
    if(!gameOver&&playVsAI&&!turnWhite)setTimeout(aiMove,350);
}

function sleep(ms){return new Promise(r=>setTimeout(r,ms))}
function clickSquare(r,c){
    if(gameOver||aiThinking)return;
    if(playVsAI&&!turnWhite)return;
    if(selected){
        const sr=selected.r,sc=selected.c;
        const leg=getLegalMoves(board,sr,sc,state);
        if(leg.some(([tr,tc])=>tr===r&&tc===c)){executeMove(sr,sc,r,c);return}
        const p=board[r][c];
        if(p&&(turnWhite?isWhite(p):isBlack(p))){selected={r,c};render();return}
        selected=null;render();return;
    }
    const p=board[r][c];
    if(p&&(turnWhite?isWhite(p):isBlack(p))){selected={r,c};render()}
}

function render(){
    $board.innerHTML='';$pieceLayer.innerHTML='';
    let possibleMoves=[];
    if(selected)possibleMoves=getLegalMoves(board,selected.r,selected.c,state);
    for(let r=0;r<8;r++){
        for(let c=0;c<8;c++){
            const sq=document.createElement('div');
            const light=(r+c)%2===0;
            sq.className='square '+(light?'light':'dark');
            const isPos=possibleMoves.some(([tr,tc])=>tr===r&&tc===c);
            if(isPos)sq.classList.add(board[r][c]?'possible-capture':'possible');
            sq.onclick=()=>clickSquare(r,c);
            $board.appendChild(sq);
            const pc=board[r][c];
            if(pc){
                const piece=document.createElement("div");
                piece.className="piece";
                piece.innerText=pieceChar[pc];
                piece.style.transform=`translate(${c*64}px,${r*64}px)`;
                $pieceLayer.appendChild(piece);
            }
        }
    }
    updateGameStatus();
    $pgnBox.value=pgnMoves.join(' ');
}

function exportPGN(){alert(pgnMoves.join(' '))}
resetGame();
</script>
</body>
</html>

AI变强了,不能重置棋局了

0 条评论

目前还没有评论...