
1版(棋子拖尾过于单调):
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>中国象棋</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "KaiTi", "STKaiti", "楷体", serif;
background: #e8e4dc;
background-image:
radial-gradient(circle at 20% 30%, rgba(180,160,130,0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(160,140,110,0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(200,190,170,0.2) 0%, transparent 70%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
color: #2c2c2c;
}
h1 {
font-size: 38px;
margin-bottom: 15px;
letter-spacing: 12px;
color: #1a1a1a;
font-weight: normal;
text-shadow: 1px 1px 0 rgba(0,0,0,0.08);
position: relative;
}
h1::after {
content: '';
display: block;
width: 60%;
height: 2px;
margin: 8px auto 0;
background: linear-gradient(90deg, transparent, #555, transparent);
}
.info {
display: flex;
gap: 30px;
margin-bottom: 20px;
align-items: center;
font-size: 18px;
}
.turn-indicator {
padding: 10px 24px;
border-radius: 4px;
font-weight: bold;
transition: all 0.3s;
border: 1px solid #333;
background: #f5f1ea;
color: #2c2c2c;
letter-spacing: 2px;
}
.turn-red { background: #c0392b; color: #f5f1ea; border-color: #8b2a1e; }
.turn-black { background: #2c2c2c; color: #f5f1ea; border-color: #000; }
button {
background: #f5f1ea;
color: #2c2c2c;
border: 1px solid #333;
padding: 10px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-family: inherit;
font-weight: bold;
letter-spacing: 2px;
transition: all 0.3s;
}
button:hover {
background: #2c2c2c;
color: #f5f1ea;
transform: translateY(-2px);
}
.canvas-wrap {
position: relative;
}
canvas {
background: #f5f1ea;
border: 1px solid #333;
border-radius: 2px;
box-shadow:
0 0 0 8px #f5f1ea,
0 0 0 9px #333,
0 15px 40px rgba(0,0,0,0.3);
cursor: pointer;
}
.status {
margin-top: 18px;
font-size: 20px;
min-height: 28px;
color: #c0392b;
font-weight: bold;
letter-spacing: 3px;
}
.captured {
margin-top: 12px;
display: flex;
gap: 40px;
font-size: 16px;
color: #555;
}
.captured div { min-height: 30px; }
.captured span {
display: inline-block;
margin: 0 3px;
font-size: 22px;
font-family: "KaiTi", "STKaiti", serif;
}
.fx-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
font-family: "KaiTi", "STKaiti", "楷体", serif;
font-weight: bold;
opacity: 0;
z-index: 10;
}
.fx-check {
font-size: 72px;
color: #c0392b;
letter-spacing: 20px;
text-shadow: 0 0 20px rgba(192, 57, 43, 0.6);
}
.fx-mate {
font-size: 96px;
color: #8b0000;
letter-spacing: 30px;
text-shadow: 0 0 30px rgba(139, 0, 0, 0.8), 0 0 60px rgba(192, 57, 43, 0.5);
}
@keyframes fxFlash {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }
30% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
}
@keyframes fxShake {
0%, 100% { transform: translate(0, 0); }
10% { transform: translate(-6px, -4px); }
20% { transform: translate(5px, 3px); }
30% { transform: translate(-4px, 5px); }
40% { transform: translate(6px, -3px); }
50% { transform: translate(-5px, 4px); }
60% { transform: translate(3px, -5px); }
70% { transform: translate(-3px, 3px); }
80% { transform: translate(4px, -2px); }
90% { transform: translate(-2px, 2px); }
}
.shake { animation: fxShake 0.5s ease-in-out; }
</style>
</head>
<body>
<h1>中 國 象 棋</h1>
<div class="info">
<div class="turn-indicator" id="turnIndicator">红方走棋</div>
<button id="restartBtn">重 新 开 始</button>
<button id="undoBtn">悔 棋</button>
</div>
<div class="canvas-wrap" id="canvasWrap">
<canvas id="board" width="540" height="600"></canvas>
<div class="fx-text fx-check" id="fxCheck">将 軍</div>
<div class="fx-text fx-mate" id="fxMate">绝 殺</div>
</div>
<div class="status" id="status"></div>
<div class="captured">
<div>红方被吃 · <span id="capturedByRed"></span></div>
<div>黑方被吃 · <span id="capturedByBlack"></span></div>
</div>
<script>
const CELL = 60;
const MARGIN_X = 30;
const MARGIN_Y = 30;
const COLS = 9;
const ROWS = 10;
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const canvasWrap = document.getElementById('canvasWrap');
const RED = 'red';
const BLACK = 'black';
const PIECES = {
red: {
king: '帥', advisor: '仕', elephant: '相', horse: '傌',
chariot: '俥', cannon: '炮', pawn: '兵'
},
black: {
king: '將', advisor: '士', elephant: '象', horse: '馬',
chariot: '車', cannon: '砲', pawn: '卒'
}
};
let board = [];
let currentPlayer = RED;
let selected = null;
let legalMoves = [];
let gameOver = false;
let history = [];
let capturedByRed = [];
let capturedByBlack = [];
let effects = [];
let animating = false;
let checkAnimStart = 0;
let checkKingPos = null;
let checkmateAnimStart = 0;
function P(type, color) {
return { type, color };
}
function initBoard() {
board = [
[P('chariot',BLACK),P('horse',BLACK),P('elephant',BLACK),P('advisor',BLACK),P('king',BLACK),P('advisor',BLACK),P('elephant',BLACK),P('horse',BLACK),P('chariot',BLACK)],
[null,null,null,null,null,null,null,null,null],
[null,P('cannon',BLACK),null,null,null,null,null,P('cannon',BLACK),null],
[P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK)],
[null,null,null,null,null,null,null,null,null],
[null,null,null,null,null,null,null,null,null],
[P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED)],
[null,P('cannon',RED),null,null,null,null,null,P('cannon',RED),null],
[null,null,null,null,null,null,null,null,null],
[P('chariot',RED),P('horse',RED),P('elephant',RED),P('advisor',RED),P('king',RED),P('advisor',RED),P('elephant',RED),P('horse',RED),P('chariot',RED)]
];
currentPlayer = RED;
selected = null;
legalMoves = [];
gameOver = false;
history = [];
capturedByRed = [];
capturedByBlack = [];
effects = [];
checkAnimStart = 0;
checkKingPos = null;
checkmateAnimStart = 0;
hideFx('fxCheck');
hideFx('fxMate');
updateUI();
draw();
}
function isInBoard(r, c) {
return r >= 0 && r < ROWS && c >= 0 && c < COLS;
}
function inPalace(r, c, color) {
if (c < 3 || c > 5) return false;
if (color === RED) return r >= 7 && r <= 9;
return r >= 0 && r <= 2;
}
function onOwnSide(r, color) {
if (color === RED) return r >= 5;
return r <= 4;
}
function getLegalMoves(r, c) {
const piece = board[r][c];
if (!piece) return [];
const color = piece.color;
const type = piece.type;
const moves = [];
const addMove = (nr, nc) => {
if (!isInBoard(nr, nc)) return;
const target = board[nr][nc];
if (target && target.color === color) return;
moves.push({ r: nr, c: nc, capture: !!target });
};
switch (type) {
case 'king': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (inPalace(nr, nc, color)) addMove(nr, nc);
}
break;
}
case 'advisor': {
const dirs = [[-1,-1],[-1,1],[1,-1],[1,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (inPalace(nr, nc, color)) addMove(nr, nc);
}
break;
}
case 'elephant': {
const dirs = [[-2,-2],[-2,2],[2,-2],[2,2]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
const mr = r + dr/2, mc = c + dc/2;
if (!isInBoard(nr, nc)) continue;
if (!onOwnSide(nr, color)) continue;
if (board[mr][mc]) continue;
addMove(nr, nc);
}
break;
}
case 'horse': {
const hm = [
{dr:-2,dc:-1,br:-1,bc:0}, {dr:-2,dc:1,br:-1,bc:0},
{dr:2,dc:-1,br:1,bc:0}, {dr:2,dc:1,br:1,bc:0},
{dr:-1,dc:-2,br:0,bc:-1}, {dr:1,dc:-2,br:0,bc:-1},
{dr:-1,dc:2,br:0,bc:1}, {dr:1,dc:2,br:0,bc:1}
];
for (const m of hm) {
const nr = r + m.dr, nc = c + m.dc;
const lr = r + m.br, lc = c + m.bc;
if (!isInBoard(nr, nc)) continue;
if (board[lr][lc]) continue;
addMove(nr, nc);
}
break;
}
case 'chariot': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
while (isInBoard(nr, nc)) {
const target = board[nr][nc];
if (!target) {
moves.push({ r: nr, c: nc, capture: false });
} else {
if (target.color !== color) {
moves.push({ r: nr, c: nc, capture: true });
}
break;
}
nr += dr; nc += dc;
}
}
break;
}
case 'cannon': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
let jumped = false;
while (isInBoard(nr, nc)) {
const target = board[nr][nc];
if (!jumped) {
if (!target) {
moves.push({ r: nr, c: nc, capture: false });
} else {
jumped = true;
}
} else {
if (target) {
if (target.color !== color) {
moves.push({ r: nr, c: nc, capture: true });
}
break;
}
}
nr += dr; nc += dc;
}
}
break;
}
case 'pawn': {
const forward = color === RED ? -1 : 1;
const nr = r + forward;
if (isInBoard(nr, c)) addMove(nr, c);
if (!onOwnSide(r, color)) {
if (isInBoard(r, c - 1)) addMove(r, c - 1);
if (isInBoard(r, c + 1)) addMove(r, c + 1);
}
break;
}
}
return moves;
}
function findKing(color) {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].type === 'king' && board[r][c].color === color) return { r, c };
}
}
return null;
}
function kingsFacing() {
const rk = findKing(RED);
const bk = findKing(BLACK);
if (!rk || !bk || rk.c !== bk.c) return false;
const col = rk.c;
const minR = Math.min(rk.r, bk.r);
const maxR = Math.max(rk.r, bk.r);
for (let r = minR + 1; r < maxR; r++) {
if (board[r][col]) return false;
}
return true;
}
function getLegalMovesFiltered(r, c) {
const moves = getLegalMoves(r, c);
return moves.filter(m => {
const captured = board[m.r][m.c];
const moving = board[r][c];
board[m.r][m.c] = moving;
board[r][c] = null;
const inCheck = isKingInCheck(moving.color) || kingsFacing();
board[r][c] = moving;
board[m.r][m.c] = captured;
return !inCheck;
});
}
function isKingInCheck(color) {
const king = findKing(color);
if (!king) return true;
const oppColor = color === RED ? BLACK : RED;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].color === oppColor) {
const moves = getLegalMoves(r, c);
if (moves.some(m => m.r === king.r && m.c === king.c)) return true;
}
}
}
return false;
}
function hasAnyLegalMove(color) {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].color === color) {
if (getLegalMovesFiltered(r, c).length > 0) return true;
}
}
}
return false;
}
function drawBoard() {
const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
grad.addColorStop(0, '#f8f4ec');
grad.addColorStop(0.5, '#f0ebe0');
grad.addColorStop(1, '#ede6d8');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1.5;
for (let r = 0; r < ROWS; r++) {
ctx.beginPath();
ctx.moveTo(MARGIN_X, MARGIN_Y + r * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + r * CELL);
ctx.stroke();
}
for (let c = 0; c < COLS; c++) {
if (c === 0 || c === COLS - 1) {
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + (ROWS - 1) * CELL);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + 4 * CELL);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y + 5 * CELL);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + 9 * CELL);
ctx.stroke();
}
}
ctx.beginPath();
ctx.moveTo(MARGIN_X, MARGIN_Y + 4 * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + 4 * CELL);
ctx.moveTo(MARGIN_X, MARGIN_Y + 5 * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + 5 * CELL);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(MARGIN_X + 3 * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + 5 * CELL, MARGIN_Y + 2 * CELL);
ctx.moveTo(MARGIN_X + 5 * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + 3 * CELL, MARGIN_Y + 2 * CELL);
ctx.moveTo(MARGIN_X + 3 * CELL, MARGIN_Y + 7 * CELL);
ctx.lineTo(MARGIN_X + 5 * CELL, MARGIN_Y + 9 * CELL);
ctx.moveTo(MARGIN_X + 5 * CELL, MARGIN_Y + 7 * CELL);
ctx.lineTo(MARGIN_X + 3 * CELL, MARGIN_Y + 9 * CELL);
ctx.stroke();
ctx.fillStyle = '#1a1a1a';
ctx.font = 'bold 26px "KaiTi", "STKaiti", "楷体", serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('楚 河', MARGIN_X + 1.5 * CELL, MARGIN_Y + 4.5 * CELL);
ctx.fillText('漢 界', MARGIN_X + 6.5 * CELL, MARGIN_Y + 4.5 * CELL);
drawStarPoints();
}
function drawStarPoints() {
const points = [
[2,1],[2,7],[7,1],[7,7],
[3,0],[3,2],[3,4],[3,6],[3,8],
[6,0],[6,2],[6,4],[6,6],[6,8]
];
for (const [r, c] of points) {
drawStar(r, c);
}
}
function drawStar(r, c) {
const x = MARGIN_X + c * CELL;
const y = MARGIN_Y + r * CELL;
const s = 6;
const g = 5;
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1.2;
const dirs = [];
if (c > 0) dirs.push([-1,-1],[-1,1]);
if (c < COLS - 1) dirs.push([1,-1],[1,1]);
for (const [dx, dy] of dirs) {
ctx.beginPath();
ctx.moveTo(x + dx * g, y + dy * g);
ctx.lineTo(x + dx * (g + s), y + dy * g);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + dx * g, y + dy * g);
ctx.lineTo(x + dx * g, y + dy * (g + s));
ctx.stroke();
}
}
function drawPiece(r, c, type, color, highlight) {
const x = MARGIN_X + c * CELL;
const y = MARGIN_Y + r * CELL;
const radius = 26;
if (highlight) {
ctx.beginPath();
ctx.arc(x, y, radius + 5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(192, 57, 43, 0.25)';
ctx.fill();
}
const inkGrad = ctx.createRadialGradient(x - 3, y - 3, 2, x, y, radius);
if (color === RED) {
inkGrad.addColorStop(0, '#e8e0d4');
inkGrad.addColorStop(0.6, '#d4c8b4');
inkGrad.addColorStop(1, '#b8a88c');
} else {
inkGrad.addColorStop(0, '#e8e0d4');
inkGrad.addColorStop(0.6, '#d0c4b0');
inkGrad.addColorStop(1, '#b0a288');
}
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = inkGrad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(x, y, radius - 4, 0, Math.PI * 2);
ctx.strokeStyle = color === RED ? '#c0392b' : '#1a1a1a';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = color === RED ? '#c0392b' : '#1a1a1a';
ctx.font = 'bold 28px "KaiTi", "STKaiti", "楷体", serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(PIECES[color][type], x, y + 1);
}
function draw() {
drawBoard();
for (const move of legalMoves) {
const x = MARGIN_X + move.c * CELL;
const y = MARGIN_Y + move.r * CELL;
if (move.capture) {
ctx.beginPath();
ctx.arc(x, y, 30, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(192, 57, 43, 0.7)';
ctx.lineWidth = 2.5;
ctx.setLineDash([4, 3]);
ctx.stroke();
ctx.setLineDash([]);
} else {
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(26, 26, 26, 0.5)';
ctx.fill();
}
}
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c]) {
const p = board[r][c];
drawPiece(r, c, p.type, p.color, selected && selected.r === r && selected.c === c);
}
}
}
const redKing = findKing(RED);
const blackKing = findKing(BLACK);
const now = performance.now();
if (redKing && isKingInCheck(RED)) {
if (!checkKingPos || checkKingPos.color !== RED || checkKingPos.r !== redKing.r || checkKingPos.c !== redKing.c) {
checkAnimStart = now;
checkKingPos = { r: redKing.r, c: redKing.c, color: RED };
}
const elapsed = now - checkAnimStart;
const pulse = 0.5 + 0.5 * Math.sin(elapsed / 150);
const x = MARGIN_X + redKing.c * CELL;
const y = MARGIN_Y + redKing.r * CELL;
const r = 32 + pulse * 8;
const grad = ctx.createRadialGradient(x, y, 20, x, y, r);
grad.addColorStop(0, `rgba(192, 57, 43, ${0.15 + pulse * 0.25})`);
grad.addColorStop(0.7, `rgba(192, 57, 43, ${0.3 + pulse * 0.3})`);
grad.addColorStop(1, 'rgba(192, 57, 43, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 34 + pulse * 4, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(192, 57, 43, ${0.6 + pulse * 0.4})`;
ctx.lineWidth = 2.5;
ctx.stroke();
} else {
if (checkKingPos && checkKingPos.color === RED) { checkAnimStart = 0; checkKingPos = null; }
}
if (blackKing && isKingInCheck(BLACK)) {
if (!checkKingPos || checkKingPos.color !== BLACK || checkKingPos.r !== blackKing.r || checkKingPos.c !== blackKing.c) {
checkAnimStart = now;
checkKingPos = { r: blackKing.r, c: blackKing.c, color: BLACK };
}
const elapsed = now - checkAnimStart;
const pulse = 0.5 + 0.5 * Math.sin(elapsed / 150);
const x = MARGIN_X + blackKing.c * CELL;
const y = MARGIN_Y + blackKing.r * CELL;
const r = 32 + pulse * 8;
const grad = ctx.createRadialGradient(x, y, 20, x, y, r);
grad.addColorStop(0, `rgba(192, 57, 43, ${0.15 + pulse * 0.25})`);
grad.addColorStop(0.7, `rgba(192, 57, 43, ${0.3 + pulse * 0.3})`);
grad.addColorStop(1, 'rgba(192, 57, 43, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 34 + pulse * 4, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(192, 57, 43, ${0.6 + pulse * 0.4})`;
ctx.lineWidth = 2.5;
ctx.stroke();
} else {
if (checkKingPos && checkKingPos.color === BLACK) { checkAnimStart = 0; checkKingPos = null; }
}
}
function drawEffects() {
const now = performance.now();
const remaining = [];
for (const e of effects) {
const elapsed = now - e.startTime;
const progress = elapsed / e.duration;
if (progress >= 1) continue;
if (e.type === 'capture') {
drawCaptureEffect(e, progress);
remaining.push(e);
} else if (e.type === 'checkmate') {
drawCheckmateEffect(e, progress);
remaining.push(e);
} else if (e.type === 'inkTrail') {
drawInkTrailEffect(e, progress);
remaining.push(e);
}
}
effects = remaining;
}
function drawCaptureEffect(e, p) {
const x = e.x, y = e.y;
const inkColor2 = 'rgba(40, 40, 40,';
for (let i = 0; i < 3; i++) {
const delay = i * 0.08;
const pp = Math.max(0, Math.min(1, (p - delay) / (1 - delay)));
if (pp <= 0) continue;
const radius = 10 + pp * 55;
const alpha = (1 - pp) * 0.8;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = inkColor2 + alpha.toFixed(2) + ')';
ctx.lineWidth = 3 - pp * 2.5;
ctx.stroke();
}
for (let i = 0; i < 14; i++) {
const angle = (i / 14) * Math.PI * 2 + p * 0.8;
const dist = p * 55;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
const size = (1 - p) * 5 + 1;
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor2 + ((1 - p) * 0.7).toFixed(2) + ')';
ctx.fill();
}
const coreGrad = ctx.createRadialGradient(x, y, 0, x, y, 30 * (1 - p * 0.6));
coreGrad.addColorStop(0, inkColor2 + (0.5 * (1 - p)).toFixed(2) + ')');
coreGrad.addColorStop(1, inkColor2 + '0)');
ctx.beginPath();
ctx.arc(x, y, 30 * (1 - p * 0.6), 0, Math.PI * 2);
ctx.fillStyle = coreGrad;
ctx.fill();
}
function drawCheckmateEffect(e, p) {
const x = e.x, y = e.y;
ctx.save();
ctx.globalAlpha = Math.min(1, p * 3);
for (let i = 0; i < 5; i++) {
const delay = i * 0.06;
const pp = Math.max(0, Math.min(1, (p - delay) / (1 - delay)));
if (pp <= 0) continue;
const radius = 15 + pp * 200;
const alpha = (1 - pp) * 0.6;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(30, 30, 30, ${alpha.toFixed(2)})`;
ctx.lineWidth = 4 - pp * 3;
ctx.stroke();
}
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
const dist = Math.min(300, p * 600);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(angle) * dist, y + Math.sin(angle) * dist);
ctx.strokeStyle = `rgba(30, 30, 30, ${(1 - p) * 0.5})`;
ctx.lineWidth = 2;
ctx.stroke();
}
for (let i = 0; i < 30; i++) {
const angle = (i / 30) * Math.PI * 2 + p * 1.5;
const dist = p * 80 + Math.sin(i * 3 + p * 10) * 15;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
const size = (1 - p) * 8 + 2;
const gray = 30 + (i % 3) * 25;
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${gray}, ${gray}, ${gray}, ${(1 - p) * 0.8})`;
ctx.fill();
}
const flashGrad = ctx.createRadialGradient(x, y, 0, x, y, 250 * Math.max(0.3, 1 - p));
flashGrad.addColorStop(0, `rgba(255, 255, 255, ${Math.max(0, (0.6 - p * 0.6))})`);
flashGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0)');
ctx.beginPath();
ctx.arc(x, y, 250, 0, Math.PI * 2);
ctx.fillStyle = flashGrad;
ctx.fill();
ctx.restore();
}
function drawInkTrailEffect(e, p) {
const x = e.sx + (e.dx - e.sx) * p;
const y = e.sy + (e.dy - e.sy) * p;
const inkColor = 'rgba(40, 40, 40,';
for (let i = 0; i < 6; i++) {
const pp = Math.max(0, p - i * 0.04);
const tx = e.sx + (e.dx - e.sx) * pp;
const ty = e.sy + (e.dy - e.sy) * pp;
const alpha = (1 - pp) * 0.4;
const size = (1 - pp) * 18 + 3;
ctx.beginPath();
ctx.arc(tx, ty, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor + alpha.toFixed(2) + ')';
ctx.fill();
}
ctx.beginPath();
ctx.arc(x, y, 28, 0, Math.PI * 2);
ctx.fillStyle = inkColor + '0.6)';
ctx.fill();
}
function animate() {
draw();
drawEffects();
const redKing = findKing(RED);
const blackKing = findKing(BLACK);
const redChecked = redKing && isKingInCheck(RED);
const blackChecked = blackKing && isKingInCheck(BLACK);
if (effects.length > 0 || redChecked || blackChecked) {
animating = true;
requestAnimationFrame(animate);
} else {
animating = false;
}
}
function triggerAnimation() {
if (!animating) {
animating = true;
requestAnimationFrame(animate);
}
}
function getXY(r, c) {
return { x: MARGIN_X + c * CELL, y: MARGIN_Y + r * CELL };
}
function addCaptureEffect(r, c, color) {
const { x, y } = getXY(r, c);
effects.push({
type: 'capture',
x, y,
color,
startTime: performance.now(),
duration: 800
});
}
function addInkTrailEffect(sr, sc, dr, dc, color) {
const { x: sx, y: sy } = getXY(sr, sc);
const { x: dx, y: dy } = getXY(dr, dc);
effects.push({
type: 'inkTrail',
sx, sy, dx, dy,
color,
startTime: performance.now(),
duration: 400
});
}
function addCheckmateEffect(r, c) {
const { x, y } = getXY(r, c);
effects.push({
type: 'checkmate',
x, y,
startTime: performance.now(),
duration: 2000
});
}
function showFx(id, duration) {
const el = document.getElementById(id);
el.style.animation = 'none';
el.offsetHeight;
el.style.animation = `fxFlash ${duration}ms ease-out forwards`;
}
function hideFx(id) {
const el = document.getElementById(id);
el.style.animation = 'none';
el.style.opacity = '0';
}
function triggerShake() {
canvasWrap.classList.remove('shake');
void canvasWrap.offsetWidth;
canvasWrap.classList.add('shake');
}
canvas.addEventListener('click', (e) => {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
const c = Math.round((x - MARGIN_X) / CELL);
const r = Math.round((y - MARGIN_Y) / CELL);
if (!isInBoard(r, c)) return;
const piece = board[r][c];
if (selected) {
const move = legalMoves.find(m => m.r === r && m.c === c);
if (move) {
makeMove(selected.r, selected.c, r, c);
selected = null;
legalMoves = [];
checkGameEnd();
return;
}
if (piece && piece.color === currentPlayer) {
selected = { r, c };
legalMoves = getLegalMovesFiltered(r, c);
draw();
return;
}
selected = null;
legalMoves = [];
draw();
return;
}
if (piece && piece.color === currentPlayer) {
selected = { r, c };
legalMoves = getLegalMovesFiltered(r, c);
draw();
}
});
function makeMove(fr, fc, tr, tc) {
const moved = board[fr][fc];
const captured = board[tr][tc];
history.push({ fr, fc, tr, tc, moved, captured, player: currentPlayer });
addInkTrailEffect(fr, fc, tr, tc, moved.color);
if (captured) {
addCaptureEffect(tr, tc, moved.color);
triggerShake();
}
board[tr][tc] = moved;
board[fr][fc] = null;
if (captured) {
if (currentPlayer === RED) capturedByRed.push(captured.type);
else capturedByBlack.push(captured.type);
}
currentPlayer = currentPlayer === RED ? BLACK : RED;
updateUI();
triggerAnimation();
}
function undo() {
if (history.length === 0) return;
const last = history.pop();
board[last.fr][last.fc] = last.moved;
board[last.tr][last.tc] = last.captured;
if (last.captured) {
if (last.player === RED) capturedByRed.pop();
else capturedByBlack.pop();
}
currentPlayer = last.player;
gameOver = false;
selected = null;
legalMoves = [];
effects = [];
checkAnimStart = 0;
checkKingPos = null;
hideFx('fxCheck');
hideFx('fxMate');
updateUI();
draw();
}
function checkGameEnd() {
const opp = currentPlayer === RED ? BLACK : RED;
const oppKing = findKing(opp);
const statusEl = document.getElementById('status');
if (!oppKing) {
gameOver = true;
const winner = currentPlayer === RED ? '红方' : '黑方';
statusEl.textContent = winner + ' 获胜 · 将军抽帅';
addCheckmateEffect(oppKing?.r ?? 5, oppKing?.c ?? 4);
showFx('fxMate', 2000);
triggerShake();
return;
}
if (!hasAnyLegalMove(currentPlayer)) {
gameOver = true;
const inCheck = isKingInCheck(currentPlayer);
if (inCheck) {
statusEl.textContent = '绝杀 · ' + (opp === RED ? '红方' : '黑方') + ' 获胜';
const king = findKing(currentPlayer);
if (king) {
addCheckmateEffect(king.r, king.c);
}
showFx('fxMate', 2000);
triggerShake();
} else {
statusEl.textContent = '和棋 · 困毙';
}
return;
}
if (isKingInCheck(currentPlayer)) {
statusEl.textContent = '将 军';
showFx('fxCheck', 1500);
} else {
statusEl.textContent = '';
}
}
function updateUI() {
const ind = document.getElementById('turnIndicator');
if (gameOver) {
ind.textContent = '游戏结束';
ind.className = 'turn-indicator';
} else if (currentPlayer === RED) {
ind.textContent = '红方走棋';
ind.className = 'turn-indicator turn-red';
} else {
ind.textContent = '黑方走棋';
ind.className = 'turn-indicator turn-black';
}
document.getElementById('capturedByRed').textContent = capturedByRed.map(p => PIECES.black[p] || '?').join('');
document.getElementById('capturedByBlack').textContent = capturedByBlack.map(p => PIECES.red[p] || '?').join('');
}
document.getElementById('restartBtn').addEventListener('click', initBoard);
document.getElementById('undoBtn').addEventListener('click', undo);
initBoard();
</script>
</body>
</html>
思索再三后决定重写棋子移动:
二版(新增了水墨粒子飞溅的效果):
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>中国象棋 · 水墨特效版</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "KaiTi", "STKaiti", "楷体", serif;
background: #e8e4dc;
background-image:
radial-gradient(circle at 20% 30%, rgba(180,160,130,0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(160,140,110,0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(200,190,170,0.2) 0%, transparent 70%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
color: #2c2c2c;
}
h1 {
font-size: 38px;
margin-bottom: 15px;
letter-spacing: 12px;
color: #1a1a1a;
font-weight: normal;
text-shadow: 1px 1px 0 rgba(0,0,0,0.08);
position: relative;
}
h1::after {
content: '';
display: block;
width: 60%;
height: 2px;
margin: 8px auto 0;
background: linear-gradient(90deg, transparent, #555, transparent);
}
.info {
display: flex;
gap: 30px;
margin-bottom: 20px;
align-items: center;
font-size: 18px;
}
.turn-indicator {
padding: 10px 24px;
border-radius: 4px;
font-weight: bold;
transition: all 0.3s;
border: 1px solid #333;
background: #f5f1ea;
color: #2c2c2c;
letter-spacing: 2px;
}
.turn-red { background: #c0392b; color: #f5f1ea; border-color: #8b2a1e; }
.turn-black { background: #2c2c2c; color: #f5f1ea; border-color: #000; }
button {
background: #f5f1ea;
color: #2c2c2c;
border: 1px solid #333;
padding: 10px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-family: inherit;
font-weight: bold;
letter-spacing: 2px;
transition: all 0.3s;
}
button:hover {
background: #2c2c2c;
color: #f5f1ea;
transform: translateY(-2px);
}
.canvas-wrap {
position: relative;
}
canvas {
background: #f5f1ea;
border: 1px solid #333;
border-radius: 2px;
box-shadow:
0 0 0 8px #f5f1ea,
0 0 0 9px #333,
0 15px 40px rgba(0,0,0,0.3);
cursor: pointer;
}
.status {
margin-top: 18px;
font-size: 20px;
min-height: 28px;
color: #c0392b;
font-weight: bold;
letter-spacing: 3px;
}
.captured {
margin-top: 12px;
display: flex;
gap: 40px;
font-size: 16px;
color: #555;
}
.captured div { min-height: 30px; }
.captured span {
display: inline-block;
margin: 0 3px;
font-size: 22px;
font-family: "KaiTi", "STKaiti", serif;
}
.fx-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
font-family: "KaiTi", "STKaiti", "楷体", serif;
font-weight: bold;
opacity: 0;
z-index: 10;
}
.fx-check {
font-size: 72px;
color: #c0392b;
letter-spacing: 20px;
text-shadow: 0 0 20px rgba(192, 57, 43, 0.6);
}
.fx-mate {
font-size: 96px;
color: #8b0000;
letter-spacing: 30px;
text-shadow: 0 0 30px rgba(139, 0, 0, 0.8), 0 0 60px rgba(192, 57, 43, 0.5);
}
@keyframes fxFlash {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }
30% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
}
@keyframes fxShake {
0%, 100% { transform: translate(0, 0); }
10% { transform: translate(-6px, -4px); }
20% { transform: translate(5px, 3px); }
30% { transform: translate(-4px, 5px); }
40% { transform: translate(6px, -3px); }
50% { transform: translate(-5px, 4px); }
60% { transform: translate(3px, -5px); }
70% { transform: translate(-3px, 3px); }
80% { transform: translate(4px, -2px); }
90% { transform: translate(-2px, 2px); }
}
.shake { animation: fxShake 0.5s ease-in-out; }
</style>
</head>
<body>
<h1>中 國 象 棋</h1>
<div class="info">
<div class="turn-indicator" id="turnIndicator">红方走棋</div>
<button id="restartBtn">重 新 开 始</button>
<button id="undoBtn">悔 棋</button>
</div>
<div class="canvas-wrap" id="canvasWrap">
<canvas id="board" width="540" height="600"></canvas>
<div class="fx-text fx-check" id="fxCheck">将 軍</div>
<div class="fx-text fx-mate" id="fxMate">绝 殺</div>
</div>
<div class="status" id="status"></div>
<div class="captured">
<div>红方被吃 · <span id="capturedByRed"></span></div>
<div>黑方被吃 · <span id="capturedByBlack"></span></div>
</div>
<script>
const CELL = 60;
const MARGIN_X = 30;
const MARGIN_Y = 30;
const COLS = 9;
const ROWS = 10;
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const canvasWrap = document.getElementById('canvasWrap');
const RED = 'red';
const BLACK = 'black';
const PIECES = {
red: {
king: '帥', advisor: '仕', elephant: '相', horse: '傌',
chariot: '俥', cannon: '炮', pawn: '兵'
},
black: {
king: '將', advisor: '士', elephant: '象', horse: '馬',
chariot: '車', cannon: '砲', pawn: '卒'
}
};
let board = [];
let currentPlayer = RED;
let selected = null;
let legalMoves = [];
let gameOver = false;
let history = [];
let capturedByRed = [];
let capturedByBlack = [];
let effects = [];
let animating = false;
let checkAnimStart = 0;
let checkKingPos = null;
let checkmateAnimStart = 0;
function P(type, color) {
return { type, color };
}
function initBoard() {
board = [
[P('chariot',BLACK),P('horse',BLACK),P('elephant',BLACK),P('advisor',BLACK),P('king',BLACK),P('advisor',BLACK),P('elephant',BLACK),P('horse',BLACK),P('chariot',BLACK)],
[null,null,null,null,null,null,null,null,null],
[null,P('cannon',BLACK),null,null,null,null,null,P('cannon',BLACK),null],
[P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK)],
[null,null,null,null,null,null,null,null,null],
[null,null,null,null,null,null,null,null,null],
[P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED)],
[null,P('cannon',RED),null,null,null,null,null,P('cannon',RED),null],
[null,null,null,null,null,null,null,null,null],
[P('chariot',RED),P('horse',RED),P('elephant',RED),P('advisor',RED),P('king',RED),P('advisor',RED),P('elephant',RED),P('horse',RED),P('chariot',RED)]
];
currentPlayer = RED;
selected = null;
legalMoves = [];
gameOver = false;
history = [];
capturedByRed = [];
capturedByBlack = [];
effects = [];
checkAnimStart = 0;
checkKingPos = null;
checkmateAnimStart = 0;
hideFx('fxCheck');
hideFx('fxMate');
updateUI();
draw();
}
function isInBoard(r, c) {
return r >= 0 && r < ROWS && c >= 0 && c < COLS;
}
function inPalace(r, c, color) {
if (c < 3 || c > 5) return false;
if (color === RED) return r >= 7 && r <= 9;
return r >= 0 && r <= 2;
}
function onOwnSide(r, color) {
if (color === RED) return r >= 5;
return r <= 4;
}
function getLegalMoves(r, c) {
const piece = board[r][c];
if (!piece) return [];
const color = piece.color;
const type = piece.type;
const moves = [];
const addMove = (nr, nc) => {
if (!isInBoard(nr, nc)) return;
const target = board[nr][nc];
if (target && target.color === color) return;
moves.push({ r: nr, c: nc, capture: !!target });
};
switch (type) {
case 'king': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (inPalace(nr, nc, color)) addMove(nr, nc);
}
break;
}
case 'advisor': {
const dirs = [[-1,-1],[-1,1],[1,-1],[1,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (inPalace(nr, nc, color)) addMove(nr, nc);
}
break;
}
case 'elephant': {
const dirs = [[-2,-2],[-2,2],[2,-2],[2,2]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
const mr = r + dr/2, mc = c + dc/2;
if (!isInBoard(nr, nc)) continue;
if (!onOwnSide(nr, color)) continue;
if (board[mr][mc]) continue;
addMove(nr, nc);
}
break;
}
case 'horse': {
const hm = [
{dr:-2,dc:-1,br:-1,bc:0}, {dr:-2,dc:1,br:-1,bc:0},
{dr:2,dc:-1,br:1,bc:0}, {dr:2,dc:1,br:1,bc:0},
{dr:-1,dc:-2,br:0,bc:-1}, {dr:1,dc:-2,br:0,bc:-1},
{dr:-1,dc:2,br:0,bc:1}, {dr:1,dc:2,br:0,bc:1}
];
for (const m of hm) {
const nr = r + m.dr, nc = c + m.dc;
const lr = r + m.br, lc = c + m.bc;
if (!isInBoard(nr, nc)) continue;
if (board[lr][lc]) continue;
addMove(nr, nc);
}
break;
}
case 'chariot': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
while (isInBoard(nr, nc)) {
const target = board[nr][nc];
if (!target) {
moves.push({ r: nr, c: nc, capture: false });
} else {
if (target.color !== color) {
moves.push({ r: nr, c: nc, capture: true });
}
break;
}
nr += dr; nc += dc;
}
}
break;
}
case 'cannon': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
let jumped = false;
while (isInBoard(nr, nc)) {
const target = board[nr][nc];
if (!jumped) {
if (!target) {
moves.push({ r: nr, c: nc, capture: false });
} else {
jumped = true;
}
} else {
if (target) {
if (target.color !== color) {
moves.push({ r: nr, c: nc, capture: true });
}
break;
}
}
nr += dr; nc += dc;
}
}
break;
}
case 'pawn': {
const forward = color === RED ? -1 : 1;
const nr = r + forward;
if (isInBoard(nr, c)) addMove(nr, c);
if (!onOwnSide(r, color)) {
if (isInBoard(r, c - 1)) addMove(r, c - 1);
if (isInBoard(r, c + 1)) addMove(r, c + 1);
}
break;
}
}
return moves;
}
function findKing(color) {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].type === 'king' && board[r][c].color === color) return { r, c };
}
}
return null;
}
function kingsFacing() {
const rk = findKing(RED);
const bk = findKing(BLACK);
if (!rk || !bk || rk.c !== bk.c) return false;
const col = rk.c;
const minR = Math.min(rk.r, bk.r);
const maxR = Math.max(rk.r, bk.r);
for (let r = minR + 1; r < maxR; r++) {
if (board[r][col]) return false;
}
return true;
}
function getLegalMovesFiltered(r, c) {
const moves = getLegalMoves(r, c);
return moves.filter(m => {
const captured = board[m.r][m.c];
const moving = board[r][c];
board[m.r][m.c] = moving;
board[r][c] = null;
const inCheck = isKingInCheck(moving.color) || kingsFacing();
board[r][c] = moving;
board[m.r][m.c] = captured;
return !inCheck;
});
}
function isKingInCheck(color) {
const king = findKing(color);
if (!king) return true;
const oppColor = color === RED ? BLACK : RED;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].color === oppColor) {
const moves = getLegalMoves(r, c);
if (moves.some(m => m.r === king.r && m.c === king.c)) return true;
}
}
}
return false;
}
function hasAnyLegalMove(color) {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].color === color) {
if (getLegalMovesFiltered(r, c).length > 0) return true;
}
}
}
return false;
}
function drawBoard() {
const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
grad.addColorStop(0, '#f8f4ec');
grad.addColorStop(0.5, '#f0ebe0');
grad.addColorStop(1, '#ede6d8');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1.5;
for (let r = 0; r < ROWS; r++) {
ctx.beginPath();
ctx.moveTo(MARGIN_X, MARGIN_Y + r * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + r * CELL);
ctx.stroke();
}
for (let c = 0; c < COLS; c++) {
if (c === 0 || c === COLS - 1) {
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + (ROWS - 1) * CELL);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + 4 * CELL);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y + 5 * CELL);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + 9 * CELL);
ctx.stroke();
}
}
ctx.beginPath();
ctx.moveTo(MARGIN_X, MARGIN_Y + 4 * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + 4 * CELL);
ctx.moveTo(MARGIN_X, MARGIN_Y + 5 * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + 5 * CELL);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(MARGIN_X + 3 * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + 5 * CELL, MARGIN_Y + 2 * CELL);
ctx.moveTo(MARGIN_X + 5 * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + 3 * CELL, MARGIN_Y + 2 * CELL);
ctx.moveTo(MARGIN_X + 3 * CELL, MARGIN_Y + 7 * CELL);
ctx.lineTo(MARGIN_X + 5 * CELL, MARGIN_Y + 9 * CELL);
ctx.moveTo(MARGIN_X + 5 * CELL, MARGIN_Y + 7 * CELL);
ctx.lineTo(MARGIN_X + 3 * CELL, MARGIN_Y + 9 * CELL);
ctx.stroke();
ctx.fillStyle = '#1a1a1a';
ctx.font = 'bold 26px "KaiTi", "STKaiti", "楷体", serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('楚 河', MARGIN_X + 1.5 * CELL, MARGIN_Y + 4.5 * CELL);
ctx.fillText('漢 界', MARGIN_X + 6.5 * CELL, MARGIN_Y + 4.5 * CELL);
drawStarPoints();
}
function drawStarPoints() {
const points = [
[2,1],[2,7],[7,1],[7,7],
[3,0],[3,2],[3,4],[3,6],[3,8],
[6,0],[6,2],[6,4],[6,6],[6,8]
];
for (const [r, c] of points) {
drawStar(r, c);
}
}
function drawStar(r, c) {
const x = MARGIN_X + c * CELL;
const y = MARGIN_Y + r * CELL;
const s = 6;
const g = 5;
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1.2;
const dirs = [];
if (c > 0) dirs.push([-1,-1],[-1,1]);
if (c < COLS - 1) dirs.push([1,-1],[1,1]);
for (const [dx, dy] of dirs) {
ctx.beginPath();
ctx.moveTo(x + dx * g, y + dy * g);
ctx.lineTo(x + dx * (g + s), y + dy * g);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + dx * g, y + dy * g);
ctx.lineTo(x + dx * g, y + dy * (g + s));
ctx.stroke();
}
}
function drawPiece(r, c, type, color, highlight) {
const x = MARGIN_X + c * CELL;
const y = MARGIN_Y + r * CELL;
const radius = 26;
if (highlight) {
ctx.beginPath();
ctx.arc(x, y, radius + 5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(192, 57, 43, 0.25)';
ctx.fill();
}
const inkGrad = ctx.createRadialGradient(x - 3, y - 3, 2, x, y, radius);
if (color === RED) {
inkGrad.addColorStop(0, '#e8e0d4');
inkGrad.addColorStop(0.6, '#d4c8b4');
inkGrad.addColorStop(1, '#b8a88c');
} else {
inkGrad.addColorStop(0, '#e8e0d4');
inkGrad.addColorStop(0.6, '#d0c4b0');
inkGrad.addColorStop(1, '#b0a288');
}
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = inkGrad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(x, y, radius - 4, 0, Math.PI * 2);
ctx.strokeStyle = color === RED ? '#c0392b' : '#1a1a1a';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = color === RED ? '#c0392b' : '#1a1a1a';
ctx.font = 'bold 28px "KaiTi", "STKaiti", "楷体", serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(PIECES[color][type], x, y + 1);
}
function draw() {
drawBoard();
for (const move of legalMoves) {
const x = MARGIN_X + move.c * CELL;
const y = MARGIN_Y + move.r * CELL;
if (move.capture) {
ctx.beginPath();
ctx.arc(x, y, 30, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(192, 57, 43, 0.7)';
ctx.lineWidth = 2.5;
ctx.setLineDash([4, 3]);
ctx.stroke();
ctx.setLineDash([]);
} else {
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(26, 26, 26, 0.5)';
ctx.fill();
}
}
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c]) {
const p = board[r][c];
drawPiece(r, c, p.type, p.color, selected && selected.r === r && selected.c === c);
}
}
}
const redKing = findKing(RED);
const blackKing = findKing(BLACK);
const now = performance.now();
if (redKing && isKingInCheck(RED)) {
if (!checkKingPos || checkKingPos.color !== RED || checkKingPos.r !== redKing.r || checkKingPos.c !== redKing.c) {
checkAnimStart = now;
checkKingPos = { r: redKing.r, c: redKing.c, color: RED };
}
const elapsed = now - checkAnimStart;
const pulse = 0.5 + 0.5 * Math.sin(elapsed / 150);
const x = MARGIN_X + redKing.c * CELL;
const y = MARGIN_Y + redKing.r * CELL;
const r = 32 + pulse * 8;
const grad = ctx.createRadialGradient(x, y, 20, x, y, r);
grad.addColorStop(0, `rgba(192, 57, 43, ${0.15 + pulse * 0.25})`);
grad.addColorStop(0.7, `rgba(192, 57, 43, ${0.3 + pulse * 0.3})`);
grad.addColorStop(1, 'rgba(192, 57, 43, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 34 + pulse * 4, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(192, 57, 43, ${0.6 + pulse * 0.4})`;
ctx.lineWidth = 2.5;
ctx.stroke();
} else {
if (checkKingPos && checkKingPos.color === RED) { checkAnimStart = 0; checkKingPos = null; }
}
if (blackKing && isKingInCheck(BLACK)) {
if (!checkKingPos || checkKingPos.color !== BLACK || checkKingPos.r !== blackKing.r || checkKingPos.c !== blackKing.c) {
checkAnimStart = now;
checkKingPos = { r: blackKing.r, c: blackKing.c, color: BLACK };
}
const elapsed = now - checkAnimStart;
const pulse = 0.5 + 0.5 * Math.sin(elapsed / 150);
const x = MARGIN_X + blackKing.c * CELL;
const y = MARGIN_Y + blackKing.r * CELL;
const r = 32 + pulse * 8;
const grad = ctx.createRadialGradient(x, y, 20, x, y, r);
grad.addColorStop(0, `rgba(192, 57, 43, ${0.15 + pulse * 0.25})`);
grad.addColorStop(0.7, `rgba(192, 57, 43, ${0.3 + pulse * 0.3})`);
grad.addColorStop(1, 'rgba(192, 57, 43, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 34 + pulse * 4, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(192, 57, 43, ${0.6 + pulse * 0.4})`;
ctx.lineWidth = 2.5;
ctx.stroke();
} else {
if (checkKingPos && checkKingPos.color === BLACK) { checkAnimStart = 0; checkKingPos = null; }
}
}
function drawEffects() {
const now = performance.now();
const remaining = [];
for (const e of effects) {
const elapsed = now - e.startTime;
const progress = elapsed / e.duration;
if (progress >= 1) continue;
if (e.type === 'capture') {
drawCaptureEffect(e, progress);
remaining.push(e);
} else if (e.type === 'checkmate') {
drawCheckmateEffect(e, progress);
remaining.push(e);
} else if (e.type === 'inkTrail') {
drawInkTrailEffect(e, progress);
remaining.push(e);
}
}
effects = remaining;
}
function drawCaptureEffect(e, p) {
const x = e.x, y = e.y;
const inkColor2 = 'rgba(40, 40, 40,';
for (let i = 0; i < 3; i++) {
const delay = i * 0.08;
const pp = Math.max(0, Math.min(1, (p - delay) / (1 - delay)));
if (pp <= 0) continue;
const radius = 10 + pp * 55;
const alpha = (1 - pp) * 0.8;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = inkColor2 + alpha.toFixed(2) + ')';
ctx.lineWidth = 3 - pp * 2.5;
ctx.stroke();
}
for (let i = 0; i < 14; i++) {
const angle = (i / 14) * Math.PI * 2 + p * 0.8;
const dist = p * 55;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
const size = (1 - p) * 5 + 1;
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor2 + ((1 - p) * 0.7).toFixed(2) + ')';
ctx.fill();
}
const coreGrad = ctx.createRadialGradient(x, y, 0, x, y, 30 * (1 - p * 0.6));
coreGrad.addColorStop(0, inkColor2 + (0.5 * (1 - p)).toFixed(2) + ')');
coreGrad.addColorStop(1, inkColor2 + '0)');
ctx.beginPath();
ctx.arc(x, y, 30 * (1 - p * 0.6), 0, Math.PI * 2);
ctx.fillStyle = coreGrad;
ctx.fill();
}
function drawCheckmateEffect(e, p) {
const x = e.x, y = e.y;
ctx.save();
ctx.globalAlpha = Math.min(1, p * 3);
for (let i = 0; i < 5; i++) {
const delay = i * 0.06;
const pp = Math.max(0, Math.min(1, (p - delay) / (1 - delay)));
if (pp <= 0) continue;
const radius = 15 + pp * 200;
const alpha = (1 - pp) * 0.6;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(30, 30, 30, ${alpha.toFixed(2)})`;
ctx.lineWidth = 4 - pp * 3;
ctx.stroke();
}
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
const dist = Math.min(300, p * 600);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(angle) * dist, y + Math.sin(angle) * dist);
ctx.strokeStyle = `rgba(30, 30, 30, ${(1 - p) * 0.5})`;
ctx.lineWidth = 2;
ctx.stroke();
}
for (let i = 0; i < 30; i++) {
const angle = (i / 30) * Math.PI * 2 + p * 1.5;
const dist = p * 80 + Math.sin(i * 3 + p * 10) * 15;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
const size = (1 - p) * 8 + 2;
const gray = 30 + (i % 3) * 25;
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${gray}, ${gray}, ${gray}, ${(1 - p) * 0.8})`;
ctx.fill();
}
const flashGrad = ctx.createRadialGradient(x, y, 0, x, y, 250 * Math.max(0.3, 1 - p));
flashGrad.addColorStop(0, `rgba(255, 255, 255, ${Math.max(0, (0.6 - p * 0.6))})`);
flashGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0)');
ctx.beginPath();
ctx.arc(x, y, 250, 0, Math.PI * 2);
ctx.fillStyle = flashGrad;
ctx.fill();
ctx.restore();
}
function drawInkTrailEffect(e, p) {
const x = e.sx + (e.dx - e.sx) * p;
const y = e.sy + (e.dy - e.sy) * p;
const inkColor = 'rgba(40, 40, 40,';
const dx = e.dx - e.sx;
const dy = e.dy - e.sy;
const angle = Math.atan2(dy, dx);
const splatters = e.splatters;
for (let i = 0; i < 8; i++) {
const pp = Math.max(0, p - i * 0.035);
const tx = e.sx + dx * pp;
const ty = e.sy + dy * pp;
const alpha = (1 - pp) * 0.5;
const size = (1 - pp) * 20 + 4;
ctx.beginPath();
ctx.arc(tx, ty, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor + alpha.toFixed(2) + ')';
ctx.fill();
}
for (let i = 0; i < 5; i++) {
const pp = Math.max(0, p - i * 0.06);
const tx = e.sx + dx * pp;
const ty = e.sy + dy * pp;
const spread = 8 * (1 - pp);
const perpAngle = angle + Math.PI / 2;
const offset = (Math.sin(i * 1.7 + pp * 5) * spread);
const ox = tx + Math.cos(perpAngle) * offset;
const oy = ty + Math.sin(perpAngle) * offset;
const size = (1 - pp) * 10 + 2;
const alpha = (1 - pp) * 0.35;
ctx.beginPath();
ctx.arc(ox, oy, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor + alpha.toFixed(2) + ')';
ctx.fill();
}
for (let i = 0; i < splatters.length; i++) {
const sp = splatters[i];
const progress = (p - sp.delay) / (1 - sp.delay);
if (progress <= 0 || progress > 1) continue;
const sx = x + Math.cos(sp.angle) * (progress * 35 + 5) + sp.offsetX * progress;
const sy = y + Math.sin(sp.angle) * (progress * 35 + 5) + sp.offsetY * progress;
const size = (1 - progress) * sp.maxSize + sp.minSize;
const alpha = (1 - progress) * sp.maxAlpha;
ctx.beginPath();
ctx.arc(sx, sy, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor + alpha.toFixed(2) + ')';
ctx.fill();
}
for (let i = 0; i < e.clusters.length; i++) {
const cl = e.clusters[i];
const progress = (p - cl.delay) / (1 - cl.delay);
if (progress <= 0 || progress > 1) continue;
const cx = x + Math.cos(cl.angle) * (progress * 40);
const cy = y + Math.sin(cl.angle) * (progress * 40);
const size = (1 - progress) * cl.maxSize + cl.minSize;
const alpha = (1 - progress) * cl.maxAlpha;
ctx.beginPath();
ctx.arc(cx, cy, size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${cl.gray}, ${cl.gray}, ${cl.gray}, ${alpha.toFixed(2)})`;
ctx.fill();
}
const headGrad = ctx.createRadialGradient(x, y, 0, x, y, 30);
headGrad.addColorStop(0, inkColor + '0.7)');
headGrad.addColorStop(0.6, inkColor + '0.4)');
headGrad.addColorStop(1, inkColor + '0)');
ctx.beginPath();
ctx.arc(x, y, 30, 0, Math.PI * 2);
ctx.fillStyle = headGrad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 22, 0, Math.PI * 2);
ctx.fillStyle = inkColor + '0.55)';
ctx.fill();
}
function animate() {
draw();
drawEffects();
const redKing = findKing(RED);
const blackKing = findKing(BLACK);
const redChecked = redKing && isKingInCheck(RED);
const blackChecked = blackKing && isKingInCheck(BLACK);
if (effects.length > 0 || redChecked || blackChecked) {
animating = true;
requestAnimationFrame(animate);
} else {
animating = false;
}
}
function triggerAnimation() {
if (!animating) {
animating = true;
requestAnimationFrame(animate);
}
}
function getXY(r, c) {
return { x: MARGIN_X + c * CELL, y: MARGIN_Y + r * CELL };
}
function addCaptureEffect(r, c, color) {
const { x, y } = getXY(r, c);
effects.push({
type: 'capture',
x, y,
color,
startTime: performance.now(),
duration: 800
});
}
function addInkTrailEffect(sr, sc, dr, dc, color) {
const { x: sx, y: sy } = getXY(sr, sc);
const { x: dx, y: dy } = getXY(dr, dc);
const moveAngle = Math.atan2(dy - sy, dx - sx);
const splatters = [];
for (let i = 0; i < 12; i++) {
const spreadAngle = moveAngle + Math.PI + (Math.random() - 0.5) * 1.8;
splatters.push({
angle: spreadAngle,
delay: i * 0.07,
offsetX: (Math.random() - 0.5) * 10,
offsetY: (Math.random() - 0.5) * 10,
maxSize: 6 + Math.random() * 5,
minSize: 1 + Math.random() * 1.5,
maxAlpha: 0.5 + Math.random() * 0.3
});
}
const clusters = [];
for (let i = 0; i < 4; i++) {
const clusterAngle = moveAngle + Math.PI + (i / 4) * 0.8 - 0.4 + (Math.random() - 0.5) * 0.3;
clusters.push({
angle: clusterAngle,
delay: i * 0.15,
gray: 25 + Math.floor(Math.random() * 30),
maxSize: 4 + Math.random() * 4,
minSize: 0.8 + Math.random() * 1.2,
maxAlpha: 0.4 + Math.random() * 0.3
});
}
effects.push({
type: 'inkTrail',
sx, sy, dx, dy,
color,
splatters,
clusters,
startTime: performance.now(),
duration: 400
});
}
function addCheckmateEffect(r, c) {
const { x, y } = getXY(r, c);
effects.push({
type: 'checkmate',
x, y,
startTime: performance.now(),
duration: 2000
});
}
function showFx(id, duration) {
const el = document.getElementById(id);
el.style.animation = 'none';
el.offsetHeight;
el.style.animation = `fxFlash ${duration}ms ease-out forwards`;
}
function hideFx(id) {
const el = document.getElementById(id);
el.style.animation = 'none';
el.style.opacity = '0';
}
function triggerShake() {
canvasWrap.classList.remove('shake');
void canvasWrap.offsetWidth;
canvasWrap.classList.add('shake');
}
canvas.addEventListener('click', (e) => {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
const c = Math.round((x - MARGIN_X) / CELL);
const r = Math.round((y - MARGIN_Y) / CELL);
if (!isInBoard(r, c)) return;
const piece = board[r][c];
if (selected) {
const move = legalMoves.find(m => m.r === r && m.c === c);
if (move) {
makeMove(selected.r, selected.c, r, c);
selected = null;
legalMoves = [];
checkGameEnd();
return;
}
if (piece && piece.color === currentPlayer) {
selected = { r, c };
legalMoves = getLegalMovesFiltered(r, c);
draw();
return;
}
selected = null;
legalMoves = [];
draw();
return;
}
if (piece && piece.color === currentPlayer) {
selected = { r, c };
legalMoves = getLegalMovesFiltered(r, c);
draw();
}
});
function makeMove(fr, fc, tr, tc) {
const moved = board[fr][fc];
const captured = board[tr][tc];
history.push({ fr, fc, tr, tc, moved, captured, player: currentPlayer });
addInkTrailEffect(fr, fc, tr, tc, moved.color);
if (captured) {
addCaptureEffect(tr, tc, moved.color);
triggerShake();
}
board[tr][tc] = moved;
board[fr][fc] = null;
if (captured) {
if (currentPlayer === RED) capturedByRed.push(captured.type);
else capturedByBlack.push(captured.type);
}
currentPlayer = currentPlayer === RED ? BLACK : RED;
updateUI();
triggerAnimation();
}
function undo() {
if (history.length === 0) return;
const last = history.pop();
board[last.fr][last.fc] = last.moved;
board[last.tr][last.tc] = last.captured;
if (last.captured) {
if (last.player === RED) capturedByRed.pop();
else capturedByBlack.pop();
}
currentPlayer = last.player;
gameOver = false;
selected = null;
legalMoves = [];
effects = [];
checkAnimStart = 0;
checkKingPos = null;
hideFx('fxCheck');
hideFx('fxMate');
updateUI();
draw();
}
function checkGameEnd() {
const opp = currentPlayer === RED ? BLACK : RED;
const oppKing = findKing(opp);
const statusEl = document.getElementById('status');
if (!oppKing) {
gameOver = true;
const winner = currentPlayer === RED ? '红方' : '黑方';
statusEl.textContent = winner + ' 获胜 · 将军抽帅';
addCheckmateEffect(oppKing?.r ?? 5, oppKing?.c ?? 4);
showFx('fxMate', 2000);
triggerShake();
return;
}
if (!hasAnyLegalMove(currentPlayer)) {
gameOver = true;
const inCheck = isKingInCheck(currentPlayer);
if (inCheck) {
statusEl.textContent = '绝杀 · ' + (opp === RED ? '红方' : '黑方') + ' 获胜';
const king = findKing(currentPlayer);
if (king) {
addCheckmateEffect(king.r, king.c);
}
showFx('fxMate', 2000);
triggerShake();
} else {
statusEl.textContent = '和棋 · 困毙';
}
return;
}
if (isKingInCheck(currentPlayer)) {
statusEl.textContent = '将 军';
showFx('fxCheck', 1500);
} else {
statusEl.textContent = '';
}
}
function updateUI() {
const ind = document.getElementById('turnIndicator');
if (gameOver) {
ind.textContent = '游戏结束';
ind.className = 'turn-indicator';
} else if (currentPlayer === RED) {
ind.textContent = '红方走棋';
ind.className = 'turn-indicator turn-red';
} else {
ind.textContent = '黑方走棋';
ind.className = 'turn-indicator turn-black';
}
document.getElementById('capturedByRed').textContent = capturedByRed.map(p => PIECES.black[p] || '?').join('');
document.getElementById('capturedByBlack').textContent = capturedByBlack.map(p => PIECES.red[p] || '?').join('');
}
document.getElementById('restartBtn').addEventListener('click', initBoard);
document.getElementById('undoBtn').addEventListener('click', undo);
initBoard();
</script>
</body>
</html>
随后发现了一个问题,水墨拖尾特效与棋子移动并不匹配:

问题在于棋子是一瞬间移到终点的,而拖尾则是随后缓慢出现的
又写了100多行
这下算是完成了:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>中国象棋 · 水墨特效版</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "KaiTi", "STKaiti", "楷体", serif;
background: #e8e4dc;
background-image:
radial-gradient(circle at 20% 30%, rgba(180,160,130,0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(160,140,110,0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(200,190,170,0.2) 0%, transparent 70%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
color: #2c2c2c;
}
h1 {
font-size: 38px;
margin-bottom: 15px;
letter-spacing: 12px;
color: #1a1a1a;
font-weight: normal;
text-shadow: 1px 1px 0 rgba(0,0,0,0.08);
position: relative;
}
h1::after {
content: '';
display: block;
width: 60%;
height: 2px;
margin: 8px auto 0;
background: linear-gradient(90deg, transparent, #555, transparent);
}
.info {
display: flex;
gap: 30px;
margin-bottom: 20px;
align-items: center;
font-size: 18px;
}
.turn-indicator {
padding: 10px 24px;
border-radius: 4px;
font-weight: bold;
transition: all 0.3s;
border: 1px solid #333;
background: #f5f1ea;
color: #2c2c2c;
letter-spacing: 2px;
}
.turn-red { background: #c0392b; color: #f5f1ea; border-color: #8b2a1e; }
.turn-black { background: #2c2c2c; color: #f5f1ea; border-color: #000; }
button {
background: #f5f1ea;
color: #2c2c2c;
border: 1px solid #333;
padding: 10px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-family: inherit;
font-weight: bold;
letter-spacing: 2px;
transition: all 0.3s;
}
button:hover {
background: #2c2c2c;
color: #f5f1ea;
transform: translateY(-2px);
}
.canvas-wrap {
position: relative;
}
canvas {
background: #f5f1ea;
border: 1px solid #333;
border-radius: 2px;
box-shadow:
0 0 0 8px #f5f1ea,
0 0 0 9px #333,
0 15px 40px rgba(0,0,0,0.3);
cursor: pointer;
}
.status {
margin-top: 18px;
font-size: 20px;
min-height: 28px;
color: #c0392b;
font-weight: bold;
letter-spacing: 3px;
}
.captured {
margin-top: 12px;
display: flex;
gap: 40px;
font-size: 16px;
color: #555;
}
.captured div { min-height: 30px; }
.captured span {
display: inline-block;
margin: 0 3px;
font-size: 22px;
font-family: "KaiTi", "STKaiti", serif;
}
.fx-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
font-family: "KaiTi", "STKaiti", "楷体", serif;
font-weight: bold;
opacity: 0;
z-index: 10;
}
.fx-check {
font-size: 72px;
color: #c0392b;
letter-spacing: 20px;
text-shadow: 0 0 20px rgba(192, 57, 43, 0.6);
}
.fx-mate {
font-size: 96px;
color: #8b0000;
letter-spacing: 30px;
text-shadow: 0 0 30px rgba(139, 0, 0, 0.8), 0 0 60px rgba(192, 57, 43, 0.5);
}
@keyframes fxFlash {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }
30% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
}
@keyframes fxShake {
0%, 100% { transform: translate(0, 0); }
10% { transform: translate(-6px, -4px); }
20% { transform: translate(5px, 3px); }
30% { transform: translate(-4px, 5px); }
40% { transform: translate(6px, -3px); }
50% { transform: translate(-5px, 4px); }
60% { transform: translate(3px, -5px); }
70% { transform: translate(-3px, 3px); }
80% { transform: translate(4px, -2px); }
90% { transform: translate(-2px, 2px); }
}
.shake { animation: fxShake 0.5s ease-in-out; }
</style>
</head>
<body>
<h1>中 國 象 棋</h1>
<div class="info">
<div class="turn-indicator" id="turnIndicator">红方走棋</div>
<button id="restartBtn">重 新 开 始</button>
<button id="undoBtn">悔 棋</button>
</div>
<div class="canvas-wrap" id="canvasWrap">
<canvas id="board" width="540" height="600"></canvas>
<div class="fx-text fx-check" id="fxCheck">将 軍</div>
<div class="fx-text fx-mate" id="fxMate">绝 殺</div>
</div>
<div class="status" id="status"></div>
<div class="captured">
<div>红方被吃 · <span id="capturedByRed"></span></div>
<div>黑方被吃 · <span id="capturedByBlack"></span></div>
</div>
<script>
const CELL = 60;
const MARGIN_X = 30;
const MARGIN_Y = 30;
const COLS = 9;
const ROWS = 10;
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const canvasWrap = document.getElementById('canvasWrap');
const RED = 'red';
const BLACK = 'black';
const PIECES = {
red: {
king: '帥', advisor: '仕', elephant: '相', horse: '傌',
chariot: '俥', cannon: '炮', pawn: '兵'
},
black: {
king: '將', advisor: '士', elephant: '象', horse: '馬',
chariot: '車', cannon: '砲', pawn: '卒'
}
};
let board = [];
let currentPlayer = RED;
let selected = null;
let legalMoves = [];
let gameOver = false;
let history = [];
let capturedByRed = [];
let capturedByBlack = [];
let effects = [];
let animating = false;
let checkAnimStart = 0;
let checkKingPos = null;
let movingPiece = null;
function P(type, color) {
return { type, color };
}
function initBoard() {
board = [
[P('chariot',BLACK),P('horse',BLACK),P('elephant',BLACK),P('advisor',BLACK),P('king',BLACK),P('advisor',BLACK),P('elephant',BLACK),P('horse',BLACK),P('chariot',BLACK)],
[null,null,null,null,null,null,null,null,null],
[null,P('cannon',BLACK),null,null,null,null,null,P('cannon',BLACK),null],
[P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK),null,P('pawn',BLACK)],
[null,null,null,null,null,null,null,null,null],
[null,null,null,null,null,null,null,null,null],
[P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED),null,P('pawn',RED)],
[null,P('cannon',RED),null,null,null,null,null,P('cannon',RED),null],
[null,null,null,null,null,null,null,null,null],
[P('chariot',RED),P('horse',RED),P('elephant',RED),P('advisor',RED),P('king',RED),P('advisor',RED),P('elephant',RED),P('horse',RED),P('chariot',RED)]
];
currentPlayer = RED;
selected = null;
legalMoves = [];
gameOver = false;
history = [];
capturedByRed = [];
capturedByBlack = [];
effects = [];
movingPiece = null;
checkAnimStart = 0;
checkKingPos = null;
hideFx('fxCheck');
hideFx('fxMate');
updateUI();
draw();
}
function isInBoard(r, c) {
return r >= 0 && r < ROWS && c >= 0 && c < COLS;
}
function inPalace(r, c, color) {
if (c < 3 || c > 5) return false;
if (color === RED) return r >= 7 && r <= 9;
return r >= 0 && r <= 2;
}
function onOwnSide(r, color) {
if (color === RED) return r >= 5;
return r <= 4;
}
function getLegalMoves(r, c) {
const piece = board[r][c];
if (!piece) return [];
const color = piece.color;
const type = piece.type;
const moves = [];
const addMove = (nr, nc) => {
if (!isInBoard(nr, nc)) return;
const target = board[nr][nc];
if (target && target.color === color) return;
moves.push({ r: nr, c: nc, capture: !!target });
};
switch (type) {
case 'king': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (inPalace(nr, nc, color)) addMove(nr, nc);
}
break;
}
case 'advisor': {
const dirs = [[-1,-1],[-1,1],[1,-1],[1,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (inPalace(nr, nc, color)) addMove(nr, nc);
}
break;
}
case 'elephant': {
const dirs = [[-2,-2],[-2,2],[2,-2],[2,2]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
const mr = r + dr/2, mc = c + dc/2;
if (!isInBoard(nr, nc)) continue;
if (!onOwnSide(nr, color)) continue;
if (board[mr][mc]) continue;
addMove(nr, nc);
}
break;
}
case 'horse': {
const hm = [
{dr:-2,dc:-1,br:-1,bc:0}, {dr:-2,dc:1,br:-1,bc:0},
{dr:2,dc:-1,br:1,bc:0}, {dr:2,dc:1,br:1,bc:0},
{dr:-1,dc:-2,br:0,bc:-1}, {dr:1,dc:-2,br:0,bc:-1},
{dr:-1,dc:2,br:0,bc:1}, {dr:1,dc:2,br:0,bc:1}
];
for (const m of hm) {
const nr = r + m.dr, nc = c + m.dc;
const lr = r + m.br, lc = c + m.bc;
if (!isInBoard(nr, nc)) continue;
if (board[lr][lc]) continue;
addMove(nr, nc);
}
break;
}
case 'chariot': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
while (isInBoard(nr, nc)) {
const target = board[nr][nc];
if (!target) {
moves.push({ r: nr, c: nc, capture: false });
} else {
if (target.color !== color) {
moves.push({ r: nr, c: nc, capture: true });
}
break;
}
nr += dr; nc += dc;
}
}
break;
}
case 'cannon': {
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
let jumped = false;
while (isInBoard(nr, nc)) {
const target = board[nr][nc];
if (!jumped) {
if (!target) {
moves.push({ r: nr, c: nc, capture: false });
} else {
jumped = true;
}
} else {
if (target) {
if (target.color !== color) {
moves.push({ r: nr, c: nc, capture: true });
}
break;
}
}
nr += dr; nc += dc;
}
}
break;
}
case 'pawn': {
const forward = color === RED ? -1 : 1;
const nr = r + forward;
if (isInBoard(nr, c)) addMove(nr, c);
if (!onOwnSide(r, color)) {
if (isInBoard(r, c - 1)) addMove(r, c - 1);
if (isInBoard(r, c + 1)) addMove(r, c + 1);
}
break;
}
}
return moves;
}
function findKing(color) {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].type === 'king' && board[r][c].color === color) return { r, c };
}
}
return null;
}
function kingsFacing() {
const rk = findKing(RED);
const bk = findKing(BLACK);
if (!rk || !bk || rk.c !== bk.c) return false;
const col = rk.c;
const minR = Math.min(rk.r, bk.r);
const maxR = Math.max(rk.r, bk.r);
for (let r = minR + 1; r < maxR; r++) {
if (board[r][col]) return false;
}
return true;
}
function getLegalMovesFiltered(r, c) {
const moves = getLegalMoves(r, c);
return moves.filter(m => {
const captured = board[m.r][m.c];
const moving = board[r][c];
board[m.r][m.c] = moving;
board[r][c] = null;
const inCheck = isKingInCheck(moving.color) || kingsFacing();
board[r][c] = moving;
board[m.r][m.c] = captured;
return !inCheck;
});
}
function isKingInCheck(color) {
const king = findKing(color);
if (!king) return true;
const oppColor = color === RED ? BLACK : RED;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].color === oppColor) {
const moves = getLegalMoves(r, c);
if (moves.some(m => m.r === king.r && m.c === king.c)) return true;
}
}
}
return false;
}
function hasAnyLegalMove(color) {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && board[r][c].color === color) {
if (getLegalMovesFiltered(r, c).length > 0) return true;
}
}
}
return false;
}
function drawBoard() {
const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
grad.addColorStop(0, '#f8f4ec');
grad.addColorStop(0.5, '#f0ebe0');
grad.addColorStop(1, '#ede6d8');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1.5;
for (let r = 0; r < ROWS; r++) {
ctx.beginPath();
ctx.moveTo(MARGIN_X, MARGIN_Y + r * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + r * CELL);
ctx.stroke();
}
for (let c = 0; c < COLS; c++) {
if (c === 0 || c === COLS - 1) {
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + (ROWS - 1) * CELL);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + 4 * CELL);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(MARGIN_X + c * CELL, MARGIN_Y + 5 * CELL);
ctx.lineTo(MARGIN_X + c * CELL, MARGIN_Y + 9 * CELL);
ctx.stroke();
}
}
ctx.beginPath();
ctx.moveTo(MARGIN_X, MARGIN_Y + 4 * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + 4 * CELL);
ctx.moveTo(MARGIN_X, MARGIN_Y + 5 * CELL);
ctx.lineTo(MARGIN_X + (COLS - 1) * CELL, MARGIN_Y + 5 * CELL);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(MARGIN_X + 3 * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + 5 * CELL, MARGIN_Y + 2 * CELL);
ctx.moveTo(MARGIN_X + 5 * CELL, MARGIN_Y);
ctx.lineTo(MARGIN_X + 3 * CELL, MARGIN_Y + 2 * CELL);
ctx.moveTo(MARGIN_X + 3 * CELL, MARGIN_Y + 7 * CELL);
ctx.lineTo(MARGIN_X + 5 * CELL, MARGIN_Y + 9 * CELL);
ctx.moveTo(MARGIN_X + 5 * CELL, MARGIN_Y + 7 * CELL);
ctx.lineTo(MARGIN_X + 3 * CELL, MARGIN_Y + 9 * CELL);
ctx.stroke();
ctx.fillStyle = '#1a1a1a';
ctx.font = 'bold 26px "KaiTi", "STKaiti", "楷体", serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('楚 河', MARGIN_X + 1.5 * CELL, MARGIN_Y + 4.5 * CELL);
ctx.fillText('漢 界', MARGIN_X + 6.5 * CELL, MARGIN_Y + 4.5 * CELL);
drawStarPoints();
}
function drawStarPoints() {
const points = [
[2,1],[2,7],[7,1],[7,7],
[3,0],[3,2],[3,4],[3,6],[3,8],
[6,0],[6,2],[6,4],[6,6],[6,8]
];
for (const [r, c] of points) {
drawStar(r, c);
}
}
function drawStar(r, c) {
const x = MARGIN_X + c * CELL;
const y = MARGIN_Y + r * CELL;
const s = 6;
const g = 5;
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1.2;
const dirs = [];
if (c > 0) dirs.push([-1,-1],[-1,1]);
if (c < COLS - 1) dirs.push([1,-1],[1,1]);
for (const [dx, dy] of dirs) {
ctx.beginPath();
ctx.moveTo(x + dx * g, y + dy * g);
ctx.lineTo(x + dx * (g + s), y + dy * g);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + dx * g, y + dy * g);
ctx.lineTo(x + dx * g, y + dy * (g + s));
ctx.stroke();
}
}
function drawPiece(r, c, type, color, highlight) {
const x = MARGIN_X + c * CELL;
const y = MARGIN_Y + r * CELL;
drawPieceAtXY(x, y, type, color, highlight, 1);
}
function drawPieceAtXY(x, y, type, color, highlight, scale) {
const radius = 26 * scale;
if (highlight) {
ctx.beginPath();
ctx.arc(x, y, radius + 5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(192, 57, 43, 0.25)';
ctx.fill();
}
const inkGrad = ctx.createRadialGradient(x - 3, y - 3, 2, x, y, radius);
inkGrad.addColorStop(0, '#e8e0d4');
inkGrad.addColorStop(0.6, '#d4c8b4');
inkGrad.addColorStop(1, '#b8a88c');
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = inkGrad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(x, y, radius - 4, 0, Math.PI * 2);
ctx.strokeStyle = color === RED ? '#c0392b' : '#1a1a1a';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = color === RED ? '#c0392b' : '#1a1a1a';
ctx.font = `bold ${28 * scale}px "KaiTi", "STKaiti", "楷体", serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(PIECES[color][type], x, y + 1);
}
function draw() {
drawBoard();
for (const move of legalMoves) {
const x = MARGIN_X + move.c * CELL;
const y = MARGIN_Y + move.r * CELL;
if (move.capture) {
ctx.beginPath();
ctx.arc(x, y, 30, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(192, 57, 43, 0.7)';
ctx.lineWidth = 2.5;
ctx.setLineDash([4, 3]);
ctx.stroke();
ctx.setLineDash([]);
} else {
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(26, 26, 26, 0.5)';
ctx.fill();
}
}
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (board[r][c] && (!movingPiece || (movingPiece.toR !== r || movingPiece.toC !== c) && (movingPiece.fromR !== r || movingPiece.fromC !== c))) {
const p = board[r][c];
drawPiece(r, c, p.type, p.color, selected && selected.r === r && selected.c === c);
}
}
}
if (movingPiece) {
const now = performance.now();
const progress = Math.min(1, (now - movingPiece.startTime) / movingPiece.duration);
const easeProgress = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
if (movingPiece.captured) {
const tcx = MARGIN_X + movingPiece.toC * CELL;
const tcy = MARGIN_Y + movingPiece.toR * CELL;
const capFade = Math.max(0, 1 - progress * 2.5);
if (capFade > 0) {
ctx.save();
ctx.globalAlpha = capFade;
drawPiece(movingPiece.toR, movingPiece.toC, movingPiece.captured.type, movingPiece.captured.color, false);
ctx.restore();
}
}
const sx = MARGIN_X + movingPiece.fromC * CELL;
const sy = MARGIN_Y + movingPiece.fromR * CELL;
const tx = MARGIN_X + movingPiece.toC * CELL;
const ty = MARGIN_Y + movingPiece.toR * CELL;
const ix = sx + (tx - sx) * easeProgress;
const iy = sy + (ty - sy) * easeProgress;
const angle = Math.atan2(ty - sy, tx - sx);
ctx.save();
ctx.translate(ix, iy);
ctx.rotate(angle * 0.15 * (1 - easeProgress));
ctx.translate(-ix, -iy);
drawPieceAtXY(ix, iy, movingPiece.piece.type, movingPiece.piece.color, false, 1 + (1 - easeProgress) * 0.1);
ctx.restore();
if (easeProgress < 0.9) {
const shadowGrad = ctx.createRadialGradient(ix, iy + 20, 0, ix, iy + 20, 25);
shadowGrad.addColorStop(0, 'rgba(0,0,0,0.25)');
shadowGrad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.beginPath();
ctx.arc(ix, iy + 20, 25, 0, Math.PI * 2);
ctx.fillStyle = shadowGrad;
ctx.fill();
}
}
const redKing = findKing(RED);
const blackKing = findKing(BLACK);
const now = performance.now();
if (redKing && isKingInCheck(RED)) {
if (!checkKingPos || checkKingPos.color !== RED || checkKingPos.r !== redKing.r || checkKingPos.c !== redKing.c) {
checkAnimStart = now;
checkKingPos = { r: redKing.r, c: redKing.c, color: RED };
}
const elapsed = now - checkAnimStart;
const pulse = 0.5 + 0.5 * Math.sin(elapsed / 150);
const x = MARGIN_X + redKing.c * CELL;
const y = MARGIN_Y + redKing.r * CELL;
const r = 32 + pulse * 8;
const grad = ctx.createRadialGradient(x, y, 20, x, y, r);
grad.addColorStop(0, `rgba(192, 57, 43, ${0.15 + pulse * 0.25})`);
grad.addColorStop(0.7, `rgba(192, 57, 43, ${0.3 + pulse * 0.3})`);
grad.addColorStop(1, 'rgba(192, 57, 43, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 34 + pulse * 4, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(192, 57, 43, ${0.6 + pulse * 0.4})`;
ctx.lineWidth = 2.5;
ctx.stroke();
} else {
if (checkKingPos && checkKingPos.color === RED) { checkAnimStart = 0; checkKingPos = null; }
}
if (blackKing && isKingInCheck(BLACK)) {
if (!checkKingPos || checkKingPos.color !== BLACK || checkKingPos.r !== blackKing.r || checkKingPos.c !== blackKing.c) {
checkAnimStart = now;
checkKingPos = { r: blackKing.r, c: blackKing.c, color: BLACK };
}
const elapsed = now - checkAnimStart;
const pulse = 0.5 + 0.5 * Math.sin(elapsed / 150);
const x = MARGIN_X + blackKing.c * CELL;
const y = MARGIN_Y + blackKing.r * CELL;
const r = 32 + pulse * 8;
const grad = ctx.createRadialGradient(x, y, 20, x, y, r);
grad.addColorStop(0, `rgba(192, 57, 43, ${0.15 + pulse * 0.25})`);
grad.addColorStop(0.7, `rgba(192, 57, 43, ${0.3 + pulse * 0.3})`);
grad.addColorStop(1, 'rgba(192, 57, 43, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 34 + pulse * 4, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(192, 57, 43, ${0.6 + pulse * 0.4})`;
ctx.lineWidth = 2.5;
ctx.stroke();
} else {
if (checkKingPos && checkKingPos.color === BLACK) { checkAnimStart = 0; checkKingPos = null; }
}
}
function drawEffects() {
const now = performance.now();
const remaining = [];
for (const e of effects) {
const elapsed = now - e.startTime;
const progress = elapsed / e.duration;
if (progress >= 1) continue;
if (e.type === 'capture') {
drawCaptureEffect(e, progress);
remaining.push(e);
} else if (e.type === 'checkmate') {
drawCheckmateEffect(e, progress);
remaining.push(e);
} else if (e.type === 'inkTrail') {
drawInkTrailEffect(e, progress);
remaining.push(e);
}
}
effects = remaining;
}
function drawCaptureEffect(e, p) {
const x = e.x, y = e.y;
const inkColor2 = 'rgba(40, 40, 40,';
for (let i = 0; i < 3; i++) {
const delay = i * 0.08;
const pp = Math.max(0, Math.min(1, (p - delay) / (1 - delay)));
if (pp <= 0) continue;
const radius = 10 + pp * 55;
const alpha = (1 - pp) * 0.8;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = inkColor2 + alpha.toFixed(2) + ')';
ctx.lineWidth = 3 - pp * 2.5;
ctx.stroke();
}
for (let i = 0; i < 14; i++) {
const angle = (i / 14) * Math.PI * 2 + p * 0.8;
const dist = p * 55;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
const size = (1 - p) * 5 + 1;
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor2 + ((1 - p) * 0.7).toFixed(2) + ')';
ctx.fill();
}
const coreGrad = ctx.createRadialGradient(x, y, 0, x, y, 30 * (1 - p * 0.6));
coreGrad.addColorStop(0, inkColor2 + (0.5 * (1 - p)).toFixed(2) + ')');
coreGrad.addColorStop(1, inkColor2 + '0)');
ctx.beginPath();
ctx.arc(x, y, 30 * (1 - p * 0.6), 0, Math.PI * 2);
ctx.fillStyle = coreGrad;
ctx.fill();
}
function drawCheckmateEffect(e, p) {
const x = e.x, y = e.y;
ctx.save();
ctx.globalAlpha = Math.min(1, p * 3);
for (let i = 0; i < 5; i++) {
const delay = i * 0.06;
const pp = Math.max(0, Math.min(1, (p - delay) / (1 - delay)));
if (pp <= 0) continue;
const radius = 15 + pp * 200;
const alpha = (1 - pp) * 0.6;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(30, 30, 30, ${alpha.toFixed(2)})`;
ctx.lineWidth = 4 - pp * 3;
ctx.stroke();
}
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
const dist = Math.min(300, p * 600);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(angle) * dist, y + Math.sin(angle) * dist);
ctx.strokeStyle = `rgba(30, 30, 30, ${(1 - p) * 0.5})`;
ctx.lineWidth = 2;
ctx.stroke();
}
for (let i = 0; i < 30; i++) {
const angle = (i / 30) * Math.PI * 2 + p * 1.5;
const dist = p * 80 + Math.sin(i * 3 + p * 10) * 15;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
const size = (1 - p) * 8 + 2;
const gray = 30 + (i % 3) * 25;
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${gray}, ${gray}, ${gray}, ${(1 - p) * 0.8})`;
ctx.fill();
}
const flashGrad = ctx.createRadialGradient(x, y, 0, x, y, 250 * Math.max(0.3, 1 - p));
flashGrad.addColorStop(0, `rgba(255, 255, 255, ${Math.max(0, (0.6 - p * 0.6))})`);
flashGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0)');
ctx.beginPath();
ctx.arc(x, y, 250, 0, Math.PI * 2);
ctx.fillStyle = flashGrad;
ctx.fill();
ctx.restore();
}
function easeInOut(t) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
function drawInkTrailEffect(e, p) {
const easeP = easeInOut(Math.min(1, p));
const x = e.sx + (e.dx - e.sx) * easeP;
const y = e.sy + (e.dy - e.sy) * easeP;
const inkColor = 'rgba(40, 40, 40,';
const dx = e.dx - e.sx;
const dy = e.dy - e.sy;
const angle = Math.atan2(dy, dx);
const splatters = e.splatters;
for (let i = 0; i < 8; i++) {
const rawP = Math.max(0, p - i * 0.035);
const pp = easeInOut(rawP);
const tx = e.sx + dx * pp;
const ty = e.sy + dy * pp;
const alpha = (1 - rawP) * 0.5;
const size = (1 - rawP) * 20 + 4;
ctx.beginPath();
ctx.arc(tx, ty, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor + alpha.toFixed(2) + ')';
ctx.fill();
}
for (let i = 0; i < 5; i++) {
const rawP = Math.max(0, p - i * 0.06);
const pp = easeInOut(rawP);
const tx = e.sx + dx * pp;
const ty = e.sy + dy * pp;
const spread = 8 * (1 - rawP);
const perpAngle = angle + Math.PI / 2;
const offset = (Math.sin(i * 1.7 + rawP * 5) * spread);
const ox = tx + Math.cos(perpAngle) * offset;
const oy = ty + Math.sin(perpAngle) * offset;
const size = (1 - rawP) * 10 + 2;
const alpha = (1 - rawP) * 0.35;
ctx.beginPath();
ctx.arc(ox, oy, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor + alpha.toFixed(2) + ')';
ctx.fill();
}
for (let i = 0; i < splatters.length; i++) {
const sp = splatters[i];
const progress = (p - sp.delay) / (1 - sp.delay);
if (progress <= 0 || progress > 1) continue;
const sx = x + Math.cos(sp.angle) * (progress * 35 + 5) + sp.offsetX * progress;
const sy = y + Math.sin(sp.angle) * (progress * 35 + 5) + sp.offsetY * progress;
const size = (1 - progress) * sp.maxSize + sp.minSize;
const alpha = (1 - progress) * sp.maxAlpha;
ctx.beginPath();
ctx.arc(sx, sy, size, 0, Math.PI * 2);
ctx.fillStyle = inkColor + alpha.toFixed(2) + ')';
ctx.fill();
}
for (let i = 0; i < e.clusters.length; i++) {
const cl = e.clusters[i];
const progress = (p - cl.delay) / (1 - cl.delay);
if (progress <= 0 || progress > 1) continue;
const cx = x + Math.cos(cl.angle) * (progress * 40);
const cy = y + Math.sin(cl.angle) * (progress * 40);
const size = (1 - progress) * cl.maxSize + cl.minSize;
const alpha = (1 - progress) * cl.maxAlpha;
ctx.beginPath();
ctx.arc(cx, cy, size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${cl.gray}, ${cl.gray}, ${cl.gray}, ${alpha.toFixed(2)})`;
ctx.fill();
}
const headGrad = ctx.createRadialGradient(x, y, 0, x, y, 30);
headGrad.addColorStop(0, inkColor + '0.7)');
headGrad.addColorStop(0.6, inkColor + '0.4)');
headGrad.addColorStop(1, inkColor + '0)');
ctx.beginPath();
ctx.arc(x, y, 30, 0, Math.PI * 2);
ctx.fillStyle = headGrad;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 22, 0, Math.PI * 2);
ctx.fillStyle = inkColor + '0.55)';
ctx.fill();
}
function animate() {
if (movingPiece) {
const now = performance.now();
if (now - movingPiece.startTime >= movingPiece.duration) {
finalizeMove();
}
}
draw();
drawEffects();
if (movingPiece) {
animating = true;
requestAnimationFrame(animate);
return;
}
const redKing = findKing(RED);
const blackKing = findKing(BLACK);
const redChecked = redKing && isKingInCheck(RED);
const blackChecked = blackKing && isKingInCheck(BLACK);
if (effects.length > 0 || redChecked || blackChecked) {
animating = true;
requestAnimationFrame(animate);
} else {
animating = false;
}
}
function triggerAnimation() {
if (!animating) {
animating = true;
requestAnimationFrame(animate);
}
}
function getXY(r, c) {
return { x: MARGIN_X + c * CELL, y: MARGIN_Y + r * CELL };
}
function addCaptureEffect(r, c, color) {
const { x, y } = getXY(r, c);
effects.push({
type: 'capture',
x, y,
color,
startTime: performance.now(),
duration: 800
});
}
function addInkTrailEffect(sr, sc, dr, dc, color, duration) {
const { x: sx, y: sy } = getXY(sr, sc);
const { x: dx, y: dy } = getXY(dr, dc);
const moveAngle = Math.atan2(dy - sy, dx - sx);
const splatters = [];
for (let i = 0; i < 12; i++) {
const spreadAngle = moveAngle + Math.PI + (Math.random() - 0.5) * 1.8;
splatters.push({
angle: spreadAngle,
delay: i * 0.07,
offsetX: (Math.random() - 0.5) * 10,
offsetY: (Math.random() - 0.5) * 10,
maxSize: 6 + Math.random() * 5,
minSize: 1 + Math.random() * 1.5,
maxAlpha: 0.5 + Math.random() * 0.3
});
}
const clusters = [];
for (let i = 0; i < 4; i++) {
const clusterAngle = moveAngle + Math.PI + (i / 4) * 0.8 - 0.4 + (Math.random() - 0.5) * 0.3;
clusters.push({
angle: clusterAngle,
delay: i * 0.15,
gray: 25 + Math.floor(Math.random() * 30),
maxSize: 4 + Math.random() * 4,
minSize: 0.8 + Math.random() * 1.2,
maxAlpha: 0.4 + Math.random() * 0.3
});
}
effects.push({
type: 'inkTrail',
sx, sy, dx, dy,
color,
splatters,
clusters,
startTime: performance.now(),
duration: duration
});
}
function addCheckmateEffect(r, c) {
const { x, y } = getXY(r, c);
effects.push({
type: 'checkmate',
x, y,
startTime: performance.now(),
duration: 2000
});
}
function showFx(id, duration) {
const el = document.getElementById(id);
el.style.animation = 'none';
el.offsetHeight;
el.style.animation = `fxFlash ${duration}ms ease-out forwards`;
}
function hideFx(id) {
const el = document.getElementById(id);
el.style.animation = 'none';
el.style.opacity = '0';
}
function triggerShake() {
canvasWrap.classList.remove('shake');
void canvasWrap.offsetWidth;
canvasWrap.classList.add('shake');
}
canvas.addEventListener('click', (e) => {
if (gameOver || movingPiece) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
const c = Math.round((x - MARGIN_X) / CELL);
const r = Math.round((y - MARGIN_Y) / CELL);
if (!isInBoard(r, c)) return;
const piece = board[r][c];
if (selected) {
const move = legalMoves.find(m => m.r === r && m.c === c);
if (move) {
makeMove(selected.r, selected.c, r, c);
selected = null;
legalMoves = [];
return;
}
if (piece && piece.color === currentPlayer) {
selected = { r, c };
legalMoves = getLegalMovesFiltered(r, c);
draw();
return;
}
selected = null;
legalMoves = [];
draw();
return;
}
if (piece && piece.color === currentPlayer) {
selected = { r, c };
legalMoves = getLegalMovesFiltered(r, c);
draw();
}
});
function makeMove(fr, fc, tr, tc) {
const moved = board[fr][fc];
const captured = board[tr][tc];
history.push({ fr, fc, tr, tc, moved, captured, player: currentPlayer });
board[fr][fc] = null;
board[tr][tc] = null;
const duration = 380;
movingPiece = {
piece: moved,
fromR: fr, fromC: fc,
toR: tr, toC: tc,
captured: captured,
player: currentPlayer,
startTime: performance.now(),
duration: duration
};
addInkTrailEffect(fr, fc, tr, tc, moved.color, duration);
if (captured) {
addCaptureEffect(tr, tc, moved.color);
triggerShake();
}
triggerAnimation();
}
function finalizeMove() {
if (!movingPiece) return;
const { piece, toR, toC, captured, player } = movingPiece;
board[toR][toC] = piece;
movingPiece = null;
if (captured) {
if (player === RED) capturedByRed.push(captured.type);
else capturedByBlack.push(captured.type);
}
currentPlayer = currentPlayer === RED ? BLACK : RED;
updateUI();
checkGameEnd();
}
function undo() {
if (movingPiece) {
board[movingPiece.fromR][movingPiece.fromC] = movingPiece.piece;
board[movingPiece.toR][movingPiece.toC] = movingPiece.captured;
movingPiece = null;
history.pop();
selected = null;
legalMoves = [];
effects = [];
checkAnimStart = 0;
checkKingPos = null;
updateUI();
draw();
return;
}
if (history.length === 0) return;
const last = history.pop();
board[last.fr][last.fc] = last.moved;
board[last.tr][last.tc] = last.captured;
if (last.captured) {
if (last.player === RED) capturedByRed.pop();
else capturedByBlack.pop();
}
currentPlayer = last.player;
gameOver = false;
selected = null;
legalMoves = [];
effects = [];
checkAnimStart = 0;
checkKingPos = null;
hideFx('fxCheck');
hideFx('fxMate');
updateUI();
draw();
}
function checkGameEnd() {
const opp = currentPlayer === RED ? BLACK : RED;
const oppKing = findKing(opp);
const statusEl = document.getElementById('status');
if (!oppKing) {
gameOver = true;
const winner = currentPlayer === RED ? '红方' : '黑方';
statusEl.textContent = winner + ' 获胜 · 将军抽帅';
addCheckmateEffect(oppKing?.r ?? 5, oppKing?.c ?? 4);
showFx('fxMate', 2000);
triggerShake();
return;
}
if (!hasAnyLegalMove(currentPlayer)) {
gameOver = true;
const inCheck = isKingInCheck(currentPlayer);
if (inCheck) {
statusEl.textContent = '绝杀 · ' + (opp === RED ? '红方' : '黑方') + ' 获胜';
const king = findKing(currentPlayer);
if (king) {
addCheckmateEffect(king.r, king.c);
}
showFx('fxMate', 2000);
triggerShake();
} else {
statusEl.textContent = '和棋 · 困毙';
}
return;
}
if (isKingInCheck(currentPlayer)) {
statusEl.textContent = '将 军';
showFx('fxCheck', 1500);
} else {
statusEl.textContent = '';
}
}
function updateUI() {
const ind = document.getElementById('turnIndicator');
if (gameOver) {
ind.textContent = '游戏结束';
ind.className = 'turn-indicator';
} else if (currentPlayer === RED) {
ind.textContent = '红方走棋';
ind.className = 'turn-indicator turn-red';
} else {
ind.textContent = '黑方走棋';
ind.className = 'turn-indicator turn-black';
}
document.getElementById('capturedByRed').textContent = capturedByRed.map(p => PIECES.black[p] || '?').join('');
document.getElementById('capturedByBlack').textContent = capturedByBlack.map(p => PIECES.red[p] || '?').join('');
}
document.getElementById('restartBtn').addEventListener('click', initBoard);
document.getElementById('undoBtn').addEventListener('click', undo);
initBoard();
</script>
</body>
</html>
保存成.txt后缀粘贴后改为.html后缀即可使用。