我正在尝试构建一个 React Web 应用程序,让人们能够解决国际象棋难题。我使用 chessground 库来渲染棋盘,并使用 chess.js 库来处理国际象棋逻辑。
我创建了一个组件 Board.tsx 来在我的 React 项目中使用 chessground 库。这是它的代码:
import React from 'react';
import { Chessground } from 'chessground';
import { Config } from 'chessground/config';
import { Chess } from 'chess.js';
import './Board.css';
import * as cg from 'chessground/types';
interface BoardProperties {
fen: cg.FEN; // chess position in Forsyth notation
orientation: cg.Color; // board orientation: 'white' | 'black'
game?: Chess;
turnColor?: cg.Color;
coordinates?: boolean;
viewOnly?: boolean;
disableContextMenu?: boolean;
highlight?: {
lastMove?: boolean;
check?: boolean;
};
animation?: {
enabled?: boolean;
duration?: number;
};
draggable?: {
enabled?: boolean;
distance?: number;
autoDistance?: boolean;
showGhost?: boolean;
deleteOnDropOff?: boolean;
};
events?: {
change?: () => void;
move?: (orig: cg.Key, dest: cg.Key, capturedPiece?: cg.Piece) => void;
dropNewPiece?: (piece: cg.Piece, key: cg.Key) => void;
select?: (key: cg.Key) => void;
insert?: (elements: cg.Elements) => void;
};
}
class Board extends React.Component<BoardProperties> {
private boardRef: React.RefObject<HTMLDivElement>;
private groundInstance: ReturnType<typeof Chessground> | null;
constructor(props: BoardProperties) {
super(props);
this.boardRef = React.createRef();
this.groundInstance = null;
}
// Get allowed moves
getLegalMoves() {
const dests = new Map();
const { game } = this.props;
if (game) {
let allLegalMoves = game.moves({ verbose: true });
allLegalMoves.forEach((move) => {
if (!dests.has(move.from)) {
dests.set(move.from, []);
}
dests.get(move.from).push(move.to);
});
}
return dests;
}
// Make a move
makeMove(from: cg.Key, to: cg.Key) {
if (this.groundInstance) {
this.groundInstance.move(from, to);
console.log(`Move executed: ${from} -> ${to}`);
}
}
componentDidMount() {
if (this.boardRef.current) {
const config: Config = {
fen: this.props.fen,
orientation: this.props.orientation,
coordinates: this.props.coordinates ?? true,
turnColor: this.props.turnColor || 'white',
viewOnly: this.props.viewOnly || false,
disableContextMenu: this.props.disableContextMenu || true,
animation: this.props.animation || { enabled: true, duration: 500 },
movable: {
color: 'both',
free: false,
dests: this.getLegalMoves(),
showDests: true,
},
highlight: this.props.highlight || {},
events: this.props.events || {},
};
this.groundInstance = Chessground(this.boardRef.current, config);
}
}
componentDidUpdate(prevProps: BoardProperties) {
if (prevProps.fen !== this.props.fen) {
if (this.groundInstance) {
this.groundInstance.set({ fen: this.props.fen, movable: {
color: 'both',
free: false,
dests: this.getLegalMoves(),
showDests: true,
},});
}
}
}
componentWillUnmount() {
if (this.groundInstance) {
this.groundInstance.destroy();
}
}
render() {
return (
<div>
<div ref={this.boardRef} style={{ width: '400px', height: '400px' }}></div>
</div>
);
}
}
export default Board;
我还创建了一个 React 组件 PuzzlesPage.js 来实现我的想法。在解释它的工作原理之前,我将先展示它的代码:
import Board from "./board/Board.tsx";
import { useEffect, useState, useRef } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Chess } from 'chess.js';
const DEFAULT_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
export default function PuzzlesPage() {
const [fen, setFen] = useState(DEFAULT_FEN);
const [boardOrientation, setBoardOrientation] = useState('white');
const [correctMoves, setCorrectMoves] = useState([]);
const [game, setGame] = useState(new Chess(fen));
const [currentMoveIndex, setCurrentMoveIndex] = useState(0);
const [isUserTurn, setIsUserTurn] = useState(false);
const [isComputerMove, setIsComputerMove] = useState(false);
const boardRef = useRef(null);
const makeAIMove = () => {
console.log('makeAIMove called');
console.log('currentMoveIndex:', currentMoveIndex);
console.log('correctMoves:', correctMoves);
if (currentMoveIndex < correctMoves.length) {
const move = correctMoves[currentMoveIndex];
console.log('Computer move:', move);
const [orig, dest] = [move.slice(0, 2), move.slice(2, 4)];
if (game.move({ from: orig, to: dest })) {
console.log('Computer move is valid');
setFen(game.fen());
boardRef.current.makeMove(orig, dest);
setCurrentMoveIndex((prevIndex) => prevIndex + 1);
setIsUserTurn(true);
setIsComputerMove(false);
console.log('User turn set to true, computer move executed');
} else {
console.log('Invalid computer move');
}
} else {
console.log('currentMoveIndex ', currentMoveIndex, ' is >= correctMoves.length ', correctMoves.length);
}
};
const handleUserMove = (orig, dest) => {
console.log('User move:', orig, dest);
console.log('isUserTurn:', isUserTurn);
console.log('isComputerMove:', isComputerMove);
if (!isUserTurn || currentMoveIndex >= correctMoves.length) {
console.log('Not user\'s turn or moves are done');
return;
}
const correctMove = correctMoves[currentMoveIndex];
console.log('Expected user move:', correctMove);
if (orig + dest === correctMove) {
console.log('User move is correct');
if (game.move({ from: orig, to: dest })) {
setFen(game.fen());
setCurrentMoveIndex((prevIndex) => prevIndex + 1);
setIsUserTurn(false);
setIsComputerMove(true);
console.log('User turn set to false, computer turn will follow');
if (currentMoveIndex + 1 < correctMoves.length) {
console.log('Preparing for next computer move');
setTimeout(() => makeAIMove(), 500);
} else {
console.log('Puzzle solved');
toast.success('Puzzle solved!');
}
}
} else {
console.log('User move is incorrect');
toast.error('User move is incorrect');
boardRef.current.groundInstance.set({ fen: game.fen() });
}
};
function getRandomPuzzle() {
console.log('Fetching new puzzle');
fetch('http://localhost:5000/api/random-puzzle')
.then((response) => response.json())
.then((data) => {
console.log('Puzzle received:', data);
setFen(data.fen);
if (data.moves && data.moves.trim()) {
const moves = data.moves.split(' ').filter(move => move);
console.log('Parsed moves:', moves);
setCorrectMoves(moves);
} else {
console.error('No valid moves found in the response');
setCorrectMoves([]);
}
setGame(new Chess(data.fen));
setBoardOrientation(data.fen.split(' ')[1] === 'b' ? 'white' : 'black');
setCurrentMoveIndex(0);
setIsUserTurn(false);
setIsComputerMove(false);
})
.catch((error) => {
console.error('Error fetching puzzle:', error);
});
}
useEffect(() => {
if (correctMoves.length > 0) {
console.log('First computer move after puzzle load');
setTimeout(() => makeAIMove(), 500);
}
}, [correctMoves]);
return (
<div>
<Board
ref={boardRef}
fen={fen}
game={game}
orientation={boardOrientation}
events={{
move: (orig, dest) => {
console.log('Move event triggered:', orig, dest);
console.log('isUserTurn before handling:', isUserTurn);
console.log('isComputerMove before handling:', isComputerMove);
if (isComputerMove) {
console.log('Ignoring computer move event:', orig, dest);
setIsComputerMove(false);
return;
}
handleUserMove(orig, dest);
},
}}
/>
<button
onClick={getRandomPuzzle}
style={{ marginTop: '20px', padding: '10px 20px', fontSize: '16px' }}
>
New puzzle
</button>
<ToastContainer />
</div>
);
}
工作原理:
当用户按下按钮时,将从服务器加载一个新的谜题。这是服务器响应的示例:
{
"id": 3684723,
"puzzle_id": "ZgWE3",
"fen": "8/5pk1/8/3rq2p/6p1/6P1/1pQ2PP1/1R4K1 w - - 0 43",
"moves": "b1b2 e5e1 g1h2 d5d1 c2d1 e1d1",
"rating": 1681,
"rating_deviation": 84,
"popularity": 79,
"nb_plays": 39,
"themes": "crushing endgame long quietMove",
"game_url": "https://lichess.org/5kJUutVK#85",
"opening_tags": null
}
然后它将“移动”转换为移动数组,并在创建
useEffect
数组后在 makeAIMove()
中调用 correctMoves
。当 makeAIMove()
被调用时,如果从 correctMoves
开始第一步移动并开始等待用户的移动。当用户进行移动时,它会检查此移动是否在计算机在 correctMoves
中的 handleUserMove()
移动之后进行,如果是,则再次调用 makeAIMove()
等等,直到计算机和用户从 correctMoves
开始进行所有移动。
问题:
计算机通过
boardRef.current.makeMove(orig, dest);
做出的动作和用户自己做出的动作都会调用Board组件中的events.move事件。仅当用户移动以获取其移动坐标时,我才需要处理此事件。我使用布尔标志 isUserTurn
和 isComputerMove
来决定哪个动作称为事件。但问题是,标志 isUserTurn
和 isComputerMove
并未在组件中的所有位置更新,因此在我的代码中的某个位置(例如,在 events.move 中)它们包含旧值。
我可以向您展示控制台日志,这可能有助于您理解我的问题:
*New puzzle button has been pressed*
Fetching new puzzle
PuzzlesPage.js:90 Puzzle received: {id: 4823889, puzzle_id: 'qmZAy', fen: '8/p1p2p2/8/2p1kP2/4P3/1PrP3R/3K4/8 b - - 2 34', moves: 'c3b3 d3d4 c5d4 h3b3', rating: 875, …}
PuzzlesPage.js:96 Parsed moves: (4) ['c3b3', 'd3d4', 'c5d4', 'h3b3']
PuzzlesPage.js:117 First computer move after puzzle load
PuzzlesPage.js:21 makeAIMove called
*Then computer makes its first move*
currentMoveIndex: 0
PuzzlesPage.js:23 correctMoves: (4) ['c3b3', 'd3d4', 'c5d4', 'h3b3']
PuzzlesPage.js:27 Computer move: c3b3
PuzzlesPage.js:31 Computer move is valid
Board.tsx:73 Move executed: c3 -> b3
PuzzlesPage.js:37 User turn set to true, computer move executed
PuzzlesPage.js:132 Move event triggered: c3 b3
*Here it should decide that event was called by AI move*
PuzzlesPage.js:133 isUserTurn before handling: false (Why?)
PuzzlesPage.js:134 isComputerMove before handling: false
PuzzlesPage.js:48 User move: c3 b3
*It decided that it was user's turn and took coords and called handleUserMove()*
PuzzlesPage.js:49 isUserTurn: false (Why?)
PuzzlesPage.js:50 isComputerMove: false
PuzzlesPage.js:53 Not user's turn or moves are done
*Then user makes a random move at board*
Move event triggered: h3 h6
PuzzlesPage.js:133 isUserTurn before handling: false
PuzzlesPage.js:134 isComputerMove before handling: false
PuzzlesPage.js:48 User move: h3 h6
*It decided that it was user's turn and took coords and called handleUserMove() but it's too late*
PuzzlesPage.js:49 isUserTurn: false
PuzzlesPage.js:50 isComputerMove: false
PuzzlesPage.js:53 Not user's turn or moves are done
可能还有另一个问题导致该行为,但除了错误的值更新之外,我找不到其他原因。
您的
events.move
函数对 PuzzlesPage
组件中的状态值有一个过时的闭包。 events.move
函数仅在创建时知道其周围范围内的变量/状态。由于您仅使用 Chessground
中的 componentDidMount
实例注册此函数,因此您只注册了在 events.move
的第一个渲染中创建的第一个 PuzzlesPage
函数(它只知道在该渲染的渲染中创建的初始状态变量)范围)。
您有几个选项可以解决此问题:
使用 Chessground React 包装器:https://github.com/react-chess/chessground,以便在配置更改时将新的配置对象与最新功能一起使用
重置您的
Chessground
实例以使用最新的事件对象:
componentDidUpdate(prevProps) {
// ... existing logic ...
if (prevProps.events !== this.props.events) {
this.groundInstance.set({ events: this.props.events });
}
}
请注意,当您的事件函数在每次重新渲染时发生更改时,此操作会在每次重新渲染时触发(您可以通过记住回调来更改)。