<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>国际象棋|增强AI版|平滑落子动画</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 0.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,0.6);align-items:center;justify-content:center;z-index:100;
}
.promoPanel{background:white;padding:12px;border-radius:8px;display:flex;gap:8px}
.promoBtn{
    font-size:46px;
    width:60px;height:60px;
    cursor:pointer;
    border:1px solid #888;
    background:#fff;
    display:flex;
    align-items:center;
    justify-content:center;
}
</style>
</head>
<body>
<div id="panel">
    <div id="info"></div>
    <button id="resetBtn">重置棋局</button>
    <button id="exportPgnBtn">导出PGN</button>
    <button id="modeBtn">切换双人/人机</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="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 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]]
};
const pieceValue = {p:100,n:320,b:330,r:500,q:900,k:9999};

const openingBook = {
    "e2e4":["e7e5","c7c5","e7e6","c7c6"],
    "d2d4":["d7d5","g8f6","e7e6"],
    "c2c4":["e7e5","d7d5"],
    "e2e4e7e5":["g1f3","b1c3"],
    "e2e4c7c5":["g1f3"]
};

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'],
];

let board = null;
let turnWhite = true;
let selected = null;
let pgnMoves = [];
let enPassantTarget = null;
let castlingRights = {K:true,Q:true,k:true,q:true};
let gameOver = false;
let aiDepth = 3;
let playVsAI = true;
let promoPending = null;

const $board = document.getElementById('board');
const $info = document.getElementById('info');
const $pgnBox = document.getElementById('pgnBox');
const $promoModal = document.getElementById('promoModal');
const $pieceLayer = document.getElementById('pieceLayer');
const $animLayer = document.getElementById('animLayer');

document.getElementById('resetBtn').onclick = resetGame;
document.getElementById('exportPgnBtn').onclick = exportPGN;
document.getElementById('modeBtn').onclick = ()=>{
    playVsAI = !playVsAI;
    alert(playVsAI ? "人机对战:你白方,电脑黑方" : "双人对战");
    render();
};

document.querySelectorAll('.promoPanel').forEach(b=>{
    b.onclick = ()=>{
        const pc = b.dataset.p;
        const {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 resetGame(){
    board = copyBoard(startBoard);
    turnWhite = true;
    selected = null;
    pgnMoves = [];
    enPassantTarget = null;
    castlingRights = {K:true,Q:true,k:true,q:true};
    gameOver = false;
    promoPending=null;
    render();
}

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;
        const moves = getRawMoves(b,i,j);
        if(moves.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 = [];
    const w = isWhite(p);
    const 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;
        if(w?isWhite(t):isBlack(t)) return false;
        return true;
    }
    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;
        const 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; const 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){
            const tr=r+dr; const tc=c+dc;
            if(canGo(tr,tc)) moves.push([tr,tc]);
        }
    }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 if(type==='k'){
        const dirs=[[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];
        for(const [dr,dc] of dirs){
            const tr=r+dr; const tc=c+dc;
            if(canGo(tr,tc)) moves.push([tr,tc]);
        }
    }
    return moves;
}

function getLegalMoves(b, r, c){
    const p = b[r][c];
    const w = isWhite(p);
    let raw = getRawMoves(b,r,c);
    const legal = [];
    for(const [tr,tc] of raw){
        const sim = copyBoard(b);
        sim[tr][tc] = sim[r][c];
        sim[r][c] = '';
        const kingPos = findKing(sim,w);
        if(!isSquareAttacked(sim, kingPos.r, kingPos.c, !w)){
            legal.push([tr,tc]);
        }
    }
    if(p.toLowerCase()==='k' && r === (w?7:0)){
        const kr = w ? 'K':'k';
        const qr = w ? 'Q':'q';
        if(castlingRights[kr]){
            if(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(castlingRights[qr]){
            if(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' && enPassantTarget){
        const epR = enPassantTarget.r;
        const epC = enPassantTarget.c;
        const dir = w ? -1 : 1;
        if(r+dir === epR && Math.abs(c - epC) === 1){
            legal.push([epR, epC]);
        }
    }
    return legal;
}

function getAllLegalMoves(isWhiteTurn, boardState=board){
    const moves = [];
    for(let r=0;r<8;r++)for(let c=0;c<8;c++){
        const p = boardState[r][c];
        if(!p) continue;
        if(isWhiteTurn ? isWhite(p) : isBlack(p)){
            const lm = getLegalMoves(boardState, r,c);
            for(const m of lm) moves.push({from:[r,c],to:m});
        }
    }
    return moves;
}

function inCheck(whiteTurn,b=board){
    const kp = findKing(b, whiteTurn);
    return isSquareAttacked(b, kp.r, kp.c, !whiteTurn);
}

// ==================== 增强评估函数 ====================
function evaluateBoard(b){
    let score = 0;
    let whitePawns = Array(8).fill(0);
    let blackPawns = Array(8).fill(0);
    let endgame = true;
    let whitePieces=0,blackPieces=0;

    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();
            const w = isWhite(p);
            let val = pieceValue[tp];
            let posScore;
            if(w){
                posScore = posTable[tp][r][c];
            }else{
                posScore = posTable[tp][7-r][c];
                val = -val; posScore = -posScore;
            }
            score += val + posScore;

            if(tp!=='p'&&tp!=='k'){
                endgame=false;
                w?whitePieces++:blackPieces++;
            }
            if(tp==='p'){
                if(w) whitePawns[c]++;
                else blackPawns[c]++;
            }
            //中心格子d4 d5 e4 e5
            if((c===3||c===4)&&(r===3||r===4)){
                score += w ? 12 : -12;
            }else if((c===2||c===5)&&(r===2||r===5)){
                score += w ? 6 : -6;
            }
        }
    }

    //兵型惩罚:叠兵、孤立兵
    for(let c=0;c<8;c++){
        if(whitePawns[c]>1) score -= 20*(whitePawns[c]-1);
        if(blackPawns[c]>1) score +=20*(blackPawns[c]-1);
        if(whitePawns[c]>0){
            if(whitePawns[c-1]===0&&whitePawns[c+1]===0) score -=15;
        }
        if(blackPawns[c]>0){
            if(blackPawns[c-1]===0&&blackPawns[c+1]===0) score +=15;
        }
    }

    //通路兵加分
    for(let c=0;c<8;c++){
        if(whitePawns[c]>0 && blackPawns[c]===0){
            score += 25;
        }
        if(blackPawns[c]>0 && whitePawns[c]===0){
            score -=25;
        }
    }

    //残局王要主动
    if(endgame){
        const wk = findKing(b,true);
        const bk = findKing(b,false);
        const wkd = Math.abs(3.5-wk.r)+Math.abs(3.5-wk.c);
        const bkd = Math.abs(3.5-bk.r)+Math.abs(3.5-bk.c);
        score -= wkd*6;
        score += bkd*6;
    }

    //将军惩罚
    if(inCheck(true,b)) score-=70;
    if(inCheck(false,b)) score+=70;

    return score;
}

//Minimax αβ剪枝
function minimax(b, depth, alpha, beta, isMaxPlayer){
    if(depth===0){
        return evaluateBoard(b);
    }
    const moves = getAllLegalMoves(isMaxPlayer,b);
    if(moves.length===0){
        if(inCheck(isMaxPlayer,b)){
            return isMaxPlayer ? -10000 : 10000;
        }else{
            return 0;
        }
    }

    if(isMaxPlayer){
        let bestVal = -Infinity;
        for(const m of moves){
            const sim = copyBoard(b);
            sim[m.to[0]][m.to[1]] = sim[m.from[0]][m.from[1]];
            sim[m.from[0]][m.from[1]] = '';
            const val = minimax(sim, depth-1, alpha, beta, false);
            bestVal = Math.max(bestVal,val);
            alpha = Math.max(alpha,val);
            if(beta <= alpha) break;
        }
        return bestVal;
    }else{
        let bestVal = Infinity;
        for(const m of moves){
            const sim = copyBoard(b);
            sim[m.to[0]][m.to[1]] = sim[m.from[0]][m.from[1]];
            sim[m.from[0]][m.from[1]] = '';
            const val = minimax(sim, depth-1, alpha, beta, true);
            bestVal = Math.min(bestVal,val);
            beta = Math.min(beta,val);
            if(beta <= alpha) break;
        }
        return bestVal;
    }
}

function moveToStr(moveObj){
    const f = String.fromCharCode(97+moveObj.from[1]);
    const fr = 8-moveObj.from[0];
    const t = String.fromCharCode(97+moveObj.to[1]);
    const tr = 8-moveObj.to[0];
    return `${f}${fr}${t}${tr}`;
}

async function aiMove(){
    if(gameOver || turnWhite) return;
    const moves = getAllLegalMoves(false);
    if(moves.length===0) return;

    let bestMove = null;
    let bestScore = Infinity;

    //开局库优先
    if(pgnMoves.length <=2){
        let lastStr = null;
        if(pgnMoves.length>=1){
            lastStr = pgnMoves[0];
        }
        if(lastStr && openingBook[lastStr]){
            for(const resp of openingBook[lastStr]){
                const found = moves.find(m=>moveToStr(m)===resp);
                if(found){
                    bestMove = found;
                    break;
                }
            }
        }
    }

    if(!bestMove){
        //遍历全部走法
        for(const m of moves){
            const sim = copyBoard(board);
            sim[m.to[0]][m.to[1]] = sim[m.from[0]][m.from[1]];
            sim[m.from[0]][m.from[1]] = '';
            const sc = minimax(sim, aiDepth-1, -Infinity, Infinity, true);
            if(sc < bestScore){
                bestScore = sc;
                bestMove = m;
            }
        }
    }

    if(bestMove){
        await executeMove(bestMove.from[0],bestMove.from[1],bestMove.to[0],bestMove.to[1]);
    }
}

function updateGameStatus(){
    const allMoves = getAllLegalMoves(turnWhite);
    const check = inCheck(turnWhite);
    if(allMoves.length === 0){
        gameOver = true;
        if(check){
            $info.textContent = turnWhite ? "黑方将死白方!" : "白方将死黑方!";
        }else{
            $info.textContent = "逼和(无子可动)平局";
        }
    }else{
        if(check){
            $info.textContent = turnWhite ? "白方回合,将军!" : "黑方回合,将军!";
        }else{
            $info.textContent = turnWhite ? "白方回合" : "黑方回合";
        }
    }
}

function colToFile(c){ return String.fromCharCode(97 + c); }
function moveToPGN(fromR,fromC,toR,toC,prom){
    const piece = board[fromR][fromC];
    const type = piece.toLowerCase();
    const f = colToFile(fromC);
    const r = 8-fromR;
    const tf = colToFile(toC);
    const tr = 8-toR;
    let notation = '';
    if(type === 'p'){
        if(board[toR][toC] || (enPassantTarget && toR===enPassantTarget.r && toC===enPassantTarget.c)) notation += f;
        notation += tf + tr;
        if(prom) notation += '=' + prom.toUpperCase();
    }else{
        notation += piece.toUpperCase();
        notation += tf+tr;
    }
    return notation;
}

async function executeMove(sr,sc,tr,tc){
    const piece = board[sr][sc];
    const w = isWhite(piece);
    const 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] = '';
        }
        castlingRights[w?'K':'k'] = false;
        castlingRights[w?'Q':'q'] = false;
    }

    //吃过路兵
    if(type==='p' && enPassantTarget && tr === enPassantTarget.r && tc === enPassantTarget.c){
        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];
    const type = pc.toLowerCase();
    const w = isWhite(pc);
    enPassantTarget = null;
    if(type==='p' && Math.abs(sr-tr)===2){
        enPassantTarget = {r:(sr+tr)/2, c:sc};
    }
    if(type === 'k') castlingRights[w?'K':'k'] = castlingRights[w?'Q':'q'] = false;
    if(type === 'r'){
        if(sr === 7 && sc === 0) castlingRights.Q = false;
        if(sr ===7 && sc ===7) castlingRights.K = false;
        if(sr ===0 && sc ===0) castlingRights.q = false;
        if(sr ===0 && sc ===7) castlingRights.k = false;
    }
    const pgnStr = moveToPGN(sr,sc,tr,tc, board[tr][tc]);
    pgnMoves.push(pgnStr);
    turnWhite = !turnWhite;
    selected = null;
    render();
    if(!gameOver && playVsAI && !turnWhite){
        setTimeout(aiMove,400);
    }
}

function sleep(ms){
    return new Promise(resolve=>setTimeout(resolve,ms));
}

function clickSquare(r,c){
    if(gameOver) return;
    if(playVsAI && !turnWhite) return;
    if(selected){
        const sr = selected.r, sc = selected.c;
        const leg = getLegalMoves(board, sr, sc);
        const ok = leg.some(([tr,tc])=>tr===r&&tc===c);
        if(ok){
            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);
    }
    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 isPossible = possibleMoves.some(([tr,tc])=>tr===r&&tc===c);
            if(isPossible){
                if(board[r][c]) sq.classList.add('possible-capture');
                else sq.classList.add('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强了些

3 条评论

  • 1