开发一个能下中国象棋的 Web 组件,需要同时处理:
Gemini 3 在这里展现出巨大价值:
比如“炮的走法 + 障碍判断 + 打炮规则”,一句话即可生成稳定代码。
例如 “使用 Negamax 的 AlphaBeta 并加入剪枝,保持纯函数风格” Gemini 3 会输出与整份组件完全一致的代码风格。

我们采用典型的 React SFC(一个 .jsx 文件) 结构:
export default function Xiangqi() {
const canvasRef = useRef(null);
const [board, setBoard] = useState(initBoard());
const [selected, setSelected] = useState(null);
useEffect(() => { draw(); }, [board]);
const draw = () => {};
const onClick = (e) => {};
const generateMoves = () => {};
const evaluate = () => {};
const alphaBeta = () => {};
return <canvas ref={canvasRef} onClick={onClick} width={540} height={600} />;
}所有棋规、走法生成、AI 搜索全部在这个组件内部。
这使得 demo 易于复制,也利于 Gemini 3 自动协助扩展。
使用 Canvas:
const drawBoard = (ctx) => {
ctx.strokeStyle = "#333";
for (let i = 0; i < 10; i++) {
ctx.beginPath();
ctx.moveTo(0, i * grid);
ctx.lineTo(8 * grid, i * grid);
ctx.stroke();
}
for (let i = 0; i < 9; i++) {
ctx.beginPath();
ctx.moveTo(i * grid, 0);
ctx.lineTo(i * grid, 9 * grid);
ctx.stroke();
}
};绘制棋子:
const drawPiece = (ctx, p, x, y) => {
ctx.beginPath();
ctx.arc(x, y, 22, 0, Math.PI * 2);
ctx.fillStyle = p.side === "r" ? "#fee" : "#eef";
ctx.fill();
ctx.stroke();
ctx.fillStyle = p.side === "r" ? "#b00" : "#008";
ctx.font = "20px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(p.char, x, y);
};每个棋子在自己的方法中生成合法走法,例如:
const horseMoves = (x, y) => {
const moves = [];
const legs = [
{ b: [0, -1], m: [-1, -2] },
{ b: [0, -1], m: [1, -2] },
{ b: [0, 1], m: [-1, 2] },
{ b: [0, 1], m: [1, 2] },
{ b: [-1, 0], m: [-2, -1] },
{ b: [-1, 0], m: [-2, 1] },
{ b: [1, 0], m: [2, -1] },
{ b: [1, 0], m: [2, 1] },
];
legs.forEach(({ b, m }) => {
const [bx, by] = [x + b[0], y + b[1]];
if (board[bx]?.[by]) return; // 蹩马腿
const [nx, ny] = [x + m[0], y + m[1]];
if (isInside(nx, ny) && !sameSide(board[nx][ny], board[x][y])) {
moves.push([x, y, nx, ny]);
}
});
return moves;
};Gemini 3 在这里最常用: 它能按你的组件结构写 rule method,不会写出“独立函数无法访问 board”之类的问题。
为了能在浏览器中轻量运行:
const evaluate = (side) => {
let score = 0;
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 9; j++) {
const p = board[i][j];
if (!p) continue;
const v = value[p.id] + pst[p.id][i][j];
score += p.side === side ? v : -v;
}
}
return score;
};React 组件内部 state 不会在搜索期间更新。
使用 Negamax + AlphaBeta:
const alphaBeta = (depth, alpha, beta, side) => {
if (depth === 0) return evaluate(side);
const moves = generateMoves(side);
for (const mv of moves) {
const snap = applyMove(mv);
const score = -alphaBeta(depth - 1, -beta, -alpha, side === "r" ? "b" : "r");
undoMove(snap);
if (score >= beta) return beta;
if (score > alpha) alpha = score;
}
return alpha;
};React 的单文件组件结构非常适合 Negamax,因为:
例如:
“写一个炮的走法(含打炮规则),按我文件中的方法结构生成。”
Gemini 会:
AlphaBeta + Negamax 极易出 bug:
Gemini 3 写出来的 90% 是可立即运行的。
你不用拆文件,给它整个 JSX, 它能在 正确的位置 修改你的 methods。
import React, { useState, useEffect, useRef } from 'react';
import { RotateCcw, Trophy, Cpu, ArrowRightLeft, Bot } from 'lucide-react';
// ==========================================
// 1. Constants & Config (全局常量)
// ==========================================
// 棋盘尺寸
const BOARD_ROWS = 10;
const BOARD_COLS = 9;
// 坐标系统 (必须在 SVG 和 Game 组件之前定义)
const CELL_SIZE = 50;
const MARGIN = 30;
const BOARD_WIDTH = 8 * CELL_SIZE;
const BOARD_HEIGHT = 9 * CELL_SIZE;
const SVG_WIDTH = BOARD_WIDTH + MARGIN * 2;
const SVG_HEIGHT = BOARD_HEIGHT + MARGIN * 2;
const TYPES = {
GENERAL: 'general', ADVISOR: 'advisor', ELEPHANT: 'elephant',
HORSE: 'horse', CHARIOT: 'chariot', CANNON: 'cannon', SOLDIER: 'soldier'
};
const COLORS = { RED: 'red', BLACK: 'black' };
// UI 显示字符映射
const PIECE_CHARS = {
red: { [TYPES.GENERAL]: '帅', [TYPES.ADVISOR]: '仕', [TYPES.ELEPHANT]: '相', [TYPES.HORSE]: '马', [TYPES.CHARIOT]: '车', [TYPES.CANNON]: '炮', [TYPES.SOLDIER]: '兵' },
black: { [TYPES.GENERAL]: '将', [TYPES.ADVISOR]: '士', [TYPES.ELEPHANT]: '象', [TYPES.HORSE]: '马', [TYPES.CHARIOT]: '车', [TYPES.CANNON]: '炮', [TYPES.SOLDIER]: '卒' }
};
// ==========================================
// 2. Engine Constants (引擎常量)
// ==========================================
const R_KING = 8, R_ADV = 9, R_ELE = 10, R_HORSE = 11, R_ROOK = 12, R_CANNON = 13, R_PAWN = 14;
const B_KING = 16, B_ADV = 17, B_ELE = 18, B_HORSE = 19, B_ROOK = 20, B_CANNON = 21, B_PAWN = 22;
const COLOR_RED = 8;
const COLOR_BLACK = 16;
// 棋子价值表
const VAL = {
0: 0,
[R_KING]: 15000, [R_ADV]: 20, [R_ELE]: 20, [R_HORSE]: 400, [R_ROOK]: 900, [R_CANNON]: 450, [R_PAWN]: 10,
[B_KING]: 15000, [B_ADV]: 20, [B_ELE]: 20, [B_HORSE]: 400, [B_ROOK]: 900, [B_CANNON]: 450, [B_PAWN]: 10
};
// Zobrist Hashing 初始化
const ZOBRIST = new Uint32Array(23 * 90);
const ZOBRIST_SIDE = Math.random() * 0xFFFFFFFF >>> 0;
for(let i=0; i<ZOBRIST.length; i++) ZOBRIST[i] = Math.random() * 0xFFFFFFFF >>> 0;
// 位运算辅助
const SRC = (move) => (move >> 8) & 0xFF;
const DST = (move) => move & 0xFF;
const MOVE = (src, dst) => (src << 8) | dst;
const TT_TYPE = { EXACT: 0, LOWERBOUND: 1, UPPERBOUND: 2 };
// ==========================================
// 3. Engine Logic (AI 核心逻辑)
// ==========================================
class Engine {
constructor() {
this.board = new Int8Array(90);
this.color = COLOR_RED;
this.tt = new Map();
this.killer = new Array(20).fill(0).map(() => [0, 0]);
this.historyHeuristic = new Int32Array(90 * 90);
this.nodes = 0;
this.startTime = 0;
this.timeLimit = 1000;
this.stop = false;
}
loadBoard(uiBoard, turnColor) {
this.board.fill(0);
for(let r=0; r<10; r++) {
for(let c=0; c<9; c++) {
const cell = uiBoard[r][c];
if(cell) {
let p = 0;
if(cell.color === 'red') {
if(cell.type === 'general') p = R_KING;
if(cell.type === 'advisor') p = R_ADV;
if(cell.type === 'elephant') p = R_ELE;
if(cell.type === 'horse') p = R_HORSE;
if(cell.type === 'chariot') p = R_ROOK;
if(cell.type === 'cannon') p = R_CANNON;
if(cell.type === 'soldier') p = R_PAWN;
} else {
if(cell.type === 'general') p = B_KING;
if(cell.type === 'advisor') p = B_ADV;
if(cell.type === 'elephant') p = B_ELE;
if(cell.type === 'horse') p = B_HORSE;
if(cell.type === 'chariot') p = B_ROOK;
if(cell.type === 'cannon') p = B_CANNON;
if(cell.type === 'soldier') p = B_PAWN;
}
this.board[r*9 + c] = p;
}
}
}
this.color = turnColor === 'red' ? COLOR_RED : COLOR_BLACK;
}
getHash() {
let h = 0;
if (this.color === COLOR_BLACK) h ^= ZOBRIST_SIDE;
for(let i=0; i<90; i++) {
if(this.board[i]) h ^= ZOBRIST[this.board[i] * 90 + i];
}
return h >>> 0;
}
generateMoves(onlyCaptures = false) {
const moves = [];
const selfColor = this.color;
const enemyColor = selfColor === COLOR_RED ? COLOR_BLACK : COLOR_RED;
for (let src = 0; src < 90; src++) {
const p = this.board[src];
if (!p || (p & selfColor) === 0) continue;
const type = p;
const r = (src / 9) | 0;
const c = src % 9;
const add = (dst) => {
const target = this.board[dst];
if (target === 0) {
if (!onlyCaptures) moves.push(MOVE(src, dst));
} else if (target & enemyColor) {
moves.push(MOVE(src, dst));
}
};
if (type === R_ROOK || type === B_ROOK || type === R_CANNON || type === B_CANNON) {
const dirs = [-1, 1, -9, 9];
for (let d of dirs) {
let dst = src + d;
let jump = false;
while (dst >= 0 && dst < 90) {
if (Math.abs((dst % 9) - ((dst - d) % 9)) > 1) break;
const target = this.board[dst];
if (type === R_ROOK || type === B_ROOK) {
if (!target) { if(!onlyCaptures) moves.push(MOVE(src, dst)); }
else {
if (target & enemyColor) moves.push(MOVE(src, dst));
break;
}
} else {
if (!target) {
if (!jump && !onlyCaptures) moves.push(MOVE(src, dst));
} else {
if (jump) {
if (target & enemyColor) moves.push(MOVE(src, dst));
break;
}
jump = true;
}
}
dst += d;
}
}
}
else if (type === R_HORSE || type === B_HORSE) {
const horseMoves = [
{d: -19, l: -9, r: r-2, c: c-1}, {d: -17, l: -9, r: r-2, c: c+1},
{d: 17, l: 9, r: r+2, c: c-1}, {d: 19, l: 9, r: r+2, c: c+1},
{d: -11, l: -1, r: r-1, c: c-2}, {d: 7, l: -1, r: r+1, c: c-2},
{d: -7, l: 1, r: r-1, c: c+2}, {d: 11, l: 1, r: r+1, c: c+2}
];
for(let m of horseMoves) {
if (m.r >= 0 && m.r < 10 && m.c >= 0 && m.c < 9) {
if (this.board[src + m.l] === 0) add(src + m.d);
}
}
}
else if (type === R_ELE || type === B_ELE) {
const offsets = [
{d: -20, eye: -10, r: r-2, c: c-2}, {d: -16, eye: -8, r: r-2, c: c+2},
{d: 16, eye: 8, r: r+2, c: c-2}, {d: 20, eye: 10, r: r+2, c: c+2}
];
for(let m of offsets) {
if (m.r >= 0 && m.r < 10 && m.c >= 0 && m.c < 9) {
if (selfColor === COLOR_RED && m.r < 5) continue;
if (selfColor === COLOR_BLACK && m.r > 4) continue;
if (this.board[src + m.eye] === 0) add(src + m.d);
}
}
}
else if (type === R_ADV || type === B_ADV) {
const offsets = [-10, -8, 8, 10];
for(let o of offsets) {
const dst = src + o;
const dr = (dst/9|0), dc = dst%9;
if (dc < 3 || dc > 5) continue;
if (selfColor === COLOR_RED && dr < 7) continue;
if (selfColor === COLOR_BLACK && dr > 2) continue;
add(dst);
}
}
else if (type === R_KING || type === B_KING) {
const offsets = [-9, 9, -1, 1];
for(let o of offsets) {
const dst = src + o;
if (Math.abs((dst % 9) - c) > 1) continue;
const dr = (dst/9|0), dc = dst%9;
if (dc < 3 || dc > 5) continue;
if (selfColor === COLOR_RED && dr < 7) continue;
if (selfColor === COLOR_BLACK && dr > 2) continue;
add(dst);
}
}
else if (type === R_PAWN || type === B_PAWN) {
const forward = selfColor === COLOR_RED ? -9 : 9;
const rDst = r + (selfColor === COLOR_RED ? -1 : 1);
if (rDst >= 0 && rDst < 10) add(src + forward);
const crossed = selfColor === COLOR_RED ? r < 5 : r > 4;
if (crossed) {
if (c > 0) add(src - 1);
if (c < 8) add(src + 1);
}
}
}
return moves;
}
evaluate() {
let score = 0;
for(let i=0; i<90; i++) {
const p = this.board[i];
if(p) {
let val = VAL[p];
const c = i % 9;
if (c === 4) val += 5; // Center control bonus
score += (p & COLOR_RED) ? val : -val;
}
}
return this.color === COLOR_RED ? score : -score;
}
makeMove(move) {
const src = SRC(move);
const dst = DST(move);
const capture = this.board[dst];
const piece = this.board[src];
this.board[dst] = piece;
this.board[src] = 0;
this.color = this.color === COLOR_RED ? COLOR_BLACK : COLOR_RED;
return { move, capture };
}
undoMove(undo) {
const src = SRC(undo.move);
const dst = DST(undo.move);
this.board[src] = this.board[dst];
this.board[dst] = undo.capture;
this.color = this.color === COLOR_RED ? COLOR_BLACK : COLOR_RED;
}
orderMoves(moves, bestMove, ply) {
return moves.map(m => {
let score = 0;
if (m === bestMove) score = 2000000;
else {
const capture = this.board[DST(m)];
if (capture) score = 100000 + VAL[capture] - VAL[this.board[SRC(m)]];
else {
if (this.killer[ply] && (this.killer[ply][0] === m || this.killer[ply][1] === m)) score += 900;
score += this.historyHeuristic[m];
}
}
return { m, score };
}).sort((a, b) => b.score - a.score).map(o => o.m);
}
alphabeta(depth, alpha, beta, ply) {
this.nodes++;
if ((this.nodes & 2047) === 0) {
if (Date.now() - this.startTime > this.timeLimit) this.stop = true;
}
if (this.stop) return 0;
const h = this.getHash();
const ttEntry = this.tt.get(h);
if (ttEntry && ttEntry.depth >= depth) {
if (ttEntry.flag === 0) return ttEntry.score;
if (ttEntry.flag === 1 && ttEntry.score <= alpha) return alpha;
if (ttEntry.flag === 2 && ttEntry.score >= beta) return beta;
}
if (depth <= 0) return this.quiescence(alpha, beta);
let moves = this.generateMoves();
if (moves.length === 0) return -20000 + ply;
const bestMoveStored = ttEntry ? ttEntry.move : null;
moves = this.orderMoves(moves, bestMoveStored, ply);
let bestMove = 0;
let flag = 1;
for (let i = 0; i < moves.length; i++) {
const mv = moves[i];
const undo = this.makeMove(mv);
let score;
if (i === 0) {
score = -this.alphabeta(depth - 1, -beta, -alpha, ply + 1);
} else {
score = -this.alphabeta(depth - 1, -alpha - 1, -alpha, ply + 1);
if (score > alpha && score < beta) {
score = -this.alphabeta(depth - 1, -beta, -alpha, ply + 1);
}
}
this.undoMove(undo);
if (this.stop) return 0;
if (score >= beta) {
if (!undo.capture) {
this.killer[ply][1] = this.killer[ply][0];
this.killer[ply][0] = mv;
this.historyHeuristic[mv] += depth * depth;
}
this.tt.set(h, { score: beta, flag: 2, depth, move: mv });
return beta;
}
if (score > alpha) {
alpha = score;
bestMove = mv;
flag = 0;
}
}
this.tt.set(h, { score: alpha, flag, depth, move: bestMove });
return alpha;
}
quiescence(alpha, beta) {
this.nodes++;
const val = this.evaluate();
if (val >= beta) return beta;
if (val > alpha) alpha = val;
const moves = this.generateMoves(true);
moves.sort((a,b) => {
const va = VAL[this.board[DST(a)]] - VAL[this.board[SRC(a)]];
const vb = VAL[this.board[DST(b)]] - VAL[this.board[SRC(b)]];
return vb - va;
});
for(let mv of moves) {
const undo = this.makeMove(mv);
const score = -this.quiescence(-beta, -alpha);
this.undoMove(undo);
if (score >= beta) return beta;
if (score > alpha) alpha = score;
}
return alpha;
}
search(depth) {
this.stop = false;
this.startTime = Date.now();
this.nodes = 0;
let bestMove = 0;
for(let d=1; d<=depth; d++) {
const score = this.alphabeta(d, -30000, 30000, 0);
if (this.stop) break;
const entry = this.tt.get(this.getHash());
if (entry && entry.move) bestMove = entry.move;
console.log(`Depth ${d}: Score ${score}, Nodes ${this.nodes}`);
}
return bestMove;
}
}
// ==========================================
// 4. UI Helpers (UI 辅助函数)
// ==========================================
const initialBoardSetup = () => {
const board = Array(BOARD_ROWS).fill(null).map(() => Array(BOARD_COLS).fill(null));
const setupRow = (row, color, pieces) => pieces.forEach((type, col) => { if (type) board[row][col] = { type, color }; });
setupRow(0, COLORS.BLACK, [TYPES.CHARIOT, TYPES.HORSE, TYPES.ELEPHANT, TYPES.ADVISOR, TYPES.GENERAL, TYPES.ADVISOR, TYPES.ELEPHANT, TYPES.HORSE, TYPES.CHARIOT]);
setupRow(2, COLORS.BLACK, [null, TYPES.CANNON, null, null, null, null, null, TYPES.CANNON, null]);
setupRow(3, COLORS.BLACK, [TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER]);
setupRow(9, COLORS.RED, [TYPES.CHARIOT, TYPES.HORSE, TYPES.ELEPHANT, TYPES.ADVISOR, TYPES.GENERAL, TYPES.ADVISOR, TYPES.ELEPHANT, TYPES.HORSE, TYPES.CHARIOT]);
setupRow(7, COLORS.RED, [null, TYPES.CANNON, null, null, null, null, null, TYPES.CANNON, null]);
setupRow(6, COLORS.RED, [TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER, null, TYPES.SOLDIER]);
return board;
};
const XiangqiBoardSVG = ({ flipped }) => {
const x = (col) => MARGIN + col * CELL_SIZE;
const y = (row) => MARGIN + row * CELL_SIZE;
const CornerMark = ({ r, c }) => {
const cx = x(c), cy = y(r), len = 10, gap = 4;
const paths = [];
if (c > 0) paths.push(`M ${cx-gap-len} ${cy-gap} L ${cx-gap} ${cy-gap} L ${cx-gap} ${cy-gap-len} M ${cx-gap-len} ${cy+gap} L ${cx-gap} ${cy+gap} L ${cx-gap} ${cy+gap+len}`);
if (c < 8) paths.push(`M ${cx+gap} ${cy-gap-len} L ${cx+gap} ${cy-gap} L ${cx+gap+len} ${cy-gap} M ${cx+gap} ${cy+gap+len} L ${cx+gap} ${cy+gap} L ${cx+gap+len} ${cy+gap}`);
return <path d={paths.join(' ')} stroke="#8B4513" strokeWidth="2" fill="none" />;
};
return (
<svg width="100%" height="100%" viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`} className="absolute top-0 left-0 pointer-events-none select-none">
<rect width={SVG_WIDTH} height={SVG_HEIGHT} fill="#DEB887" />
<rect x={MARGIN - 4} y={MARGIN - 4} width={BOARD_WIDTH + 8} height={BOARD_HEIGHT + 8} stroke="#8B4513" strokeWidth="3" fill="none" />
{Array.from({ length: 10 }).map((_, i) => <line key={`h-${i}`} x1={x(0)} y1={y(i)} x2={x(8)} y2={y(i)} stroke="#8B4513" strokeWidth="1.5" />)}
{Array.from({ length: 9 }).map((_, i) => {
if (i === 0 || i === 8) return <line key={`v-${i}`} x1={x(i)} y1={y(0)} x2={x(i)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" />;
return <g key={`v-${i}`}><line x1={x(i)} y1={y(0)} x2={x(i)} y2={y(4)} stroke="#8B4513" strokeWidth="1.5" /><line x1={x(i)} y1={y(5)} x2={x(i)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" /></g>;
})}
<line x1={x(3)} y1={y(0)} x2={x(5)} y2={y(2)} stroke="#8B4513" strokeWidth="1.5" /><line x1={x(5)} y1={y(0)} x2={x(3)} y2={y(2)} stroke="#8B4513" strokeWidth="1.5" />
<line x1={x(3)} y1={y(7)} x2={x(5)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" /><line x1={x(5)} y1={y(7)} x2={x(3)} y2={y(9)} stroke="#8B4513" strokeWidth="1.5" />
<g transform={flipped ? `rotate(180, ${SVG_WIDTH/2}, ${SVG_HEIGHT/2})` : ""} style={{transformBox:"fill-box",transformOrigin:"center"}}>
<text x={x(2)} y={y(4.5)} dominantBaseline="middle" textAnchor="middle" fill="#8B4513" fontSize="24" fontWeight="bold" style={{fontFamily: "KaiTi"}}>楚 河</text>
<text x={x(6)} y={y(4.5)} dominantBaseline="middle" textAnchor="middle" fill="#8B4513" fontSize="24" fontWeight="bold" style={{fontFamily: "KaiTi"}}>汉 界</text>
</g>
<CornerMark r={2} c={1} /><CornerMark r={2} c={7} /><CornerMark r={7} c={1} /><CornerMark r={7} c={7} />
{[0,2,4,6,8].map(c => <React.Fragment key={c}><CornerMark r={3} c={c}/><CornerMark r={6} c={c}/></React.Fragment>)}
</svg>
);
};
// ==========================================
// 5. React Component (主组件)
// ==========================================
export default function XiangqiGame() {
const [uiBoard, setUiBoard] = useState(initialBoardSetup());
const [turn, setTurn] = useState(COLORS.RED);
const [selected, setSelected] = useState(null);
const [lastMove, setLastMove] = useState(null);
const [winner, setWinner] = useState(null);
const [isAIThinking, setIsAIThinking] = useState(false);
const [aiEnabled, setAiEnabled] = useState(true);
const [demoMode, setDemoMode] = useState(false);
const [playerColor, setPlayerColor] = useState(COLORS.RED);
const engineRef = useRef(new Engine());
// --- Game Loop ---
useEffect(() => {
if (winner) return;
const engine = engineRef.current;
const aiColor = playerColor === COLORS.RED ? COLORS.BLACK : COLORS.RED;
const shouldAIPlay = demoMode || (aiEnabled && turn === aiColor);
if (shouldAIPlay) {
setIsAIThinking(true);
setTimeout(() => {
engine.loadBoard(uiBoard, turn);
engine.timeLimit = 1500;
const bestMoveInt = engine.search(8);
if (bestMoveInt) {
const src = SRC(bestMoveInt);
const dst = DST(bestMoveInt);
const r1 = (src/9)|0, c1 = src%9;
const r2 = (dst/9)|0, c2 = dst%9;
const newBoard = uiBoard.map(r => [...r]);
newBoard[r2][c2] = newBoard[r1][c1];
newBoard[r1][c1] = null;
let kingFound = false;
for(let r=0; r<10; r++) for(let c=0; c<9; c++) {
if (newBoard[r][c] && newBoard[r][c].type === TYPES.GENERAL && newBoard[r][c].color !== turn) kingFound = true;
}
setUiBoard(newBoard);
setLastMove({ from: {r: r1, c: c1}, to: {r: r2, c: c2} });
setTurn(turn === COLORS.RED ? COLORS.BLACK : COLORS.RED);
if (!kingFound) setWinner(turn);
} else {
setWinner(turn === COLORS.RED ? COLORS.BLACK : COLORS.RED);
}
setIsAIThinking(false);
}, 100);
}
}, [turn, aiEnabled, demoMode, winner, playerColor, uiBoard]);
// --- Human Interaction ---
const handleCellClick = (r, c) => {
if (winner || isAIThinking || demoMode) return;
if (aiEnabled && turn !== playerColor) return;
const cell = uiBoard[r][c];
if (selected) {
// Check Move Validity using Engine
engineRef.current.loadBoard(uiBoard, turn);
const srcIdx = selected.r * 9 + selected.c;
const dstIdx = r * 9 + c;
const moves = engineRef.current.generateMoves();
const valid = moves.includes(MOVE(srcIdx, dstIdx));
if (valid) {
const newBoard = uiBoard.map(row => [...row]);
newBoard[r][c] = newBoard[selected.r][selected.c];
newBoard[selected.r][selected.c] = null;
if (uiBoard[r][c] && uiBoard[r][c].type === TYPES.GENERAL) {
setWinner(turn);
}
setUiBoard(newBoard);
setLastMove({ from: selected, to: {r, c} });
setTurn(turn === COLORS.RED ? COLORS.BLACK : COLORS.RED);
setSelected(null);
} else {
if (cell && cell.color === turn) setSelected({r, c});
else setSelected(null);
}
} else {
if (cell && cell.color === turn) setSelected({r, c});
}
};
const isFlipped = playerColor === COLORS.BLACK;
// --- Rendering Helpers ---
const getPosStyle = (r, c) => {
let left = ((MARGIN + c * CELL_SIZE) / SVG_WIDTH) * 100;
let top = ((MARGIN + r * CELL_SIZE) / SVG_HEIGHT) * 100;
if (isFlipped) { left = 100 - left; top = 100 - top; }
return { left: `${left}%`, top: `${top}%`, transform: 'translate(-50%, -50%)' };
};
const restart = () => {
setUiBoard(initialBoardSetup());
setTurn(COLORS.RED);
setWinner(null);
setLastMove(null);
setSelected(null);
setIsAIThinking(false);
setDemoMode(false);
engineRef.current = new Engine();
};
return (
<div className="min-h-screen bg-stone-100 flex flex-col items-center justify-center p-2 font-sans select-none">
{/* HUD */}
<div className="w-full max-w-md mb-4 flex justify-between items-center bg-white p-4 rounded-xl shadow-sm">
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded-full ${turn === COLORS.RED ? 'bg-red-600 animate-pulse' : 'bg-gray-300'}`}></div>
<span className={`font-bold ${turn === COLORS.RED ? 'text-red-600' : 'text-gray-400'}`}>红方</span>
</div>
<div className="font-black text-xl text-stone-700">
{winner ? (winner === COLORS.RED ? '红方胜!' : '黑方胜!') : (isAIThinking ? 'AI思考中...' : '中国象棋')}
</div>
<div className="flex items-center gap-2">
<span className={`font-bold ${turn === COLORS.BLACK ? 'text-black' : 'text-gray-400'}`}>黑方</span>
<div className={`w-4 h-4 rounded-full ${turn === COLORS.BLACK ? 'bg-black animate-pulse' : 'bg-gray-300'}`}></div>
</div>
</div>
{/* Board */}
<div className="relative w-full max-w-[460px] aspect-[460/510] rounded shadow-2xl bg-[#DEB887] overflow-hidden transition-all duration-500">
<XiangqiBoardSVG flipped={isFlipped} />
<div className="absolute top-0 left-0 w-full h-full">
{uiBoard.map((row, r) => row.map((cell, c) => {
const isSel = selected && selected.r === r && selected.c === c;
const isLast = lastMove && ((lastMove.from.r === r && lastMove.from.c === c) || (lastMove.to.r === r && lastMove.to.c === c));
return (
<div key={`${r}-${c}`} onClick={() => handleCellClick(r, c)} style={getPosStyle(r, c)}
className="absolute w-[12%] h-[12%] flex justify-center items-center cursor-pointer z-10">
{isSel && <div className="absolute w-full h-full border-2 border-blue-500 rounded-full animate-pulse"></div>}
{isLast && <div className="absolute w-3 h-3 bg-blue-500/50 rounded-full"></div>}
{cell && (
<div className={`relative w-[90%] h-[90%] rounded-full flex items-center justify-center shadow-md border-2 sm:border-[3px] font-serif font-bold text-lg sm:text-2xl
${cell.color === COLORS.RED ? 'bg-[#FDF5E6] text-[#c00] border-[#c00]' : 'bg-[#FDF5E6] text-[#111] border-[#111]'}
${isSel ? '-translate-y-1 scale-110 shadow-xl' : ''}
`}>
<div className={`absolute w-[82%] h-[82%] rounded-full border ${cell.color === COLORS.RED ? 'border-red-200' : 'border-gray-300'}`}></div>
<span className="relative -mt-1">{PIECE_CHARS[cell.color][cell.type]}</span>
</div>
)}
</div>
)
}))}
</div>
</div>
{/* Controls */}
<div className="mt-6 flex flex-wrap justify-center gap-3">
<button onClick={restart} className="px-4 py-2 bg-stone-700 text-white rounded flex items-center gap-2"><RotateCcw size={16}/> 重开</button>
<button onClick={() => setPlayerColor(c => c === COLORS.RED ? COLORS.BLACK : COLORS.RED)} className="px-4 py-2 bg-orange-600 text-white rounded flex items-center gap-2"><ArrowRightLeft size={16}/> 换边</button>
<button onClick={() => setDemoMode(!demoMode)} className={`px-4 py-2 rounded flex items-center gap-2 ${demoMode ? 'bg-purple-600 text-white' : 'bg-gray-300'}`}><Bot size={16}/> 演示</button>
<button onClick={() => setAiEnabled(!aiEnabled)} className={`px-4 py-2 rounded flex items-center gap-2 ${aiEnabled ? 'bg-blue-600 text-white' : 'bg-gray-300'}`}><Cpu size={16}/> AI: {aiEnabled ? '开' : '关'}</button>
</div>
</div>
);
}在本项目中:
App Store 截图生成器、应用图标生成器 、在线图片压缩、utc timestamp, ctf tool 乖猫记账,AI智能分类的最佳聊天学生必备记账App。 Elasticsearch可视化客户端工具 百度网盘免费加速