Files
nanocode/happy-games/games/chinese-chess/app/components/ChessBoard.tsx
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

286 lines
8.7 KiB
TypeScript

"use client";
import { useRef, useEffect } from "react";
import { Piece, Position } from "../types/chess";
import ChessPiece from "./ChessPiece";
interface ChessBoardProps {
pieces: Piece[];
selectedPieceId?: string;
validMoves: Position[];
lastMove?: { from: Position; to: Position };
onPieceClick: (piece: Piece) => void;
onEmptyClick: (pos: Position) => void;
}
// 棋盘配置
const GRID_SIZE = 55; // 格子大小
const PADDING = 40; // 边距
const BOARD_WIDTH = GRID_SIZE * 8 + PADDING * 2;
const BOARD_HEIGHT = GRID_SIZE * 9 + PADDING * 2;
export default function ChessBoard({
pieces,
selectedPieceId,
validMoves,
lastMove,
onPieceClick,
onEmptyClick,
}: ChessBoardProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
// 绘制棋盘网格
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制背景
ctx.fillStyle = "#fff9e6";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 设置线条样式
ctx.strokeStyle = "#666666";
ctx.lineWidth = 1.2;
// 绘制横线
for (let r = 0; r < 10; r++) {
ctx.beginPath();
ctx.moveTo(PADDING, PADDING + r * GRID_SIZE);
ctx.lineTo(PADDING + 8 * GRID_SIZE, PADDING + r * GRID_SIZE);
ctx.stroke();
}
// 绘制竖线(上半部分)
for (let c = 0; c < 9; c++) {
ctx.beginPath();
ctx.moveTo(PADDING + c * GRID_SIZE, PADDING);
ctx.lineTo(PADDING + c * GRID_SIZE, PADDING + 4 * GRID_SIZE);
ctx.stroke();
}
// 绘制竖线(下半部分)
for (let c = 0; c < 9; c++) {
ctx.beginPath();
ctx.moveTo(PADDING + c * GRID_SIZE, PADDING + 5 * GRID_SIZE);
ctx.lineTo(PADDING + c * GRID_SIZE, PADDING + 9 * GRID_SIZE);
ctx.stroke();
}
// 绘制九宫格斜线(黑方)
ctx.beginPath();
ctx.moveTo(PADDING + 3 * GRID_SIZE, PADDING);
ctx.lineTo(PADDING + 5 * GRID_SIZE, PADDING + 2 * GRID_SIZE);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(PADDING + 5 * GRID_SIZE, PADDING);
ctx.lineTo(PADDING + 3 * GRID_SIZE, PADDING + 2 * GRID_SIZE);
ctx.stroke();
// 绘制九宫格斜线(红方)
ctx.beginPath();
ctx.moveTo(PADDING + 3 * GRID_SIZE, PADDING + 7 * GRID_SIZE);
ctx.lineTo(PADDING + 5 * GRID_SIZE, PADDING + 9 * GRID_SIZE);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(PADDING + 5 * GRID_SIZE, PADDING + 7 * GRID_SIZE);
ctx.lineTo(PADDING + 3 * GRID_SIZE, PADDING + 9 * GRID_SIZE);
ctx.stroke();
// 绘制炮位和兵位标记
drawPositionMarkers(ctx);
// 绘制楚河汉界
ctx.font = "bold 28px serif";
ctx.fillStyle = "#8b7355";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("楚 河", PADDING + 2 * GRID_SIZE, PADDING + 4.5 * GRID_SIZE);
ctx.fillText("汉 界", PADDING + 6 * GRID_SIZE, PADDING + 4.5 * GRID_SIZE);
// 绘制上一步移动标记
if (lastMove) {
drawMoveMarker(ctx, lastMove.from, "#4a90d9");
drawMoveMarker(ctx, lastMove.to, "#4a90d9");
}
// 绘制可移动位置标记
for (const move of validMoves) {
drawValidMoveMarker(ctx, move);
}
}, [validMoves, lastMove]);
// 绘制位置标记(炮位和兵位)
const drawPositionMarkers = (ctx: CanvasRenderingContext2D) => {
ctx.strokeStyle = "#666666";
ctx.lineWidth = 1;
const markerSize = 6;
const positions = [
// 黑方炮位
{ row: 2, col: 1, left: true, right: true, top: false, bottom: true },
{ row: 2, col: 7, left: true, right: true, top: false, bottom: true },
// 黑方兵位
{ row: 3, col: 0, left: false, right: true, top: true, bottom: true },
{ row: 3, col: 2, left: true, right: true, top: true, bottom: true },
{ row: 3, col: 4, left: true, right: true, top: true, bottom: true },
{ row: 3, col: 6, left: true, right: true, top: true, bottom: true },
{ row: 3, col: 8, left: true, right: false, top: true, bottom: true },
// 红方兵位
{ row: 6, col: 0, left: false, right: true, top: true, bottom: true },
{ row: 6, col: 2, left: true, right: true, top: true, bottom: true },
{ row: 6, col: 4, left: true, right: true, top: true, bottom: true },
{ row: 6, col: 6, left: true, right: true, top: true, bottom: true },
{ row: 6, col: 8, left: true, right: false, top: true, bottom: true },
// 红方炮位
{ row: 7, col: 1, left: true, right: true, top: true, bottom: false },
{ row: 7, col: 7, left: true, right: true, top: true, bottom: false },
];
for (const pos of positions) {
const x = PADDING + pos.col * GRID_SIZE;
const y = PADDING + pos.row * GRID_SIZE;
ctx.beginPath();
if (pos.left) {
ctx.moveTo(x - markerSize, y - markerSize / 2);
ctx.lineTo(x - markerSize / 2, y - markerSize / 2);
ctx.lineTo(x - markerSize / 2, y - markerSize);
ctx.moveTo(x - markerSize, y + markerSize / 2);
ctx.lineTo(x - markerSize / 2, y + markerSize / 2);
ctx.lineTo(x - markerSize / 2, y + markerSize);
}
if (pos.right) {
ctx.moveTo(x + markerSize, y - markerSize / 2);
ctx.lineTo(x + markerSize / 2, y - markerSize / 2);
ctx.lineTo(x + markerSize / 2, y - markerSize);
ctx.moveTo(x + markerSize, y + markerSize / 2);
ctx.lineTo(x + markerSize / 2, y + markerSize / 2);
ctx.lineTo(x + markerSize / 2, y + markerSize);
}
ctx.stroke();
}
};
// 绘制移动标记
const drawMoveMarker = (
ctx: CanvasRenderingContext2D,
pos: Position,
color: string
) => {
const x = PADDING + pos.col * GRID_SIZE;
const y = PADDING + pos.row * GRID_SIZE;
const size = 8;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.stroke();
};
// 绘制可移动位置标记
const drawValidMoveMarker = (ctx: CanvasRenderingContext2D, pos: Position) => {
const x = PADDING + pos.col * GRID_SIZE;
const y = PADDING + pos.row * GRID_SIZE;
ctx.fillStyle = "rgba(0, 150, 0, 0.4)";
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
};
// 处理点击事件
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const col = Math.round((x - PADDING) / GRID_SIZE);
const row = Math.round((y - PADDING) / GRID_SIZE);
if (col < 0 || col > 8 || row < 0 || row > 9) return;
const clickedPiece = pieces.find(
(p) => p.alive && p.position.row === row && p.position.col === col
);
if (clickedPiece) {
onPieceClick(clickedPiece);
} else {
onEmptyClick({ row, col });
}
};
return (
<div
style={{
position: "relative",
width: `${BOARD_WIDTH}px`,
height: `${BOARD_HEIGHT}px`,
backgroundColor: "#fff9e6",
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.5)",
border: "2px solid #d4c4a8",
overflow: "hidden",
}}
>
{/* 棋盘画布 */}
<canvas
ref={canvasRef}
width={BOARD_WIDTH}
height={BOARD_HEIGHT}
style={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}}
/>
{/* 棋子层 */}
<div
onClick={handleClick}
style={{
position: "absolute",
top: 0,
left: 0,
width: `${BOARD_WIDTH}px`,
height: `${BOARD_HEIGHT}px`,
cursor: "pointer",
}}
>
{pieces
.filter((p) => p.alive)
.map((piece) => (
<div
key={piece.id}
style={{
position: "absolute",
left: `${PADDING + piece.position.col * GRID_SIZE - 22}px`,
top: `${PADDING + piece.position.row * GRID_SIZE - 22}px`,
}}
>
<ChessPiece
piece={piece}
isSelected={piece.id === selectedPieceId}
onClick={() => onPieceClick(piece)}
size={44}
/>
</div>
))}
</div>
</div>
);
}
export { GRID_SIZE, PADDING, BOARD_WIDTH, BOARD_HEIGHT };