<!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="undoBtn">悔棋</button>
<button id="exportPgnBtn">导出PGN</button>
<button id="modeBtn">切换双人/人机</button>
<button id="clearLearn">清空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="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:[[-30,-40,-40,-50,-50,-40,-40,-30],[-30,-40,-40,-50,-50,-40,-40,-30],[-30,-40,-40,-50,-50,-40,-40,-30],[-30,-40,-40,-50,-50,-40,-40,-30],[-20,-30,-30,-40,-40,-30,-30,-20],[-10,-20,-20,-20,-20,-20,-20,-10],[20,20,0,0,0,0,20,20],[20,30,10,0,0,10,30,20]]
};
const pieceValue = {p:100,n:320,b:330,r:500,q:900,k:9999};
// ===================== AI学习模块 =====================
let playerMoveStats = {};
const openingBook = {
"e2e4": ["e7e5","c7c5","e7e6","c7c6"],
"d2d4": ["d7d5","g8f6","e7e6"],
"c2c4": ["e7e5","d7d5"]
};
function recordPlayerMove(moveStr){
if(!playerMoveStats[moveStr]) playerMoveStats[moveStr]=0;
playerMoveStats[moveStr]++;
}
function clearLearnRecord(){
playerMoveStats = {};
alert("AI学习记录已清空,重新学习你的走法!");
}
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}`;
}
function evaluateBoardWithLearn(b, playerMoves){
let score=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();
let val = pieceValue[tp];
let posScore = posTable[tp][r][c];
if(!isWhite(p)){
posScore = posTable[tp][7-r][c];
val = -val; posScore = -posScore;
}
score += val + posScore;
}
const total = Object.values(playerMoves).reduce((a,b)=>a+b,0);
if(total>0){
score += Math.random()*20;
}
return score;
}
// =====================================================
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 history = [];
let pgnMoves = [];
let enPassantTarget = null;
let castlingRights = {K:true,Q:true,k:true,q:true};
let gameOver = false;
let aiDepth = 2;
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('undoBtn').onclick = undoMove;
document.getElementById('exportPgnBtn').onclick = exportPGN;
document.getElementById('modeBtn').onclick = ()=>{
playVsAI = !playVsAI;
alert(playVsAI ? "人机对战:你白方,电脑黑方" : "双人对战");
render();
};
document.getElementById('clearLearn').onclick = clearLearnRecord;
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;
history = [];
pgnMoves = [];
enPassantTarget = null;
castlingRights = {K:true,Q:true,k:true,q:true};
gameOver = false;
promoPending=null;
render();
}
function saveState(){
history.push({board:copyBoard(board),turnWhite,enPassantTarget,castlingRights:{...castlingRights},gameOver});
}
function undoMove(){
if(history.length < 1) return;
if(playVsAI && history.length>=2) history.pop();
const prev = history.pop();
board = prev.board;
turnWhite = prev.turnWhite;
enPassantTarget = prev.enPassantTarget;
castlingRights = prev.castlingRights;
gameOver = prev.gameOver;
if(pgnMoves.length>0) pgnMoves.pop();
selected = 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 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){
saveState();
const piece = board[sr][sc];
const w = isWhite(piece);
const type = piece.toLowerCase();
if(w){
const mStr = moveToStr({from:[sr,sc],to:[tr,tc]});
recordPlayerMove(mStr);
}
// 临时动画棋子放到独立animLayer
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; //触发重绘
// 目标位置,开启transition平滑移动
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;
}
let epCapture = false;
if(type==='p' && enPassantTarget && tr === enPassantTarget.r && tc === enPassantTarget.c){
const capR = w ? tr+1 : tr-1;
board[capR][tc] = '';
epCapture = true;
}
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,600);
}
}
function sleep(ms){
return new Promise(resolve=>setTimeout(resolve,ms));
}
// ============ Minimax ============
function minimax(b, depth, alpha, beta, isMax){
if(depth === 0) return evaluateBoardWithLearn(b, playerMoveStats);
const moves = getAllLegalMoves(isMax,b);
if(moves.length===0){
if(inCheck(isMax,b)) return isMax ? -9999 : 9999;
else return 0;
}
if(isMax){
let best = -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 sc = minimax(sim,depth-1,alpha,beta,false);
best = Math.max(best,sc);
alpha = Math.max(alpha,sc);
if(beta <= alpha) break;
}
return best;
}else{
let best = Infinity;
let bestMove = null;
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 score = minimax(sim, aiDepth-1, -Infinity, Infinity, true);
if(score < best){
best = score;
bestMove = m;
}
beta = Math.min(best,score);
if(beta <= alpha) break;
}
return best;
}
}
async function aiMove(){
if(gameOver || turnWhite) return;
const moves = getAllLegalMoves(false);
let bestMove = null;
let bestScore = Infinity;
if(pgnMoves.length < 2){
const wLastMove = pgnMoves[0];
if(wLastMove && openingBook[wLastMove]){
const bookRsp = openingBook[wLastMove];
for(const resp of bookRsp){
const match = moves.find(m=>moveToStr(m)===resp);
if(match){
bestMove = match;
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 score = minimax(sim, aiDepth-1, -Infinity, Infinity, true);
if(score < bestScore){
bestScore = score;
bestMove = m;
}
}
}
if(bestMove){
await executeMove(bestMove.from[0],bestMove.from[1],bestMove.to[0],bestMove.to[1]);
}
}
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>