286 lines
8.7 KiB
TypeScript
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 };
|