deepseek v4编写的AI人机对弈象棋,进步相当大。

html
<!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>
:root {
--bg: #f0e6d3;
--board-bg: #e8d5b0;
--board-border: #5d3a1a;
--text: #3a1f04;
--red: #c41e3a;
--red-dark: #8b0000;
--black: #1a1a1a;
--gold: #c8960c;
--highlight: #ffe484;
--ai-glow: #4da6ff;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #3a2f28 0%, #5a3d2b 30%, #4a3020 60%, #2a1f15 100%);
background-attachment: fixed;
font-family: 'STSong', 'Songti SC', 'Noto Serif SC', 'SimSun', 'KaiTi', 'STKaiti', '楷体', '宋体', serif;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
padding: 10px;
}
.game-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
background: rgba(255, 255, 255, 0.03);
border-radius: 20px;
padding: 20px 24px 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
backdrop-filter: blur(2px);
}
.game-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 600px;
gap: 16px;
flex-wrap: wrap;
}
.game-title {
font-size: 1.6em;
font-weight: bold;
color: #f0d9a0;
letter-spacing: 0.08em;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
white-space: nowrap;
}
.ai-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.7em;
background: linear-gradient(135deg, #1a3a5c, #0d2137);
color: #7ec8f8;
padding: 4px 10px;
border-radius: 14px;
letter-spacing: 0.06em;
border: 1px solid #3a6a9a;
animation: ai-pulse 2.5s infinite;
}
.ai-badge .ai-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4da6ff;
box-shadow: 0 0 8px #4da6ff;
animation: ai-dot-blink 1.2s infinite;
}
@keyframes ai-pulse {
0%,
100% {
box-shadow: 0 0 8px rgba(77, 166, 255, 0.2);
}
50% {
box-shadow: 0 0 18px rgba(77, 166, 255, 0.5);
}
}
@keyframes ai-dot-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.turn-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 25px;
font-size: 1em;
font-weight: bold;
letter-spacing: 0.05em;
transition: all 0.3s ease;
background: rgba(0, 0, 0, 0.3);
color: #ddd;
border: 2px solid transparent;
white-space: nowrap;
}
.turn-indicator.red-turn {
background: rgba(200, 30, 50, 0.2);
border-color: #c41e3a;
color: #ffaaaa;
box-shadow: 0 0 20px rgba(200, 30, 50, 0.3);
animation: pulse-red 2s infinite;
}
.turn-indicator.black-turn {
background: rgba(30, 50, 80, 0.5);
border-color: #4da6ff;
color: #b8d8f8;
box-shadow: 0 0 20px rgba(77, 166, 255, 0.3);
animation: pulse-ai 2s infinite;
}
@keyframes pulse-red {
0%,
100% {
box-shadow: 0 0 15px rgba(200, 30, 50, 0.3);
}
50% {
box-shadow: 0 0 30px rgba(255, 60, 80, 0.6);
}
}
@keyframes pulse-ai {
0%,
100% {
box-shadow: 0 0 15px rgba(77, 166, 255, 0.3);
}
50% {
box-shadow: 0 0 30px rgba(77, 166, 255, 0.65);
}
}
.turn-dot {
width: 14px;
height: 14px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.turn-dot.red {
background: #c41e3a;
box-shadow: 0 0 8px #ff4050;
}
.turn-dot.black {
background: #4da6ff;
box-shadow: 0 0 8px #7ec8f8;
}
.status-message {
font-size: 0.95em;
color: #ffcc80;
text-align: center;
min-height: 1.4em;
letter-spacing: 0.04em;
font-weight: bold;
transition: all 0.3s;
}
.status-message.check-warning {
color: #ff5555;
animation: flash-warning 0.6s infinite alternate;
}
.status-message.game-over {
color: #ffd700;
font-size: 1.1em;
}
.status-message.ai-thinking {
color: #7ec8f8;
animation: ai-thinking-pulse 0.8s infinite alternate;
}
@keyframes flash-warning {
from {
opacity: 0.7;
}
to {
opacity: 1;
}
}
@keyframes ai-thinking-pulse {
from {
opacity: 0.6;
}
to {
opacity: 1;
}
}
.canvas-wrapper {
position: relative;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 3px #5d3a1a, 0 0 0 6px #3a1f04;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.3s;
}
.canvas-wrapper.ai-thinking-wrapper {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 3px #5d3a1a, 0 0 0 6px #3a1f04,
0 0 40px rgba(77, 166, 255, 0.35);
}
.canvas-wrapper:active {
transform: scale(0.995);
}
.canvas-wrapper.no-interact {
cursor: not-allowed;
pointer-events: none;
opacity: 0.92;
}
canvas {
display: block;
max-width: 100%;
height: auto;
}
.btn-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.btn {
padding: 10px 22px;
font-size: 1em;
font-weight: bold;
letter-spacing: 0.05em;
border: none;
border-radius: 25px;
cursor: pointer;
transition: all 0.25s;
font-family: inherit;
color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.btn-restart {
background: #c41e3a;
}
.btn-restart:hover {
background: #e02845;
box-shadow: 0 6px 20px rgba(200, 30, 50, 0.5);
transform: translateY(-2px);
}
.btn-undo {
background: #555;
}
.btn-undo:hover {
background: #6a6a6a;
box-shadow: 0 6px 20px rgba(100, 100, 100, 0.5);
transform: translateY(-2px);
}
.btn:active {
transform: translateY(1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.ai-difficulty-label {
font-size: 0.8em;
color: #aaa;
letter-spacing: 0.04em;
text-align: center;
}
@media (max-width: 640px) {
.game-container {
padding: 10px 8px 14px;
gap: 8px;
border-radius: 14px;
}
.game-title {
font-size: 1.2em;
}
.turn-indicator {
font-size: 0.85em;
padding: 6px 12px;
}
.btn {
padding: 8px 16px;
font-size: 0.9em;
}
.status-message {
font-size: 0.8em;
}
.ai-badge {
font-size: 0.65em;
padding: 3px 8px;
}
}
</style>
</head>
<body>
<div class="game-container">
<div class="game-header">
<span class="game-title">🏯 中国象棋 <span class="ai-badge"><span class="ai-dot"></span>AI对战</span></span>
<span class="turn-indicator red-turn" id="turnIndicator">
<span class="turn-dot red"></span> 红方走棋(你)
</span>
</div>
<div class="status-message" id="statusMessage"></div>
<div class="canvas-wrapper" id="canvasWrapper">
<canvas id="chessCanvas"></canvas>
</div>
<div class="btn-row">
<button class="btn btn-undo" id="btnUndo" title="悔棋(撤销你上一步和AI的回应)">⟲ 悔棋</button>
<button class="btn btn-restart" id="btnRestart">🔄 重新开始</button>
</div>
<div class="ai-difficulty-label">🤖 黑方AI · 搜索深度 3 层</div>
</div>
<script>
(function() {
// ==================== DOM 元素 ====================
const canvas = document.getElementById('chessCanvas');
const ctx = canvas.getContext('2d');
const turnIndicator = document.getElementById('turnIndicator');
const statusMessage = document.getElementById('statusMessage');
const btnRestart = document.getElementById('btnRestart');
const btnUndo = document.getElementById('btnUndo');
const canvasWrapper = document.getElementById('canvasWrapper');
// ==================== 常量 ====================
const COL_COUNT = 9;
const ROW_COUNT = 10;
const CELL_SIZE = 58;
const MARGIN = 52;
const PIECE_RADIUS = 25;
const CANVAS_WIDTH = MARGIN * 2 + CELL_SIZE * (COL_COUNT - 1);
const CANVAS_HEIGHT = MARGIN * 2 + CELL_SIZE * (ROW_COUNT - 1);
const AI_DELAY = 350; // AI走棋延迟(毫秒),让动画更自然
// 设置Canvas实际尺寸
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
canvas.style.width = CANVAS_WIDTH + 'px';
canvas.style.height = CANVAS_HEIGHT + 'px';
function resizeCanvas() {
const maxWidth = Math.min(window.innerWidth - 30, 600);
if (maxWidth < CANVAS_WIDTH) {
const scale = maxWidth / CANVAS_WIDTH;
canvas.style.width = (CANVAS_WIDTH * scale) + 'px';
canvas.style.height = (CANVAS_HEIGHT * scale) + 'px';
} else {
canvas.style.width = CANVAS_WIDTH + 'px';
canvas.style.height = CANVAS_HEIGHT + 'px';
}
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// ==================== 游戏状态 ====================
let board = [];
let currentTurn = 'red';
let selectedRow = null;
let selectedCol = null;
let validMoves = [];
let gameOver = false;
let winner = null;
let inCheck = false;
let moveHistory = [];
let lastMoveInfo = null;
let aiTimeoutId = null;
let isAiThinking = false;
// ==================== 棋子类型 ====================
const PIECE_NAMES = {
red: { king: '帅', advisor: '仕', elephant: '相', knight: '馬', rook: '車', cannon: '炮', pawn: '兵' },
black: { king: '将', advisor: '士', elephant: '象', knight: '馬', rook: '車', cannon: '砲', pawn: '卒' },
};
// ==================== 棋子基础价值(从黑方AI视角) ====================
const PIECE_BASE_VALUES = {
king: 100000,
rook: 900,
cannon: 450,
knight: 400,
elephant: 200,
advisor: 200,
pawn: 100,
};
// ==================== 位置价值表(从黑方视角,10行×9列) ====================
// 黑方在棋盘上方(row 0-4),红方在下方(row 5-9)
// 正值对黑方有利
const POSITION_VALUES = {
// 黑方兵/卒位置价值(黑方视角:兵过河后价值大增)
blackPawnPos: [
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[5, 10, 15, 20, 25, 20, 15, 10, 5],
[10, 20, 30, 45, 55, 45, 30, 20, 10],
[20, 35, 50, 70, 85, 70, 50, 35, 20],
[30, 45, 60, 80, 95, 80, 60, 45, 30],
[35, 50, 65, 85, 100, 85, 65, 50, 35],
[30, 40, 50, 60, 65, 60, 50, 40, 30],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
],
// 红方兵/卒位置价值(从黑方视角,红兵价值需取负)
redPawnPos: [
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[30, 40, 50, 60, 65, 60, 50, 40, 30],
[35, 50, 65, 85, 100, 85, 65, 50, 35],
[30, 45, 60, 80, 95, 80, 60, 45, 30],
[20, 35, 50, 70, 85, 70, 50, 35, 20],
[10, 20, 30, 45, 55, 45, 30, 20, 10],
[5, 10, 15, 20, 25, 20, 15, 10, 5],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
],
// 马位置价值
knightPos: [
[0, 0, 5, 10, 10, 10, 5, 0, 0],
[0, 10, 20, 30, 30, 30, 20, 10, 0],
[5, 20, 35, 45, 50, 45, 35, 20, 5],
[10, 25, 40, 55, 60, 55, 40, 25, 10],
[10, 25, 40, 55, 60, 55, 40, 25, 10],
[10, 25, 40, 55, 60, 55, 40, 25, 10],
[5, 20, 35, 45, 50, 45, 35, 20, 5],
[0, 10, 20, 30, 30, 30, 20, 10, 0],
[0, 0, 5, 10, 10, 10, 5, 0, 0],
[0, -5, -10, 0, 0, 0, -10, -5, 0],
],
// 车位置价值
rookPos: [
[10, 15, 20, 25, 30, 25, 20, 15, 10],
[15, 25, 30, 35, 40, 35, 30, 25, 15],
[10, 20, 25, 30, 35, 30, 25, 20, 10],
[5, 15, 20, 25, 30, 25, 20, 15, 5],
[0, 10, 15, 20, 25, 20, 15, 10, 0],
[0, 10, 15, 20, 25, 20, 15, 10, 0],
[5, 15, 20, 25, 30, 25, 20, 15, 5],
[10, 20, 25, 30, 35, 30, 25, 20, 10],
[15, 25, 30, 35, 40, 35, 30, 25, 15],
[10, 15, 20, 25, 30, 25, 20, 15, 10],
],
// 炮位置价值
cannonPos: [
[5, 10, 15, 20, 25, 20, 15, 10, 5],
[10, 20, 25, 30, 35, 30, 25, 20, 10],
[5, 15, 20, 25, 30, 25, 20, 15, 5],
[0, 10, 15, 20, 25, 20, 15, 10, 0],
[0, 5, 10, 15, 20, 15, 10, 5, 0],
[0, 5, 10, 15, 20, 15, 10, 5, 0],
[0, 10, 15, 20, 25, 20, 15, 10, 0],
[5, 15, 20, 25, 30, 25, 20, 15, 5],
[10, 20, 25, 30, 35, 30, 25, 20, 10],
[5, 10, 15, 20, 25, 20, 15, 10, 5],
],
};
// ==================== 初始化棋盘 ====================
function createInitialBoard() {
const b = Array.from({ length: ROW_COUNT }, () => Array(COL_COUNT).fill(null));
const blackSetup = [
{ row: 0, col: 0, type: 'rook' }, { row: 0, col: 1, type: 'knight' }, { row: 0, col: 2,
type: 'elephant' },
{ row: 0, col: 3, type: 'advisor' }, { row: 0, col: 4, type: 'king' }, { row: 0, col: 5,
type: 'advisor' },
{ row: 0, col: 6, type: 'elephant' }, { row: 0, col: 7, type: 'knight' }, { row: 0, col: 8,
type: 'rook' },
{ row: 2, col: 1, type: 'cannon' }, { row: 2, col: 7, type: 'cannon' },
{ row: 3, col: 0, type: 'pawn' }, { row: 3, col: 2, type: 'pawn' }, { row: 3, col: 4,
type: 'pawn' },
{ row: 3, col: 6, type: 'pawn' }, { row: 3, col: 8, type: 'pawn' },
];
const redSetup = [
{ row: 9, col: 0, type: 'rook' }, { row: 9, col: 1, type: 'knight' }, { row: 9, col: 2,
type: 'elephant' },
{ row: 9, col: 3, type: 'advisor' }, { row: 9, col: 4, type: 'king' }, { row: 9, col: 5,
type: 'advisor' },
{ row: 9, col: 6, type: 'elephant' }, { row: 9, col: 7, type: 'knight' }, { row: 9, col: 8,
type: 'rook' },
{ row: 7, col: 1, type: 'cannon' }, { row: 7, col: 7, type: 'cannon' },
{ row: 6, col: 0, type: 'pawn' }, { row: 6, col: 2, type: 'pawn' }, { row: 6, col: 4,
type: 'pawn' },
{ row: 6, col: 6, type: 'pawn' }, { row: 6, col: 8, type: 'pawn' },
];
blackSetup.forEach(({ row, col, type }) => { b[row][col] = { type, side: 'black' }; });
redSetup.forEach(({ row, col, type }) => { b[row][col] = { type, side: 'red' }; });
return b;
}
function resetGame() {
clearAiTimeout();
board = createInitialBoard();
currentTurn = 'red';
selectedRow = null;
selectedCol = null;
validMoves = [];
gameOver = false;
winner = null;
inCheck = false;
moveHistory = [];
lastMoveInfo = null;
isAiThinking = false;
updateUI();
drawAll();
}
function clearAiTimeout() {
if (aiTimeoutId !== null) {
clearTimeout(aiTimeoutId);
aiTimeoutId = null;
}
isAiThinking = false;
}
// ==================== 坐标转换 ====================
function rowColToXY(row, col) {
return { x: MARGIN + col * CELL_SIZE, y: MARGIN + row * CELL_SIZE };
}
function canvasToRowCol(canvasX, canvasY) {
const rect = canvas.getBoundingClientRect();
const scaleX = CANVAS_WIDTH / rect.width;
const scaleY = CANVAS_HEIGHT / rect.height;
const x = canvasX * scaleX;
const y = canvasY * scaleY;
const col = Math.round((x - MARGIN) / CELL_SIZE);
const row = Math.round((y - MARGIN) / CELL_SIZE);
const { x: nearestX, y: nearestY } = rowColToXY(row, col);
const dist = Math.sqrt((x - nearestX) ** 2 + (y - nearestY) ** 2);
if (dist <= PIECE_RADIUS + 6 && row >= 0 && row < ROW_COUNT && col >= 0 && col < COL_COUNT) {
return { row, col };
}
return null;
}
function getPieceAt(row, col) {
if (row < 0 || row >= ROW_COUNT || col < 0 || col >= COL_COUNT) return null;
return board[row][col];
}
// ==================== 查找帅/将 ====================
function findKingInBoard(side, boardState) {
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const piece = boardState[row][col];
if (piece && piece.type === 'king' && piece.side === side) {
return { row, col };
}
}
}
return null;
}
function findKing(side) {
return findKingInBoard(side, board);
}
// ==================== 九宫格判断 ====================
function isInPalace(row, col, side) {
if (col < 3 || col > 5) return false;
if (side === 'black') return row >= 0 && row <= 2;
if (side === 'red') return row >= 7 && row <= 9;
return false;
}
function isInOwnHalf(row, side) {
if (side === 'black') return row >= 0 && row <= 4;
if (side === 'red') return row >= 5 && row <= 9;
return false;
}
// ==================== 帅/将安全检测 ====================
function isKingSafe(side, boardState) {
const kingPos = findKingInBoard(side, boardState);
if (!kingPos) return false;
const { row: kr, col: kc } = kingPos;
const opponentSide = side === 'red' ? 'black' : 'red';
for (let r = 0; r < ROW_COUNT; r++) {
for (let c = 0; c < COL_COUNT; c++) {
const piece = boardState[r][c];
if (piece && piece.side === opponentSide) {
const attacks = getRawAttacks(r, c, piece.type, piece.side, boardState);
if (attacks.some(a => a.row === kr && a.col === kc)) {
return false;
}
}
}
}
const oppKingPos = findKingInBoard(opponentSide, boardState);
if (oppKingPos && oppKingPos.col === kc) {
const minRow = Math.min(kr, oppKingPos.row);
const maxRow = Math.max(kr, oppKingPos.row);
let blocked = false;
for (let r = minRow + 1; r < maxRow; r++) {
if (boardState[r][kc] !== null) { blocked = true; break; }
}
if (!blocked) return false;
}
return true;
}
// ==================== 原始攻击范围 ====================
function getRawAttacks(row, col, type, side, boardState) {
const moves = [];
const opponentSide = side === 'red' ? 'black' : 'red';
function addLineMoves(dr, dc, maxSteps = 20) {
for (let i = 1; i <= maxSteps; i++) {
const r = row + dr * i;
const c = col + dc * i;
if (r < 0 || r >= ROW_COUNT || c < 0 || c >= COL_COUNT) break;
const piece = boardState[r][c];
if (piece) {
if (piece.side === opponentSide) moves.push({ row: r, col: c });
break;
}
moves.push({ row: r, col: c });
}
}
switch (type) {
case 'king':
for (const [dr, dc] of [
[-1, 0],
[1, 0],
[0, -1],
[0, 1]
]) {
const r = row + dr;
const c = col + dc;
if (isInPalace(r, c, side)) {
const target = boardState[r][c];
if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
}
}
break;
case 'advisor':
for (const [dr, dc] of [
[-1, -1],
[-1, 1],
[1, -1],
[1, 1]
]) {
const r = row + dr;
const c = col + dc;
if (isInPalace(r, c, side)) {
const target = boardState[r][c];
if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
}
}
break;
case 'elephant':
for (const [dr, dc, eyeDr, eyeDc] of [
[-2, -2, -1, -1],
[-2, 2, -1, 1],
[2, -2, 1, -1],
[2, 2, 1, 1]
]) {
const r = row + dr;
const c = col + dc;
const eyeR = row + eyeDr;
const eyeC = col + eyeDc;
if (r >= 0 && r < ROW_COUNT && c >= 0 && c < COL_COUNT &&
isInOwnHalf(r, side) && !boardState[eyeR][eyeC]) {
const target = boardState[r][c];
if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
}
}
break;
case 'knight':
const knightMoves = [
{ dr: -2, dc: -1, legR: -1, legC: 0 }, { dr: -2, dc: 1, legR: -1, legC: 0 },
{ dr: 2, dc: -1, legR: 1, legC: 0 }, { dr: 2, dc: 1, legR: 1, legC: 0 },
{ dr: -1, dc: -2, legR: 0, legC: -1 }, { dr: -1, dc: 2, legR: 0, legC: 1 },
{ dr: 1, dc: -2, legR: 0, legC: -1 }, { dr: 1, dc: 2, legR: 0, legC: 1 },
];
for (const { dr, dc, legR, legC } of knightMoves) {
const r = row + dr;
const c = col + dc;
const lr = row + legR;
const lc = col + legC;
if (r >= 0 && r < ROW_COUNT && c >= 0 && c < COL_COUNT && !boardState[lr][lc]) {
const target = boardState[r][c];
if (!target || target.side === opponentSide) moves.push({ row: r, col: c });
}
}
break;
case 'rook':
addLineMoves(-1, 0);
addLineMoves(1, 0);
addLineMoves(0, -1);
addLineMoves(0, 1);
break;
case 'cannon':
for (const [dr, dc] of [
[-1, 0],
[1, 0],
[0, -1],
[0, 1]
]) {
for (let i = 1; i < 20; i++) {
const r = row + dr * i;
const c = col + dc * i;
if (r < 0 || r >= ROW_COUNT || c < 0 || c >= COL_COUNT) break;
if (boardState[r][c]) break;
moves.push({ row: r, col: c });
}
}
for (const [dr, dc] of [
[-1, 0],
[1, 0],
[0, -1],
[0, 1]
]) {
let foundPlatform = false;
for (let i = 1; i < 20; i++) {
const r = row + dr * i;
const c = col + dc * i;
if (r < 0 || r >= ROW_COUNT || c < 0 || c >= COL_COUNT) break;
const piece = boardState[r][c];
if (!foundPlatform) {
if (piece) foundPlatform = true;
} else {
if (piece) {
if (piece.side === opponentSide) moves.push({ row: r, col: c });
break;
}
}
}
}
break;
case 'pawn':
if (side === 'red') {
const forwardR = row - 1;
if (forwardR >= 0) {
const target = boardState[forwardR][col];
if (!target || target.side === opponentSide) moves.push({ row: forwardR, col: col });
}
if (!isInOwnHalf(row, side)) {
for (const dc of [-1, 1]) {
const c = col + dc;
if (c >= 0 && c < COL_COUNT) {
const target = boardState[row][c];
if (!target || target.side === opponentSide) moves.push({ row: row, col: c });
}
}
}
} else {
const forwardR = row + 1;
if (forwardR < ROW_COUNT) {
const target = boardState[forwardR][col];
if (!target || target.side === opponentSide) moves.push({ row: forwardR, col: col });
}
if (!isInOwnHalf(row, side)) {
for (const dc of [-1, 1]) {
const c = col + dc;
if (c >= 0 && c < COL_COUNT) {
const target = boardState[row][c];
if (!target || target.side === opponentSide) moves.push({ row: row, col: c });
}
}
}
}
break;
}
return moves;
}
// ==================== 合法移动 ====================
function getLegalMovesForBoard(row, col, boardState) {
const piece = boardState[row][col];
if (!piece) return [];
const rawMoves = getRawAttacks(row, col, piece.type, piece.side, boardState);
const legalMoves = [];
for (const move of rawMoves) {
const newBoard = boardState.map(r => [...r]);
newBoard[move.row][move.col] = newBoard[row][col];
newBoard[row][col] = null;
if (isKingSafe(piece.side, newBoard)) {
legalMoves.push(move);
}
}
return legalMoves;
}
function getLegalMoves(row, col) {
return getLegalMovesForBoard(row, col, board);
}
function hasAnyLegalMoveForBoard(side, boardState) {
for (let r = 0; r < ROW_COUNT; r++) {
for (let c = 0; c < COL_COUNT; c++) {
const piece = boardState[r][c];
if (piece && piece.side === side) {
if (getLegalMovesForBoard(r, c, boardState).length > 0) return true;
}
}
}
return false;
}
function hasAnyLegalMove(side) {
return hasAnyLegalMoveForBoard(side, board);
}
function checkIfInCheckForBoard(side, boardState) {
return !isKingSafe(side, boardState);
}
function checkIfInCheck(side) {
return checkIfInCheckForBoard(side, board);
}
// ==================== 执行移动 ====================
function executeMove(fromRow, fromCol, toRow, toCol, recordHistory = true) {
const piece = board[fromRow][fromCol];
const captured = board[toRow][toCol];
if (recordHistory) {
moveHistory.push({
fromRow,
fromCol,
toRow,
toCol,
piece,
captured,
prevInCheck: inCheck,
});
}
board[toRow][toCol] = piece;
board[fromRow][fromCol] = null;
lastMoveInfo = { fromRow, fromCol, toRow, toCol };
const prevTurn = currentTurn;
currentTurn = currentTurn === 'red' ? 'black' : 'red';
inCheck = checkIfInCheck(currentTurn);
const opponentHasMoves = hasAnyLegalMove(currentTurn);
if (!opponentHasMoves) {
gameOver = true;
winner = prevTurn;
inCheck = false;
}
selectedRow = null;
selectedCol = null;
validMoves = [];
}
// ==================== 悔棋 ====================
function undoMove() {
clearAiTimeout();
if (gameOver) {
gameOver = false;
winner = null;
if (moveHistory.length > 0) {
const last = moveHistory.pop();
board[last.fromRow][last.fromCol] = last.piece;
board[last.toRow][last.toCol] = last.captured;
currentTurn = last.piece.side;
inCheck = last.prevInCheck;
lastMoveInfo = moveHistory.length > 0 ? {
fromRow: moveHistory[moveHistory.length - 1].fromRow,
fromCol: moveHistory[moveHistory.length - 1].fromCol,
toRow: moveHistory[moveHistory.length - 1].toRow,
toCol: moveHistory[moveHistory.length - 1].toCol,
} : null;
}
isAiThinking = false;
selectedRow = null;
selectedCol = null;
validMoves = [];
updateUI();
drawAll();
return;
}
if (moveHistory.length === 0) return;
// 如果AI正在思考,取消并撤销红方最后一步
if (isAiThinking) {
isAiThinking = false;
const last = moveHistory.pop();
board[last.fromRow][last.fromCol] = last.piece;
board[last.toRow][last.toCol] = last.captured;
currentTurn = last.piece.side;
inCheck = last.prevInCheck;
gameOver = false;
winner = null;
lastMoveInfo = moveHistory.length > 0 ? {
fromRow: moveHistory[moveHistory.length - 1].fromRow,
fromCol: moveHistory[moveHistory.length - 1].fromCol,
toRow: moveHistory[moveHistory.length - 1].toRow,
toCol: moveHistory[moveHistory.length - 1].toCol,
} : null;
selectedRow = null;
selectedCol = null;
validMoves = [];
updateUI();
drawAll();
return;
}
// 正常情况:当前是红方回合,需要撤销AI的黑步和玩家的红步(共2步)
if (currentTurn === 'red' && moveHistory.length >= 2) {
// 撤销AI的步
const aiMove = moveHistory.pop();
board[aiMove.fromRow][aiMove.fromCol] = aiMove.piece;
board[aiMove.toRow][aiMove.toCol] = aiMove.captured;
// 撤销玩家的步
const playerMove = moveHistory.pop();
board[playerMove.fromRow][playerMove.fromCol] = playerMove.piece;
board[playerMove.toRow][playerMove.toCol] = playerMove.captured;
currentTurn = 'red';
inCheck = playerMove.prevInCheck;
gameOver = false;
winner = null;
lastMoveInfo = moveHistory.length > 0 ? {
fromRow: moveHistory[moveHistory.length - 1].fromRow,
fromCol: moveHistory[moveHistory.length - 1].fromCol,
toRow: moveHistory[moveHistory.length - 1].toRow,
toCol: moveHistory[moveHistory.length - 1].toCol,
} : null;
} else if (currentTurn === 'red' && moveHistory.length === 1) {
const last = moveHistory.pop();
board[last.fromRow][last.fromCol] = last.piece;
board[last.toRow][last.toCol] = last.captured;
currentTurn = 'red';
inCheck = last.prevInCheck;
gameOver = false;
winner = null;
lastMoveInfo = null;
} else if (currentTurn === 'black' && moveHistory.length >= 1) {
// 罕见情况:红方刚走完,AI还没被触发
const last = moveHistory.pop();
board[last.fromRow][last.fromCol] = last.piece;
board[last.toRow][last.toCol] = last.captured;
currentTurn = 'red';
inCheck = last.prevInCheck;
gameOver = false;
winner = null;
lastMoveInfo = moveHistory.length > 0 ? {
fromRow: moveHistory[moveHistory.length - 1].fromRow,
fromCol: moveHistory[moveHistory.length - 1].fromCol,
toRow: moveHistory[moveHistory.length - 1].toRow,
toCol: moveHistory[moveHistory.length - 1].toCol,
} : null;
}
isAiThinking = false;
selectedRow = null;
selectedCol = null;
validMoves = [];
updateUI();
drawAll();
}
// ==================== AI 评估函数 ====================
function evaluateBoard(boardState) {
// 从黑方视角评估:正值 = 黑方优势
let score = 0;
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const piece = boardState[row][col];
if (!piece) continue;
const baseValue = PIECE_BASE_VALUES[piece.type] || 0;
let posValue = 0;
// 位置价值
if (piece.type === 'pawn') {
if (piece.side === 'black') {
posValue = (POSITION_VALUES.blackPawnPos[row] &&
POSITION_VALUES.blackPawnPos[row][col]) || 0;
} else {
posValue = (POSITION_VALUES.redPawnPos[row] &&
POSITION_VALUES.redPawnPos[row][col]) || 0;
}
} else if (piece.type === 'knight') {
posValue = (POSITION_VALUES.knightPos[row] && POSITION_VALUES.knightPos[row][col]) ||
0;
} else if (piece.type === 'rook') {
posValue = (POSITION_VALUES.rookPos[row] && POSITION_VALUES.rookPos[row][col]) || 0;
} else if (piece.type === 'cannon') {
posValue = (POSITION_VALUES.cannonPos[row] && POSITION_VALUES.cannonPos[row][col]) ||
0;
}
const totalPieceValue = baseValue + posValue;
if (piece.side === 'black') {
score += totalPieceValue;
} else {
score -= totalPieceValue;
}
}
}
// 额外:检查黑方是否被将军
if (checkIfInCheckForBoard('black', boardState)) {
score -= 350; // 黑方被将军,惩罚
}
// 检查红方是否被将军
if (checkIfInCheckForBoard('red', boardState)) {
score += 350; // 红方被将军,奖励
}
return score;
}
// ==================== Alpha-Beta 搜索 ====================
function alphaBeta(boardState, depth, alpha, beta, isMaximizing, side) {
// side: 当前走棋方
if (depth === 0) {
return evaluateBoard(boardState);
}
const opponentSide = side === 'black' ? 'red' : 'black';
// 检查当前走棋方是否有合法走法
if (!hasAnyLegalMoveForBoard(side, boardState)) {
// 当前方被将死
if (side === 'black') {
return -100000 - depth; // 黑方输了,非常不利
} else {
return 100000 + depth; // 红方输了,非常有利
}
}
// 收集所有走法
const allMoves = [];
for (let r = 0; r < ROW_COUNT; r++) {
for (let c = 0; c < COL_COUNT; c++) {
const piece = boardState[r][c];
if (piece && piece.side === side) {
const moves = getLegalMovesForBoard(r, c, boardState);
for (const move of moves) {
allMoves.push({
fromRow: r,
fromCol: c,
toRow: move.row,
toCol: move.col,
captured: boardState[move.row][move.col],
});
}
}
}
}
// 走法排序:吃子走法优先(提高剪枝效率)
allMoves.sort((a, b) => {
const valA = a.captured ? (PIECE_BASE_VALUES[a.captured.type] || 0) : 0;
const valB = b.captured ? (PIECE_BASE_VALUES[b.captured.type] || 0) : 0;
return valB - valA;
});
if (isMaximizing) {
// 黑方(AI)走棋,最大化
let maxEval = -Infinity;
for (const move of allMoves) {
// 执行走法(原地修改)
const piece = boardState[move.fromRow][move.fromCol];
const captured = boardState[move.toRow][move.toCol];
boardState[move.toRow][move.toCol] = piece;
boardState[move.fromRow][move.fromCol] = null;
const evalScore = alphaBeta(boardState, depth - 1, alpha, beta, false, opponentSide);
// 撤销走法
boardState[move.fromRow][move.fromCol] = piece;
boardState[move.toRow][move.toCol] = captured;
maxEval = Math.max(maxEval, evalScore);
alpha = Math.max(alpha, evalScore);
if (beta <= alpha) break; // 剪枝
}
return maxEval;
} else {
// 红方走棋,最小化
let minEval = Infinity;
for (const move of allMoves) {
const piece = boardState[move.fromRow][move.fromCol];
const captured = boardState[move.toRow][move.toCol];
boardState[move.toRow][move.toCol] = piece;
boardState[move.fromRow][move.fromCol] = null;
const evalScore = alphaBeta(boardState, depth - 1, alpha, beta, true, opponentSide);
boardState[move.fromRow][move.fromCol] = piece;
boardState[move.toRow][move.toCol] = captured;
minEval = Math.min(minEval, evalScore);
beta = Math.min(beta, evalScore);
if (beta <= alpha) break;
}
return minEval;
}
}
// ==================== AI 选择最佳走法 ====================
function findBestAIMove() {
const side = 'black';
const searchDepth = 3;
const allMoves = [];
for (let r = 0; r < ROW_COUNT; r++) {
for (let c = 0; c < COL_COUNT; c++) {
const piece = board[r][c];
if (piece && piece.side === side) {
const moves = getLegalMoves(r, c);
for (const move of moves) {
allMoves.push({
fromRow: r,
fromCol: c,
toRow: move.row,
toCol: move.col,
captured: board[move.row][move.col],
});
}
}
}
}
if (allMoves.length === 0) return null;
// 走法排序
allMoves.sort((a, b) => {
const valA = a.captured ? (PIECE_BASE_VALUES[a.captured.type] || 0) : 0;
const valB = b.captured ? (PIECE_BASE_VALUES[b.captured.type] || 0) : 0;
return valB - valA;
});
let bestMove = allMoves[0];
let bestScore = -Infinity;
const alpha = -Infinity;
const beta = Infinity;
// 创建工作棋盘(深拷贝一次)
const workBoard = board.map(r => [...r]);
for (const move of allMoves) {
const piece = workBoard[move.fromRow][move.fromCol];
const captured = workBoard[move.toRow][move.toCol];
workBoard[move.toRow][move.toCol] = piece;
workBoard[move.fromRow][move.fromCol] = null;
const score = alphaBeta(workBoard, searchDepth - 1, alpha, beta, false, 'red');
// 撤销
workBoard[move.fromRow][move.fromCol] = piece;
workBoard[move.toRow][move.toCol] = captured;
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
// 如果有多个得分相同的走法,随机选择(增加变化)
const topMoves = allMoves.filter(move => {
const piece = workBoard[move.fromRow][move.fromCol];
const captured = workBoard[move.toRow][move.toCol];
workBoard[move.toRow][move.toCol] = piece;
workBoard[move.fromRow][move.fromCol] = null;
const s = alphaBeta(workBoard, searchDepth - 1, -Infinity, Infinity, false, 'red');
workBoard[move.fromRow][move.fromCol] = piece;
workBoard[move.toRow][move.toCol] = captured;
return Math.abs(s - bestScore) < 5;
});
if (topMoves.length > 1) {
bestMove = topMoves[Math.floor(Math.random() * topMoves.length)];
}
return bestMove;
}
// ==================== AI 走棋触发 ====================
function triggerAI() {
if (gameOver) return;
if (currentTurn !== 'black') return;
if (isAiThinking) return;
isAiThinking = true;
updateUI();
drawAll();
aiTimeoutId = setTimeout(() => {
aiTimeoutId = null;
if (gameOver || currentTurn !== 'black') {
isAiThinking = false;
updateUI();
drawAll();
return;
}
const bestMove = findBestAIMove();
if (!bestMove) {
// AI没有合法走法,应该已经被将死
gameOver = true;
winner = 'red';
inCheck = false;
isAiThinking = false;
updateUI();
drawAll();
return;
}
executeMove(bestMove.fromRow, bestMove.fromCol, bestMove.toRow, bestMove.toCol, true);
isAiThinking = false;
updateUI();
drawAll();
// AI走完后,如果游戏还没结束,轮到红方
// 不需要额外操作
}, AI_DELAY);
}
// ==================== 绘制 ====================
function drawAll() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
drawBoardBackground();
drawGridLines();
drawRiver();
drawPalaceDiagonals();
drawLastMoveHighlight();
drawValidMoves();
drawAllPieces();
drawSelectionHighlight();
drawCheckHighlight();
}
function drawBoardBackground() {
const bgGrad = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
bgGrad.addColorStop(0, '#f5deb3');
bgGrad.addColorStop(0.5, '#faf0dc');
bgGrad.addColorStop(1, '#e8d5a0');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.strokeStyle = '#5d3a1a';
ctx.lineWidth = 4;
const bx = MARGIN - 18;
const by = MARGIN - 18;
const bw = CELL_SIZE * (COL_COUNT - 1) + 36;
const bh = CELL_SIZE * (ROW_COUNT - 1) + 36;
ctx.strokeRect(bx, by, bw, bh);
ctx.strokeStyle = '#3a1f04';
ctx.lineWidth = 1.5;
ctx.strokeRect(bx - 3, by - 3, bw + 6, bh + 6);
}
function drawGridLines() {
const startX = MARGIN;
const startY = MARGIN;
const endX = MARGIN + CELL_SIZE * (COL_COUNT - 1);
const endY = MARGIN + CELL_SIZE * (ROW_COUNT - 1);
const midY = MARGIN + CELL_SIZE * 4;
const midY2 = MARGIN + CELL_SIZE * 5;
ctx.strokeStyle = '#4a2a0a';
ctx.lineWidth = 1.2;
for (let row = 0; row <= 4; row++) {
const y = MARGIN + row * CELL_SIZE;
ctx.beginPath();
ctx.moveTo(startX, y);
ctx.lineTo(endX, y);
ctx.stroke();
}
for (let row = 5; row <= 9; row++) {
const y = MARGIN + row * CELL_SIZE;
ctx.beginPath();
ctx.moveTo(startX, y);
ctx.lineTo(endX, y);
ctx.stroke();
}
for (let col = 0; col < COL_COUNT; col++) {
const x = MARGIN + col * CELL_SIZE;
ctx.beginPath();
ctx.moveTo(x, startY);
ctx.lineTo(x, midY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, midY2);
ctx.lineTo(x, endY);
ctx.stroke();
}
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX, endY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(endX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
}
function drawRiver() {
const midY1 = MARGIN + CELL_SIZE * 4;
const midY2 = MARGIN + CELL_SIZE * 5;
const startX = MARGIN;
const endX = MARGIN + CELL_SIZE * (COL_COUNT - 1);
const centerY = (midY1 + midY2) / 2;
ctx.fillStyle = '#3a1f04';
ctx.font = 'bold 22px "STKaiti","KaiTi","楷体","STSong","宋体",serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const leftCenterX = MARGIN + CELL_SIZE * 2;
const rightCenterX = MARGIN + CELL_SIZE * 6;
ctx.fillText('楚 河', leftCenterX, centerY);
ctx.fillText('汉 界', rightCenterX, centerY);
ctx.strokeStyle = '#4a2a0a';
ctx.lineWidth = 0.5;
ctx.setLineDash([4, 8]);
ctx.beginPath();
ctx.moveTo(startX + 8, centerY);
ctx.lineTo(endX - 8, centerY);
ctx.stroke();
ctx.setLineDash([]);
}
function drawPalaceDiagonals() {
ctx.strokeStyle = '#4a2a0a';
ctx.lineWidth = 0.9;
ctx.setLineDash([3, 3]);
const blackTopLeft = rowColToXY(0, 3);
const blackTopRight = rowColToXY(0, 5);
const blackBotLeft = rowColToXY(2, 3);
const blackBotRight = rowColToXY(2, 5);
ctx.beginPath();
ctx.moveTo(blackTopLeft.x, blackTopLeft.y);
ctx.lineTo(blackBotRight.x, blackBotRight.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(blackTopRight.x, blackTopRight.y);
ctx.lineTo(blackBotLeft.x, blackBotLeft.y);
ctx.stroke();
const redTopLeft = rowColToXY(7, 3);
const redTopRight = rowColToXY(7, 5);
const redBotLeft = rowColToXY(9, 3);
const redBotRight = rowColToXY(9, 5);
ctx.beginPath();
ctx.moveTo(redTopLeft.x, redTopLeft.y);
ctx.lineTo(redBotRight.x, redBotRight.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(redTopRight.x, redTopRight.y);
ctx.lineTo(redBotLeft.x, redBotLeft.y);
ctx.stroke();
ctx.setLineDash([]);
}
function drawLastMoveHighlight() {
if (!lastMoveInfo) return;
const { fromRow, fromCol, toRow, toCol } = lastMoveInfo;
const highlightSquare = (row, col) => {
const { x, y } = rowColToXY(row, col);
const s = CELL_SIZE * 0.55;
ctx.fillStyle = 'rgba(255, 200, 50, 0.35)';
ctx.fillRect(x - s / 2, y - s / 2, s, s);
ctx.strokeStyle = 'rgba(200, 150, 30, 0.6)';
ctx.lineWidth = 2;
ctx.strokeRect(x - s / 2, y - s / 2, s, s);
};
highlightSquare(fromRow, fromCol);
highlightSquare(toRow, toCol);
}
function drawValidMoves() {
for (const move of validMoves) {
const { x, y } = rowColToXY(move.row, move.col);
const targetPiece = board[move.row][move.col];
if (targetPiece) {
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS + 3, 0, Math.PI * 2);
ctx.setLineDash([5, 3]);
ctx.strokeStyle = 'rgba(220,40,40,0.75)';
ctx.lineWidth = 3;
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = 'rgba(255,100,100,0.2)';
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(80,160,80,0.55)';
ctx.fill();
ctx.strokeStyle = 'rgba(50,120,50,0.5)';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
}
function drawAllPieces() {
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const piece = board[row][col];
if (piece) {
drawPiece(row, col, piece);
}
}
}
}
function drawPiece(row, col, piece) {
const { x, y } = rowColToXY(row, col);
const isSelected = (selectedRow === row && selectedCol === col);
const isRed = piece.side === 'red';
const pieceColor = isRed ? '#c41e3a' : '#1a1a1a';
// 阴影
ctx.beginPath();
ctx.arc(x + 2, y + 3, PIECE_RADIUS, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fill();
// 棋子主体渐变
const pieceGrad = ctx.createRadialGradient(x - 6, y - 8, PIECE_RADIUS * 0.1, x, y, PIECE_RADIUS);
pieceGrad.addColorStop(0, '#ffffff');
pieceGrad.addColorStop(0.55, '#f5f0e8');
pieceGrad.addColorStop(0.85, '#e0d5c0');
pieceGrad.addColorStop(1, '#c8b898');
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS, 0, Math.PI * 2);
ctx.fillStyle = pieceGrad;
ctx.fill();
// 边框
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS, 0, Math.PI * 2);
ctx.strokeStyle = pieceColor;
ctx.lineWidth = 2.5;
ctx.stroke();
// 内圈
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS - 4, 0, Math.PI * 2);
ctx.strokeStyle = pieceColor;
ctx.lineWidth = 1;
ctx.setLineDash([3, 2]);
ctx.stroke();
ctx.setLineDash([]);
// 选中高亮
if (isSelected) {
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS + 5, 0, Math.PI * 2);
ctx.strokeStyle = '#ffb800';
ctx.lineWidth = 3.5;
ctx.shadowColor = '#ffd700';
ctx.shadowBlur = 15;
ctx.stroke();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS + 2, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,200,50,0.5)';
ctx.lineWidth = 2;
ctx.stroke();
}
// 文字
const name = PIECE_NAMES[piece.side][piece.type];
ctx.fillStyle = pieceColor;
ctx.font = 'bold 24px "STKaiti","KaiTi","楷体","STSong","宋体","Noto Serif SC",serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(name, x, y + 1);
// 将军高亮
if (inCheck && piece.type === 'king' && piece.side === currentTurn && !gameOver) {
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS + 3, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,40,40,0.8)';
ctx.lineWidth = 3;
ctx.setLineDash([6, 3]);
ctx.stroke();
ctx.setLineDash([]);
}
}
function drawSelectionHighlight() {
if (selectedRow === null || selectedCol === null) return;
const { x, y } = rowColToXY(selectedRow, selectedCol);
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS + 7, 0, Math.PI * 2);
const glowGrad = ctx.createRadialGradient(x, y, PIECE_RADIUS, x, y, PIECE_RADIUS + 8);
glowGrad.addColorStop(0, 'rgba(255,200,60,0)');
glowGrad.addColorStop(1, 'rgba(255,200,60,0.35)');
ctx.fillStyle = glowGrad;
ctx.fill();
}
function drawCheckHighlight() {
if (!inCheck || gameOver) return;
const kingPos = findKing(currentTurn);
if (!kingPos) return;
const { x, y } = rowColToXY(kingPos.row, kingPos.col);
const pulseGrad = ctx.createRadialGradient(x, y, PIECE_RADIUS + 2, x, y, PIECE_RADIUS + 10);
pulseGrad.addColorStop(0, 'rgba(255,60,60,0)');
pulseGrad.addColorStop(1, 'rgba(255,30,30,0.25)');
ctx.beginPath();
ctx.arc(x, y, PIECE_RADIUS + 10, 0, Math.PI * 2);
ctx.fillStyle = pulseGrad;
ctx.fill();
}
// ==================== UI更新 ====================
function updateUI() {
// Canvas wrapper状态
if (isAiThinking) {
canvasWrapper.classList.add('ai-thinking-wrapper');
canvasWrapper.classList.add('no-interact');
} else if (gameOver) {
canvasWrapper.classList.remove('ai-thinking-wrapper');
canvasWrapper.classList.add('no-interact');
} else if (currentTurn === 'black') {
canvasWrapper.classList.remove('ai-thinking-wrapper');
canvasWrapper.classList.add('no-interact');
} else {
canvasWrapper.classList.remove('ai-thinking-wrapper');
canvasWrapper.classList.remove('no-interact');
}
// 状态消息
if (gameOver) {
turnIndicator.className = 'turn-indicator';
turnIndicator.innerHTML =
'<span class="turn-dot" style="background:#ffd700;box-shadow:0 0 10px #ffd700;"></span> 游戏结束';
if (winner === 'red') {
statusMessage.textContent = '🏆 恭喜!红方(你)获胜!黑方无棋可走。';
} else {
statusMessage.textContent = '🤖 黑方(AI)获胜!红方无棋可走。';
}
statusMessage.className = 'status-message game-over';
btnUndo.disabled = false;
} else if (isAiThinking) {
turnIndicator.className = 'turn-indicator black-turn';
turnIndicator.innerHTML =
'<span class="turn-dot black"></span> 黑方思考中... <span style="font-size:0.8em;">🤖</span>';
statusMessage.textContent = '🤖 AI正在计算最佳走法...';
statusMessage.className = 'status-message ai-thinking';
btnUndo.disabled = false;
} else if (inCheck && currentTurn === 'red') {
turnIndicator.className = 'turn-indicator red-turn';
turnIndicator.innerHTML =
'<span class="turn-dot red"></span> 红方走棋(你) <span style="color:#ff4444;font-size:0.85em;">⚠将军</span>';
statusMessage.textContent = '⚠ 你的帅被将军!必须应将。';
statusMessage.className = 'status-message check-warning';
btnUndo.disabled = moveHistory.length < 2;
} else if (inCheck && currentTurn === 'black') {
turnIndicator.className = 'turn-indicator black-turn';
turnIndicator.innerHTML =
'<span class="turn-dot black"></span> 黑方走棋(AI) <span style="color:#ff4444;font-size:0.85em;">⚠将军</span>';
statusMessage.textContent = '⚠ 黑方被将军,AI正在应对...';
statusMessage.className = 'status-message check-warning';
btnUndo.disabled = true;
} else if (currentTurn === 'red') {
turnIndicator.className = 'turn-indicator red-turn';
turnIndicator.innerHTML = '<span class="turn-dot red"></span> 红方走棋(你)';
statusMessage.textContent = '';
statusMessage.className = 'status-message';
btnUndo.disabled = moveHistory.length < 2;
} else {
turnIndicator.className = 'turn-indicator black-turn';
turnIndicator.innerHTML = '<span class="turn-dot black"></span> 黑方走棋(AI)';
statusMessage.textContent = '';
statusMessage.className = 'status-message';
btnUndo.disabled = true;
}
}
// ==================== 事件处理 ====================
canvas.addEventListener('click', function(e) {
if (gameOver) return;
if (isAiThinking) return;
if (currentTurn !== 'red') return; // 只有红方(玩家)可以手动走棋
const rect = canvas.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
const pos = canvasToRowCol(canvasX, canvasY);
if (!pos) {
selectedRow = null;
selectedCol = null;
validMoves = [];
drawAll();
return;
}
const { row, col } = pos;
const clickedPiece = getPieceAt(row, col);
if (selectedRow !== null && selectedCol !== null) {
const isValidTarget = validMoves.some(m => m.row === row && m.col === col);
if (isValidTarget) {
executeMove(selectedRow, selectedCol, row, col, true);
updateUI();
drawAll();
// 检查是否需要触发AI
if (!gameOver && currentTurn === 'black') {
triggerAI();
}
return;
}
}
if (clickedPiece && clickedPiece.side === 'red') {
selectedRow = row;
selectedCol = col;
validMoves = getLegalMoves(row, col);
drawAll();
return;
}
selectedRow = null;
selectedCol = null;
validMoves = [];
drawAll();
});
canvas.addEventListener('touchstart', function(e) {
e.preventDefault();
if (gameOver) return;
if (isAiThinking) return;
if (currentTurn !== 'red') return;
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const canvasX = touch.clientX - rect.left;
const canvasY = touch.clientY - rect.top;
const pos = canvasToRowCol(canvasX, canvasY);
if (!pos) {
selectedRow = null;
selectedCol = null;
validMoves = [];
drawAll();
return;
}
const { row, col } = pos;
const clickedPiece = getPieceAt(row, col);
if (selectedRow !== null && selectedCol !== null) {
const isValidTarget = validMoves.some(m => m.row === row && m.col === col);
if (isValidTarget) {
executeMove(selectedRow, selectedCol, row, col, true);
updateUI();
drawAll();
if (!gameOver && currentTurn === 'black') {
triggerAI();
}
return;
}
}
if (clickedPiece && clickedPiece.side === 'red') {
selectedRow = row;
selectedCol = col;
validMoves = getLegalMoves(row, col);
drawAll();
return;
}
selectedRow = null;
selectedCol = null;
validMoves = [];
drawAll();
}, { passive: false });
btnRestart.addEventListener('click', resetGame);
btnUndo.addEventListener('click', function() {
if (isAiThinking) {
// AI正在思考,允许悔棋取消
undoMove();
return;
}
if (gameOver) {
undoMove();
return;
}
if (currentTurn === 'red' && moveHistory.length >= 2) {
undoMove();
} else if (currentTurn === 'red' && moveHistory.length === 1) {
undoMove();
}
});
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'z') {
e.preventDefault();
if (isAiThinking || gameOver || (currentTurn === 'red' && moveHistory.length >= 1)) {
undoMove();
}
}
if (e.key === 'r' && e.ctrlKey) {
e.preventDefault();
resetGame();
}
if (e.key === 'Escape') {
if (!isAiThinking && currentTurn === 'red') {
selectedRow = null;
selectedCol = null;
validMoves = [];
drawAll();
}
}
});
// ==================== 启动 ====================
function init() {
resetGame();
updateUI();
drawAll();
}
init();
console.log('🏯 中国象棋 · 人机对弈模式已就绪');
console.log(' - 你扮演红方(下方),AI扮演黑方(上方)');
console.log(' - 点击己方棋子选中,再点击目标位置移动');
console.log(' - 绿色圆点 = 可走位置,红色虚线圈 = 可吃子位置');
console.log(' - AI使用Alpha-Beta搜索,深度3层');
console.log(' - Ctrl+Z 或点击"悔棋"撤销(会撤销AI回应+你的上一步)');
console.log(' - Ctrl+R 重新开始 | Esc 取消选择');
})();
</script>
</body>
</html>
国际象棋

html
<!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>
:root {
--board-size: min(560px, 90vw);
--cell-size: calc(var(--board-size) / 8);
--light-cell: #f0d9b5;
--dark-cell: #b58863;
--selected: #7fc97f;
--legal-move: rgba(0, 0, 0, 0.25);
--legal-capture: rgba(255, 50, 50, 0.45);
--last-move: rgba(255, 255, 0, 0.5);
--check: rgba(255, 60, 60, 0.75);
--bg: #1a1a2e;
--panel-bg: #16213e;
--text: #e0e0e0;
--accent: #e6b422;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
user-select: none;
padding: 10px;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
max-width: 700px;
}
.board-wrapper {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 30px rgba(230,180,34,0.25), 0 8px 32px rgba(0,0,0,0.5);
border: 4px solid #3d2b1f;
}
.board-container {
display: grid;
grid-template-columns: repeat(8, var(--cell-size));
grid-template-rows: repeat(8, var(--cell-size));
width: var(--board-size);
height: var(--board-size);
}
.cell {
width: var(--cell-size);
height: var(--cell-size);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
font-size: calc(var(--cell-size) * 0.78);
transition: background-color 0.1s;
}
.cell.light { background: var(--light-cell); }
.cell.dark { background: var(--dark-cell); }
.cell.selected { background: var(--selected) !important; box-shadow: inset 0 0 0 3px rgba(0,0,0,0.4); }
.cell.last-move { background: var(--last-move) !important; }
.cell.legal-move::after {
content: '';
position: absolute;
width: 28%;
height: 28%;
background: var(--legal-move);
border-radius: 50%;
pointer-events: none;
}
.cell.legal-capture::after {
content: '';
position: absolute;
width: 85%;
height: 85%;
border: 5px solid rgba(220,40,40,0.65);
border-radius: 50%;
pointer-events: none;
}
.cell.in-check {
background: var(--check) !important;
animation: checkPulse 0.6s infinite alternate;
}
@keyframes checkPulse {
from { box-shadow: inset 0 0 8px rgba(255,0,0,0.7); }
to { box-shadow: inset 0 0 22px rgba(255,0,0,0.95); }
}
.piece { pointer-events: none; text-shadow: 0 1px 3px rgba(0,0,0,0.3); }
.promotion-overlay {
display: none;
position: fixed; top:0;left:0; width:100%; height:100%;
background: rgba(0,0,0,0.7); z-index:100;
justify-content: center; align-items: center;
}
.promotion-overlay.active { display: flex; }
.promotion-dialog {
background: #2c2416; border: 3px solid var(--accent);
border-radius: 16px; padding: 20px;
display: flex; flex-direction: column; align-items: center; gap: 12px;
}
.promotion-options { display: flex; gap: 10px; }
.promotion-btn {
width: 60px; height: 60px; font-size: 42px;
border-radius: 12px; cursor: pointer; border: 2px solid #5a4a30;
background: #f0d9b5; transition: 0.15s;
display: flex; align-items: center; justify-content: center;
}
.promotion-btn:hover { background: #ffe0a0; transform: scale(1.1); }
.info-panel { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
.status {
background: var(--panel-bg); color: var(--text);
padding: 8px 18px; border-radius: 25px; font-weight: 600;
min-width: 160px; text-align: center; border: 1px solid rgba(255,255,255,0.1);
}
.status.thinking { color: #4ecdc4; animation: thinkGlow 0.8s infinite alternate; }
.status.gameover { color: #ff6b6b; }
@keyframes thinkGlow {
from { box-shadow: 0 0 8px rgba(78,205,196,0.4); }
to { box-shadow: 0 0 22px rgba(78,205,196,0.8); }
}
.btn {
padding: 8px 20px; border-radius: 25px; font-weight: 700;
cursor: pointer; border: 2px solid transparent; font-family: inherit;
background: var(--accent); color: #1a1a2e; border-color: var(--accent);
}
.btn:hover { background: #f0c830; transform: translateY(-1px); }
.btn:disabled { opacity: 0.4; pointer-events: none; }
.btn-undo { background: transparent; color: var(--text); border-color: rgba(255,255,255,0.3); }
.btn-undo:hover { background: rgba(255,255,255,0.1); }
.captured-area { font-size: 1.2rem; color: #ccc; min-height: 24px; }
</style>
</head>
<body>
<div class="container">
<div class="board-wrapper"><div class="board-container" id="board"></div></div>
<div class="captured-area" id="capturedByWhite"></div>
<div class="captured-area" id="capturedByBlack"></div>
<div class="info-panel">
<button class="btn btn-undo" id="btnUndo">⟲ 撤回</button>
<div class="status" id="status">轮到你走 · 白方</div>
<button class="btn" id="btnNewGame">🔄 新游戏</button>
</div>
</div>
<div class="promotion-overlay" id="promotionOverlay">
<div class="promotion-dialog">
<span style="color:#fff">选择升变棋子</span>
<div class="promotion-options" id="promotionOptions"></div>
</div>
</div>
<script>
(function() {
// ----- 基础定义 -----
const EMPTY = 0;
const WP=1, WN=2, WB=3, WR=4, WQ=5, WK=6;
const BP=7, BN=8, BB=9, BR=10, BQ=11, BK=12;
const WHITE='white', BLACK='black';
const PIECE_COLOR = p => p===EMPTY ? null : (p<=6 ? WHITE : BLACK);
const UNICODE = { [WP]:'♙',[WN]:'♘',[WB]:'♗',[WR]:'♖',[WQ]:'♕',[WK]:'♔',
[BP]:'♟',[BN]:'♞',[BB]:'♝',[BR]:'♜',[BQ]:'♛',[BK]:'♚' };
const VALUE = { [WP]:100,[WN]:320,[WB]:330,[WR]:500,[WQ]:900,[WK]:20000,
[BP]:100,[BN]:320,[BB]:330,[BR]:500,[BQ]:900,[BK]:20000 };
// ----- 游戏状态 -----
class GameState {
constructor() { this.reset(); }
reset() {
this.board = Array(8).fill().map(()=>Array(8).fill(EMPTY));
// 黑方 (row 0)
this.board[0] = [BR,BN,BB,BQ,BK,BB,BN,BR];
this.board[1] = [BP,BP,BP,BP,BP,BP,BP,BP];
// 白方 (row 7)
this.board[6] = [WP,WP,WP,WP,WP,WP,WP,WP];
this.board[7] = [WR,WN,WB,WQ,WK,WB,WN,WR];
this.currentPlayer = WHITE;
this.castling = { wK:true, wQ:true, bK:true, bQ:true };
this.epTarget = null;
this.moveHistory = [];
this.capturedByWhite = [];
this.capturedByBlack = [];
this.gameOver = false;
this.result = null;
this.lastFrom = null; this.lastTo = null;
this.inCheck = false;
}
clone() {
const c = new GameState();
c.board = this.board.map(r => [...r]);
c.currentPlayer = this.currentPlayer;
c.castling = {...this.castling};
c.epTarget = this.epTarget ? {...this.epTarget} : null;
c.moveHistory = [...this.moveHistory];
c.capturedByWhite = [...this.capturedByWhite];
c.capturedByBlack = [...this.capturedByBlack];
c.gameOver = this.gameOver;
c.result = this.result;
c.lastFrom = this.lastFrom; c.lastTo = this.lastTo;
c.inCheck = this.inCheck;
return c;
}
piece(r,c) { return (r>=0&&r<8&&c>=0&&c<8) ? this.board[r][c] : null; }
set(r,c,p) { this.board[r][c] = p; }
}
// ----- 攻击检测 -----
function isAttackedBy(gs, row, col, attackerColor) {
const enemyPawn = attackerColor===WHITE ? WP : BP;
const enemyKnight = attackerColor===WHITE ? WN : BN;
const enemyBishop = attackerColor===WHITE ? WB : BB;
const enemyRook = attackerColor===WHITE ? WR : BR;
const enemyQueen = attackerColor===WHITE ? WQ : BQ;
const enemyKing = attackerColor===WHITE ? WK : BK;
const pawnDir = attackerColor===WHITE ? -1 : 1;
// 兵的攻击
const pr = row - pawnDir;
if(pr>=0&&pr<=7) {
if(col-1>=0 && gs.piece(pr,col-1)===enemyPawn) return true;
if(col+1<=7 && gs.piece(pr,col+1)===enemyPawn) return true;
}
// 马
for(const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
const r=row+dr, c=col+dc;
if(r>=0&&r<8&&c>=0&&c<8 && gs.piece(r,c)===enemyKnight) return true;
}
// 滑子
const dirs = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];
for(const [dr,dc] of dirs) {
for(let i=1; i<=7; i++) {
const r=row+dr*i, c=col+dc*i;
if(r<0||r>7||c<0||c>7) break;
const p = gs.piece(r,c);
if(p===EMPTY) continue;
if(p===enemyBishop && dr!==0 && dc!==0) return true;
if(p===enemyRook && (dr===0||dc===0)) return true;
if(p===enemyQueen) return true;
break;
}
}
// 王
for(const [dr,dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
const r=row+dr, c=col+dc;
if(r>=0&&r<8&&c>=0&&c<8 && gs.piece(r,c)===enemyKing) return true;
}
return false;
}
function findKing(gs, color) {
const king = color===WHITE ? WK : BK;
for(let r=0;r<8;r++) for(let c=0;c<8;c++) if(gs.board[r][c]===king) return {row:r,col:c};
return null;
}
function inCheck(gs, color) {
const k = findKing(gs, color);
if(!k) return true;
return isAttackedBy(gs, k.row, k.col, color===WHITE?BLACK:WHITE);
}
// ----- 走法生成 (伪合法) -----
function pseudoMoves(gs, row, col) {
const piece = gs.piece(row,col);
if(!piece) return [];
const color = PIECE_COLOR(piece);
const isW = color===WHITE;
const forward = isW ? -1 : 1;
const startRow = isW ? 6 : 1;
const promoRow = isW ? 0 : 7;
const enemy = isW ? BLACK : WHITE;
const moves = [];
const add = (tr, tc, cap, special=null) => {
if(tr<0||tr>7||tc<0||tc>7) return;
const target = gs.piece(tr,tc);
if(target===EMPTY || PIECE_COLOR(target)===enemy) {
moves.push({fr:row,fc:col,tr,tc,piece,captured:target,special,promotion:null});
}
};
if(piece===WP || piece===BP) {
const fwd = row+forward;
if(fwd>=0&&fwd<=7 && gs.piece(fwd,col)===EMPTY) {
if(fwd===promoRow) {
const opts = isW ? [WQ,WR,WB,WN] : [BQ,BR,BB,BN];
opts.forEach(pp => moves.push({fr:row,fc:col,tr:fwd,tc:col,piece,captured:EMPTY,special:'promo',promotion:pp}));
} else {
add(fwd, col, EMPTY);
if(row===startRow && gs.piece(fwd+forward,col)===EMPTY) add(row+2*forward, col, EMPTY, 'double');
}
}
for(const dc of [-1,1]) {
const capRow = row+forward, capCol = col+dc;
if(capRow<0||capRow>7||capCol<0||capCol>7) continue;
const t = gs.piece(capRow,capCol);
if(t!==EMPTY && PIECE_COLOR(t)===enemy) {
if(capRow===promoRow) {
const opts = isW ? [WQ,WR,WB,WN] : [BQ,BR,BB,BN];
opts.forEach(pp => moves.push({fr:row,fc:col,tr:capRow,tc:capCol,piece,captured:t,special:'promo',promotion:pp}));
} else add(capRow, capCol, t);
}
if(gs.epTarget && gs.epTarget.row===capRow && gs.epTarget.col===capCol) {
moves.push({fr:row,fc:col,tr:capRow,tc:capCol,piece,captured:isW?BP:WP,special:'ep',promotion:null});
}
}
}
else if(piece===WN || piece===BN) {
for(const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) add(row+dr,col+dc);
}
else if(piece===WB || piece===BB) {
for(const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
for(let i=1;i<=7;i++) { const r=row+dr*i, c=col+dc*i; if(r<0||r>7||c<0||c>7) break; const t=gs.piece(r,c); if(t===EMPTY) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:EMPTY,special:null,promotion:null}); else { if(PIECE_COLOR(t)===enemy) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:t,special:null,promotion:null}); break; } }
}
}
else if(piece===WR || piece===BR) {
for(const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
for(let i=1;i<=7;i++) { const r=row+dr*i, c=col+dc*i; if(r<0||r>7||c<0||c>7) break; const t=gs.piece(r,c); if(t===EMPTY) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:EMPTY,special:null,promotion:null}); else { if(PIECE_COLOR(t)===enemy) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:t,special:null,promotion:null}); break; } }
}
}
else if(piece===WQ || piece===BQ) {
for(const [dr,dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
for(let i=1;i<=7;i++) { const r=row+dr*i, c=col+dc*i; if(r<0||r>7||c<0||c>7) break; const t=gs.piece(r,c); if(t===EMPTY) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:EMPTY,special:null,promotion:null}); else { if(PIECE_COLOR(t)===enemy) moves.push({fr:row,fc:col,tr:r,tc:c,piece,captured:t,special:null,promotion:null}); break; } }
}
}
else if(piece===WK || piece===BK) {
for(const [dr,dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) add(row+dr,col+dc);
// 易位
if(!inCheck(gs, color)) {
const kr = isW?7:0;
if(row===kr && col===4) {
if(gs.castling[isW?'wK':'bK'] && gs.piece(kr,5)===EMPTY && gs.piece(kr,6)===EMPTY &&
gs.piece(kr,7)===(isW?WR:BR) && !isAttackedBy(gs,kr,5,enemy) && !isAttackedBy(gs,kr,6,enemy))
moves.push({fr:row,fc:col,tr:kr,tc:6,piece,captured:EMPTY,special:'castleK',promotion:null});
if(gs.castling[isW?'wQ':'bQ'] && gs.piece(kr,1)===EMPTY && gs.piece(kr,2)===EMPTY && gs.piece(kr,3)===EMPTY &&
gs.piece(kr,0)===(isW?WR:BR) && !isAttackedBy(gs,kr,2,enemy) && !isAttackedBy(gs,kr,3,enemy))
moves.push({fr:row,fc:col,tr:kr,tc:2,piece,captured:EMPTY,special:'castleQ',promotion:null});
}
}
}
return moves;
}
function applyDirect(gs, m) {
const {fr,fc,tr,tc,piece,special,promotion} = m;
const isW = PIECE_COLOR(piece)===WHITE;
if(special==='ep') {
const capRow = isW ? tr+1 : tr-1;
gs.set(capRow, tc, EMPTY);
}
gs.set(fr, fc, EMPTY);
gs.set(tr, tc, (special==='promo' && promotion) ? promotion : piece);
if(special==='castleK') { const kr=isW?7:0; gs.set(kr,7,EMPTY); gs.set(kr,5,isW?WR:BR); }
if(special==='castleQ') { const kr=isW?7:0; gs.set(kr,0,EMPTY); gs.set(kr,3,isW?WR:BR); }
// 更新易位权
if(piece===WK) { gs.castling.wK=gs.castling.wQ=false; }
if(piece===BK) { gs.castling.bK=gs.castling.bQ=false; }
if(fr===7&&fc===0||tr===7&&tc===0) gs.castling.wQ=false;
if(fr===7&&fc===7||tr===7&&tc===7) gs.castling.wK=false;
if(fr===0&&fc===0||tr===0&&tc===0) gs.castling.bQ=false;
if(fr===0&&fc===7||tr===0&&tc===7) gs.castling.bK=false;
gs.epTarget = (special==='double') ? {row: isW?tr+1:tr-1, col:tc} : null;
gs.currentPlayer = gs.currentPlayer===WHITE ? BLACK : WHITE;
}
function legalMoves(gs, row, col) {
const all = pseudoMoves(gs, row, col);
const color = PIECE_COLOR(gs.piece(row,col));
const result = [];
for(const m of all) {
const test = gs.clone();
applyDirect(test, m);
if(!inCheck(test, color)) result.push(m);
}
return result;
}
function allLegalMoves(gs, color) {
const moves = [];
for(let r=0;r<8;r++) for(let c=0;c<8;c++) {
if(gs.piece(r,c)!==EMPTY && PIECE_COLOR(gs.piece(r,c))===color)
moves.push(...legalMoves(gs, r, c));
}
return moves;
}
// ----- 执行走法 (完整) -----
function applyFull(gs, m) {
const isW = PIECE_COLOR(m.piece)===WHITE;
let capPiece = m.captured;
if(m.special==='ep') capPiece = isW ? BP : WP;
if(capPiece!==EMPTY) {
if(isW) gs.capturedByWhite.push(capPiece);
else gs.capturedByBlack.push(capPiece);
}
const hist = {
m: {...m},
castling: {...gs.castling},
ep: gs.epTarget ? {...gs.epTarget} : null,
lastFrom: gs.lastFrom, lastTo: gs.lastTo,
inCheck: gs.inCheck, capPiece
};
applyDirect(gs, m);
gs.lastFrom = {row:m.fr, col:m.fc};
gs.lastTo = {row:m.tr, col:m.tc};
gs.inCheck = inCheck(gs, gs.currentPlayer);
if(allLegalMoves(gs, gs.currentPlayer).length===0) {
gs.gameOver = true;
gs.result = gs.inCheck ? (gs.currentPlayer===WHITE?'black-wins':'white-wins') : 'draw';
}
gs.moveHistory.push(hist);
}
function undoMove(gs) {
if(!gs.moveHistory.length) return false;
const h = gs.moveHistory.pop();
const m = h.m;
gs.set(m.tr, m.tc, EMPTY);
gs.set(m.fr, m.fc, m.piece);
if(m.special==='ep') {
const capRow = PIECE_COLOR(m.piece)===WHITE ? m.tr+1 : m.tr-1;
gs.set(capRow, m.tc, h.capPiece);
} else if(m.captured!==EMPTY) {
gs.set(m.tr, m.tc, m.captured);
}
if(m.special==='castleK') { const kr=PIECE_COLOR(m.piece)===WHITE?7:0; gs.set(kr,5,EMPTY); gs.set(kr,7,PIECE_COLOR(m.piece)===WHITE?WR:BR); }
if(m.special==='castleQ') { const kr=PIECE_COLOR(m.piece)===WHITE?7:0; gs.set(kr,3,EMPTY); gs.set(kr,0,PIECE_COLOR(m.piece)===WHITE?WR:BR); }
gs.castling = h.castling;
gs.epTarget = h.ep;
gs.lastFrom = h.lastFrom; gs.lastTo = h.lastTo;
gs.inCheck = h.inCheck;
gs.currentPlayer = gs.currentPlayer===WHITE ? BLACK : WHITE;
gs.gameOver = false; gs.result = null;
const arr = PIECE_COLOR(m.piece)===WHITE ? gs.capturedByWhite : gs.capturedByBlack;
const idx = arr.lastIndexOf(h.capPiece);
if(idx>=0) arr.splice(idx,1);
return true;
}
// ----- AI (深度3) -----
function evaluate(gs) {
let score=0;
for(let r=0;r<8;r++) for(let c=0;c<8;c++) {
const p=gs.board[r][c]; if(!p) continue;
const isW = p<=6;
score += isW ? VALUE[p] : -VALUE[p];
}
return score;
}
function minimax(gs, depth, alpha, beta, maximizing) {
if(depth===0 || gs.gameOver) {
if(gs.gameOver) {
if(gs.result==='white-wins') return 99999;
if(gs.result==='black-wins') return -99999;
return 0;
}
return evaluate(gs);
}
const color = gs.currentPlayer;
const moves = allLegalMoves(gs, color);
if(!moves.length) return color===WHITE ? -99999 : 99999;
if(color===WHITE) {
let best=-Infinity;
for(const m of moves) {
const child=gs.clone(); applyDirect(child, m);
best = Math.max(best, minimax(child, depth-1, alpha, beta, false));
alpha = Math.max(alpha, best);
if(beta<=alpha) break;
}
return best;
} else {
let best=Infinity;
for(const m of moves) {
const child=gs.clone(); applyDirect(child, m);
best = Math.min(best, minimax(child, depth-1, alpha, beta, true));
beta = Math.min(beta, best);
if(beta<=alpha) break;
}
return best;
}
}
function bestMove(gs) {
const moves = allLegalMoves(gs, BLACK);
if(!moves.length) return null;
let best = null, bestVal = Infinity;
for(const m of moves) {
const child=gs.clone(); applyDirect(child, m);
const val = minimax(child, 2, -Infinity, Infinity, true);
if(val < bestVal) { bestVal=val; best=m; }
}
return best;
}
// ----- UI -----
const boardEl = document.getElementById('board');
const statusEl = document.getElementById('status');
const whiteCapEl = document.getElementById('capturedByWhite');
const blackCapEl = document.getElementById('capturedByBlack');
const promoOverlay = document.getElementById('promotionOverlay');
const promoOpts = document.getElementById('promotionOptions');
const btnUndo = document.getElementById('btnUndo');
const btnNew = document.getElementById('btnNewGame');
let gs = new GameState();
let selected = null;
let legals = [];
let aiThinking = false;
let pendingPromo = null;
function render() {
boardEl.innerHTML = '';
for(let r=0; r<8; r++) {
for(let c=0; c<8; c++) {
const cell = document.createElement('div');
cell.className = 'cell ' + ((r+c)%2===0 ? 'light' : 'dark');
if(selected && selected.row===r && selected.col===c) cell.classList.add('selected');
if(gs.lastFrom && gs.lastFrom.row===r && gs.lastFrom.col===c) cell.classList.add('last-move');
if(gs.lastTo && gs.lastTo.row===r && gs.lastTo.col===c) cell.classList.add('last-move');
if(gs.inCheck) {
const king = findKing(gs, gs.currentPlayer);
if(king && king.row===r && king.col===c) cell.classList.add('in-check');
}
const isTarget = legals.some(m => m.tr===r && m.tc===c);
if(isTarget) {
const m = legals.find(m => m.tr===r && m.tc===c);
const cap = m && (m.captured!==EMPTY || m.special==='ep');
cell.classList.add(cap ? 'legal-capture' : 'legal-move');
}
const piece = gs.piece(r,c);
if(piece) {
const span = document.createElement('span');
span.className = 'piece';
span.textContent = UNICODE[piece];
cell.appendChild(span);
}
cell.addEventListener('click', () => clickCell(r,c));
boardEl.appendChild(cell);
}
}
}
function updateStatus() {
statusEl.classList.remove('thinking','gameover');
if(gs.gameOver) {
statusEl.classList.add('gameover');
statusEl.textContent = gs.result==='white-wins' ? '🏆 白方胜!' : (gs.result==='black-wins' ? '🏆 AI胜!' : '🤝 平局');
} else if(aiThinking) {
statusEl.classList.add('thinking');
statusEl.textContent = '🤔 AI思考中...';
} else {
statusEl.textContent = gs.currentPlayer===WHITE ? (gs.inCheck?'⚠️ 将军!轮到白方':'轮到白方走棋') : '黑方(AI)走棋';
}
}
function updateCaptured() {
const sortArr = arr => [...arr].sort((a,b)=> (VALUE[b]||0)-(VALUE[a]||0));
whiteCapEl.textContent = '白方吃: ' + (gs.capturedByWhite.length ? sortArr(gs.capturedByWhite).map(p=>UNICODE[p]).join(' ') : '---');
blackCapEl.textContent = '黑方吃: ' + (gs.capturedByBlack.length ? sortArr(gs.capturedByBlack).map(p=>UNICODE[p]).join(' ') : '---');
}
function refresh() { render(); updateStatus(); updateCaptured(); btnUndo.disabled = aiThinking || gs.moveHistory.length===0; }
function clickCell(r,c) {
if(aiThinking || gs.gameOver || gs.currentPlayer!==WHITE) return;
if(selected && legals.some(m=>m.tr===r&&m.tc===c)) {
const move = legals.find(m=>m.tr===r&&m.tc===c);
if(move) return executeMove(move);
}
const piece = gs.piece(r,c);
if(piece && PIECE_COLOR(piece)===WHITE) {
selected = {row:r, col:c};
legals = legalMoves(gs, r, c);
if(legals.length===0) { selected=null; legals=[]; } // 无合法走法自动取消
return refresh();
}
selected = null; legals = [];
refresh();
}
function executeMove(m) {
if(m.special==='promo' && !m.promotion) {
pendingPromo = m;
showPromoDialog(m);
return;
}
finalizeMove(m);
}
function finalizeMove(m) {
applyFull(gs, m);
selected = null; legals = [];
refresh();
if(!gs.gameOver) setTimeout(aiTurn, 100);
}
function showPromoDialog(m) {
const isW = PIECE_COLOR(m.piece)===WHITE;
const opts = isW ? [WQ,WR,WB,WN] : [BQ,BR,BB,BN];
promoOpts.innerHTML = '';
opts.forEach(pp => {
const btn = document.createElement('button');
btn.className = 'promotion-btn';
btn.textContent = UNICODE[pp];
btn.onclick = () => { m.promotion = pp; promoOverlay.classList.remove('active'); pendingPromo=null; finalizeMove(m); };
promoOpts.appendChild(btn);
});
promoOverlay.classList.add('active');
}
function aiTurn() {
if(gs.gameOver || gs.currentPlayer!==BLACK) return;
aiThinking = true; refresh();
setTimeout(() => {
const move = bestMove(gs);
if(move) {
if(move.special==='promo' && !move.promotion) move.promotion = BQ;
applyFull(gs, move);
}
aiThinking = false;
selected = null; legals = [];
refresh();
}, 50);
}
btnNew.onclick = () => {
gs.reset(); selected=null; legals=[]; aiThinking=false; pendingPromo=null;
promoOverlay.classList.remove('active'); refresh();
};
btnUndo.onclick = () => {
if(aiThinking) return;
if(gs.currentPlayer===WHITE && gs.moveHistory.length>=2) { undoMove(gs); undoMove(gs); }
else if(gs.currentPlayer===BLACK && gs.moveHistory.length>=1) { undoMove(gs); }
selected=null; legals=[]; refresh();
};
document.addEventListener('keydown', e => {
if(e.key==='Escape') { selected=null; legals=[]; promoOverlay.classList.remove('active'); pendingPromo=null; refresh(); }
});
gs.reset(); refresh();
})();
</script>
</body>
</html>