diff --git a/README.md b/README.md index d40ae69..b01dbd5 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ For more advanced code usage examples, please see example boards shown in [`Stor | arePiecesDraggable | boolean: true | [true, false] | Whether or not all pieces are draggable. | | arePremovesAllowed | boolean: false | [true, false] | Whether or not premoves are allowed. | | autoPromoteToQueen | boolean: false | [true, false] | Whether or not to automatically promote pawn to queen. | +| boardDimensions | object: { rows: 8, columns: 8 } | integer: 0 < rows, columns <= 16 | Custom board dimensions (for some chess variants). Supports up to a 16x16 board. | | boardOrientation | string: 'white' | ['white', 'black'] | The orientation of the board, the chosen colour will be at the bottom of the board. | | boardWidth | number: 560 | | The width of the board in pixels. | | clearPremovesOnRightClick | boolean: true | [true, false] | If premoves are allowed, whether or not to clear the premove queue on right click. | @@ -166,7 +167,7 @@ For more advanced code usage examples, please see example boards shown in [`Stor | onPieceDragBegin | function: (piece, sourceSquare) => {} | | User function that is run when piece is grabbed to start dragging. | | onPieceDragEnd | function: (piece, sourceSquare) => {} | | User function that is run when piece is let go after dragging. | | onPieceDrop | function: (sourceSquare, targetSquare, piece) => true | returns [true, false] | User function that is run when piece is dropped on a square. Must return whether the move was successful or not. This return value does not control whether or not the piece was placed (as that is controlled by the `position` prop) but instead controls premove logic. | -| onPromotionCheck | function: (sourceSquare, targetSquare, piece) => (((piece === "wP" && sourceSquare[1] === "7" && targetSquare[1] === "8") \|\| (piece === "bP" && sourceSquare[1] === "2" && targetSquare[1] === "1")) && Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1) | returns [true, false] | User function that is run when piece is dropped. Must return whether the move results in a promotion or not. | +| onPromotionCheck | function: (sourceSquare, targetSquare, piece) => (((piece === "wP" && sourceSquare.slice(1,3) === (boardDimensions.rows - 1).toString() && targetSquare.slice(1,3) === (boardDimensions.rows).toString())) \|\| (piece === "bP" && sourceSquare.slice(1,3) === "2" && targetSquare.slice(1,3) === "1")) && Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1) | returns [true, false] | User function that is run when piece is dropped. Must return whether the move results in a promotion or not. | | onPromotionPieceSelect | function: (piece, promoteFromSquare, promoteToSquare) => true | returns [true, false] | User function that is run when a promotion piece is selected. Must return whether the move was successful or not. | | onSquareClick | function: (square, piece) => {} | | User function that is run when a square is clicked. | | onSquareRightClick | function: (square) => {} | | User function that is run when a square is right clicked. | diff --git a/src/chessboard/components/Arrows.tsx b/src/chessboard/components/Arrows.tsx index 650dddd..269630d 100644 --- a/src/chessboard/components/Arrows.tsx +++ b/src/chessboard/components/Arrows.tsx @@ -4,21 +4,26 @@ import { getRelativeCoords } from "../functions"; import { useChessboard } from "../context/chessboard-context"; import { Arrow } from "../types"; + export const Arrows = () => { const { arrows, newArrow, + boardDimensions, boardOrientation, boardWidth, - customArrowColor: primaryArrowCollor, } = useChessboard(); + + const boardHeight = (boardWidth * boardDimensions.rows) / boardDimensions.columns; + const squareWidth = boardWidth / boardDimensions.columns; + const arrowsList = [...arrows, newArrow].filter(Boolean) as Arrow[]; return ( { {arrowsList.map((arrow, i) => { const [arrowStartField, arrowEndField, arrowColor] = arrow; if (arrowStartField === arrowEndField) return null; + const from = getRelativeCoords( + boardDimensions, boardOrientation, boardWidth, arrowStartField ); + const to = getRelativeCoords( + boardDimensions, boardOrientation, boardWidth, arrowEndField ); - let ARROW_LENGTH_REDUCER = boardWidth / 32; + + let ARROW_LENGTH_REDUCER = squareWidth / 5; const isArrowActive = i === arrows.length; - // if there are different arrows targeting the same square make their length a bit shorter + if ( arrows.some( (restArrow) => @@ -51,8 +61,9 @@ export const Arrows = () => { ) && !isArrowActive ) { - ARROW_LENGTH_REDUCER = boardWidth / 16; + ARROW_LENGTH_REDUCER = squareWidth / 3.5; } + const dx = to.x - from.x; const dy = to.y - from.y; @@ -90,7 +101,7 @@ export const Arrows = () => { opacity={isArrowActive ? "0.5" : "0.65"} stroke={arrowColor ?? primaryArrowCollor} strokeWidth={ - isArrowActive ? (0.9 * boardWidth) / 40 : boardWidth / 40 + isArrowActive ? 0.9 * squareWidth / 5.5 : squareWidth / 5.5 } markerEnd={`url(#arrowhead-${i})`} /> diff --git a/src/chessboard/components/Board.tsx b/src/chessboard/components/Board.tsx index adee1ec..56c672e 100644 --- a/src/chessboard/components/Board.tsx +++ b/src/chessboard/components/Board.tsx @@ -10,6 +10,7 @@ export function Board() { const { boardWidth, + boardDimensions, clearCurrentRightClickDown, onPromotionPieceSelect, setShowPromoteDialog, @@ -17,6 +18,8 @@ export function Board() { customBoardStyle, } = useChessboard(); + const boardHeight = (boardWidth * boardDimensions.rows) / boardDimensions.columns; + useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( @@ -39,7 +42,7 @@ export function Board() { ref={boardRef} style={{ position: "relative", - ...boardStyles(boardWidth), + ...boardStyles(boardWidth, boardHeight), ...customBoardStyle, }} > @@ -60,7 +63,7 @@ export function Board() { zIndex: "100", backgroundColor: "rgba(22,21,18,.7)", width: boardWidth, - height: boardWidth, + height: boardHeight, }} /> @@ -73,8 +76,8 @@ export function Board() { ); } -const boardStyles = (width: number) => ({ +const boardStyles = (width: number, height: number) => ({ cursor: "default", - height: width, + height, width, }); diff --git a/src/chessboard/components/CustomDragLayer.tsx b/src/chessboard/components/CustomDragLayer.tsx index cbe4ff3..fe466a2 100644 --- a/src/chessboard/components/CustomDragLayer.tsx +++ b/src/chessboard/components/CustomDragLayer.tsx @@ -9,8 +9,17 @@ export type CustomDragLayerProps = { }; export function CustomDragLayer({ boardContainer }: CustomDragLayerProps) { - const { boardWidth, chessPieces, id, snapToCursor, allowDragOutsideBoard } = - useChessboard(); + const { + boardWidth, + boardDimensions, + chessPieces, + id, + snapToCursor, + allowDragOutsideBoard, + } = useChessboard(); + + const boardHeight = (boardWidth * boardDimensions.rows) / boardDimensions.columns; + const squareWidth = boardWidth / boardDimensions.columns; const collectedProps = useDragLayer((monitor) => ({ item: monitor.getItem(), @@ -36,7 +45,8 @@ export function CustomDragLayer({ boardContainer }: CustomDragLayerProps) { if (!clientOffset || !sourceClientOffset) return { display: "none" }; let { x, y } = snapToCursor ? clientOffset : sourceClientOffset; - const halfSquareWidth = boardWidth / 8 / 2; + const halfSquareWidth = squareWidth / 2; + if (snapToCursor) { x -= halfSquareWidth; y -= halfSquareWidth; @@ -44,11 +54,12 @@ export function CustomDragLayer({ boardContainer }: CustomDragLayerProps) { if (!allowDragOutsideBoard) { const { left, top } = boardContainer; - // half square so the piece reaches the board + const maxLeft = left - halfSquareWidth; const maxTop = top - halfSquareWidth; const maxRight = left + boardWidth - halfSquareWidth; - const maxBottom = top + boardWidth - halfSquareWidth; + const maxBottom = top + boardHeight - halfSquareWidth; + x = Math.max(maxLeft, Math.min(x, maxRight)); y = Math.max(maxTop, Math.min(y, maxBottom)); } @@ -61,7 +72,7 @@ export function CustomDragLayer({ boardContainer }: CustomDragLayerProps) { touchAction: "none", }; }, - [boardWidth, allowDragOutsideBoard, snapToCursor, boardContainer] + [squareWidth, boardWidth, boardHeight, allowDragOutsideBoard, snapToCursor, boardContainer] ); return isDragging && item.id === id ? ( @@ -77,15 +88,11 @@ export function CustomDragLayer({ boardContainer }: CustomDragLayerProps) {
{typeof chessPieces[item.piece] === "function" ? ( (chessPieces[item.piece] as CustomPieceFn)({ - squareWidth: boardWidth / 8, + squareWidth, isDragging: true, }) ) : ( - + {chessPieces[item.piece] as ReactNode} )} diff --git a/src/chessboard/components/Notation.tsx b/src/chessboard/components/Notation.tsx index c5192c7..6e0af6f 100644 --- a/src/chessboard/components/Notation.tsx +++ b/src/chessboard/components/Notation.tsx @@ -1,4 +1,3 @@ -import { COLUMNS } from "../consts"; import { useChessboard } from "../context/chessboard-context"; type NotationProps = { @@ -8,6 +7,7 @@ type NotationProps = { export function Notation({ row, col }: NotationProps) { const { + boardDimensions, boardOrientation, boardWidth, customDarkSquareStyle, @@ -15,19 +15,25 @@ export function Notation({ row, col }: NotationProps) { customNotationStyle, } = useChessboard(); + const dynamicColumns = Array.from( + { length: boardDimensions.columns }, + (_, i) => String.fromCharCode(97 + i) // 97 is 'a' + ); + const squareWidth = boardWidth / boardDimensions.columns; + const whiteColor = customLightSquareStyle.backgroundColor; const blackColor = customDarkSquareStyle.backgroundColor; const isRow = col === 0; - const isColumn = row === 7; + const isColumn = row === (boardDimensions.rows - 1); const isBottomLeftSquare = isRow && isColumn; function getRow() { - return boardOrientation === "white" ? 8 - row : row + 1; + return boardOrientation === "white" ? boardDimensions.rows - row : row + 1; } function getColumn() { - return boardOrientation === "black" ? COLUMNS[7 - col] : COLUMNS[col]; + return boardOrientation === "black" ? dynamicColumns[(boardDimensions.columns - 1) - col] : dynamicColumns[col]; } function renderBottomLeft() { @@ -35,20 +41,22 @@ export function Notation({ row, col }: NotationProps) { <>
{getRow()}
{getColumn()} @@ -64,8 +72,8 @@ export function Notation({ row, col }: NotationProps) { userSelect: "none", zIndex: 3, position: "absolute", - ...{ color: col % 2 !== 0 ? blackColor : whiteColor }, - ...alphaStyle(boardWidth, customNotationStyle), + ...{ color: boardOrientation === "white" ? ((col % 2 === 0) ? whiteColor : blackColor) : ((col % 2 !== 0) === (boardDimensions.rows % 2 === 0) === (boardDimensions.columns % 2 === 0) ? blackColor : whiteColor) }, + ...alphaStyle(squareWidth, customNotationStyle), }} > {getColumn()} @@ -80,10 +88,8 @@ export function Notation({ row, col }: NotationProps) { userSelect: "none", zIndex: 3, position: "absolute", - ...(boardOrientation === "black" - ? { color: row % 2 === 0 ? blackColor : whiteColor } - : { color: row % 2 === 0 ? blackColor : whiteColor }), - ...numericStyle(boardWidth, customNotationStyle), + ...({ color: boardOrientation === "white" ? ((row % 2 === 0) === (boardDimensions.rows % 2 !== 0) ? whiteColor : blackColor) : ((row % 2 === 0) === (boardDimensions.columns % 2 !== 0) ? whiteColor : blackColor) }), + ...numericStyle(squareWidth, customNotationStyle), }} > {getRow()} @@ -107,15 +113,15 @@ export function Notation({ row, col }: NotationProps) { } const alphaStyle = (width: number, customNotationStyle?: Record) => ({ - alignSelf: "flex-end", - paddingLeft: width / 8 - width / 48, - fontSize: width / 48, + right: width / 48, + bottom: 0, + fontSize: width / 6.2, ...customNotationStyle }); const numericStyle = (width: number, customNotationStyle?: Record) => ({ - alignSelf: "flex-start", - paddingRight: width / 8 - width / 48, - fontSize: width / 48, + top: 0, + left: width / 48, + fontSize: width / 6.2, ...customNotationStyle }); diff --git a/src/chessboard/components/Piece.tsx b/src/chessboard/components/Piece.tsx index 1073cc7..a842670 100644 --- a/src/chessboard/components/Piece.tsx +++ b/src/chessboard/components/Piece.tsx @@ -21,6 +21,7 @@ export function Piece({ const { animationDuration, arePiecesDraggable, + boardDimensions, boardWidth, boardOrientation, chessPieces, @@ -38,6 +39,8 @@ export function Piece({ positionDifferences, } = useChessboard(); + const squareWidth = boardWidth / boardDimensions.columns; + const [pieceStyle, setPieceStyle] = useState({ opacity: 1, zIndex: 5, @@ -111,7 +114,6 @@ export function Piece({ const sourceSq = square; const targetSq = newSquare[0]; if (sourceSq && targetSq) { - const squareWidth = boardWidth / 8; setPieceStyle((oldPieceStyle) => ({ ...oldPieceStyle, transform: `translate(${ @@ -120,7 +122,7 @@ export function Piece({ squareWidth }px, ${ (boardOrientation === "black" ? -1 : 1) * - (Number(sourceSq[1]) - Number(targetSq[1])) * + (Number(sourceSq.slice(1,3)) - Number(targetSq.slice(1,3))) * squareWidth }px)`, transition: `transform ${animationDuration}ms`, @@ -166,15 +168,15 @@ export function Piece({ > {typeof chessPieces[piece] === "function" ? ( (chessPieces[piece] as CustomPieceFn)({ - squareWidth: boardWidth / 8, + squareWidth, isDragging, square, }) ) : ( {chessPieces[piece] as ReactNode} diff --git a/src/chessboard/components/PromotionDialog.tsx b/src/chessboard/components/PromotionDialog.tsx index 9d9e7e0..242ab1d 100644 --- a/src/chessboard/components/PromotionDialog.tsx +++ b/src/chessboard/components/PromotionDialog.tsx @@ -1,17 +1,18 @@ import { useChessboard } from "../context/chessboard-context"; import { getRelativeCoords } from "../functions"; -import { PromotionPieceOption } from "../types"; +import { PromotionPieceOption, Square } from "../types"; import { PromotionOption } from "./PromotionOption"; export function PromotionDialog() { const { + boardDimensions, boardOrientation, boardWidth, promotionDialogVariant, promoteToSquare, } = useChessboard(); - const promotePieceColor = promoteToSquare?.[1] === "1" ? "b" : "w"; + const promotePieceColor = promoteToSquare?.slice(1,3) === "1" ? "b" : "w"; const promotionOptions: PromotionPieceOption[] = [ `${promotePieceColor ?? "w"}Q`, `${promotePieceColor ?? "w"}R`, @@ -23,7 +24,7 @@ export function PromotionDialog() { default: { display: "grid", gridTemplateColumns: "1fr 1fr", - transform: `translate(${-boardWidth / 8}px, ${-boardWidth / 8}px)`, + transform: `translate(${-boardWidth / boardDimensions.columns}px, ${-boardWidth / boardDimensions.columns}px)`, }, vertical: { transform: `translate(${-boardWidth / 16}px, ${-boardWidth / 16}px)`, @@ -32,7 +33,7 @@ export function PromotionDialog() { display: "flex", justifyContent: "center", alignItems: "center", - transform: `translate(0px, ${(3 * boardWidth) / 8}px)`, + transform: `translate(0px, ${(3 * boardWidth) / boardDimensions.columns}px)`, width: "100%", height: `${boardWidth / 4}px`, top: 0, @@ -42,9 +43,10 @@ export function PromotionDialog() { }; const dialogCoords = getRelativeCoords( + boardDimensions, boardOrientation, boardWidth, - promoteToSquare || "a8" + promoteToSquare || `a${boardDimensions.rows}` as Square ); return ( diff --git a/src/chessboard/components/PromotionOption.tsx b/src/chessboard/components/PromotionOption.tsx index f381f06..423dad8 100644 --- a/src/chessboard/components/PromotionOption.tsx +++ b/src/chessboard/components/PromotionOption.tsx @@ -11,6 +11,7 @@ export function PromotionOption({ option }: Props) { const [isHover, setIsHover] = useState(false); const { + boardDimensions, boardWidth, chessPieces, customDarkSquareStyle, @@ -69,15 +70,15 @@ export function PromotionOption({ option }: Props) { }} > {(chessPieces[option] as CustomPieceFn)({ - squareWidth: boardWidth / 8, + squareWidth: boardWidth / boardDimensions.columns, isDragging: false, })}
) : ( (null); const { autoPromoteToQueen, + boardDimensions, boardWidth, boardOrientation, clearArrows, @@ -56,6 +57,8 @@ export function Square({ setShowPromoteDialog, } = useChessboard(); + const boardHeight = (boardWidth * boardDimensions.rows) / boardDimensions.columns; + const [{ isOver }, drop] = useDrop( () => ({ accept: "piece", @@ -108,10 +111,10 @@ export function Square({ const { x, y } = squareRef.current.getBoundingClientRect(); setSquares((oldSquares) => ({ ...oldSquares, [square]: { x, y } })); } - }, [boardWidth, boardOrientation]); + }, [boardWidth, boardHeight, boardOrientation]); const defaultSquareStyle = { - ...borderRadius(square, boardOrientation, customBoardStyle), + ...borderRadius(square, boardDimensions, boardOrientation, customBoardStyle), ...(squareColor === "black" ? customDarkSquareStyle : customLightSquareStyle), @@ -145,7 +148,6 @@ export function Square({ }} onMouseOver={(e) => { // noop if moving from child of square into square. - if (e.buttons === 2 && currentRightClickDown) { drawNewArrow(currentRightClickDown, square); } @@ -194,9 +196,10 @@ export function Square({ // @ts-ignore ref={squareRef as any} style={{ - ...size(boardWidth), + ...size(boardWidth, boardHeight, boardDimensions), ...center, ...(!squareHasPremove && customSquareStyles?.[square]), + position: 'relative', }} > {children} @@ -207,7 +210,7 @@ export function Square({ square={square} squareColor={squareColor} style={{ - ...size(boardWidth), + ...size(boardWidth, boardHeight, boardDimensions), ...center, ...(!squareHasPremove && customSquareStyles?.[square]), }} @@ -221,16 +224,21 @@ export function Square({ const center = { display: "flex", - justifyContent: "center", + position: "relative", }; -const size = (width: number) => ({ - width: width / 8, - height: width / 8, +const size = ( + width: number, + height: number, + boardDimensions: BoardDimensions = { rows: 8, columns: 8 } +) => ({ + width: width / boardDimensions.columns, + height: height / boardDimensions.rows, }); const borderRadius = ( square: Sq, + boardDimensions: BoardDimensions = {rows: 8, columns: 8}, boardOrientation: BoardOrientation, customBoardStyle?: Record ) => { @@ -241,7 +249,7 @@ const borderRadius = ( ? { borderBottomLeftRadius: customBoardStyle.borderRadius } : { borderTopRightRadius: customBoardStyle.borderRadius }; } - if (square === "a8") { + if (square === `a${boardDimensions.rows}`) { return boardOrientation === "white" ? { borderTopLeftRadius: customBoardStyle.borderRadius } : { borderBottomRightRadius: customBoardStyle.borderRadius }; @@ -251,7 +259,7 @@ const borderRadius = ( ? { borderBottomRightRadius: customBoardStyle.borderRadius } : { borderTopLeftRadius: customBoardStyle.borderRadius }; } - if (square === "h8") { + if (square === `h${boardDimensions.rows}`) { return boardOrientation === "white" ? { borderTopRightRadius: customBoardStyle.borderRadius } : { borderBottomLeftRadius: customBoardStyle.borderRadius }; diff --git a/src/chessboard/components/Squares.tsx b/src/chessboard/components/Squares.tsx index f5fee6f..3ca0b7e 100644 --- a/src/chessboard/components/Squares.tsx +++ b/src/chessboard/components/Squares.tsx @@ -1,5 +1,4 @@ import { useState, useMemo } from "react"; -import { COLUMNS } from "../consts"; import { useChessboard } from "../context/chessboard-context"; import { Coords, Piece as Pc, Square as Sq } from "../types"; import { Notation } from "./Notation"; @@ -17,6 +16,7 @@ export function Squares() { const { arePremovesAllowed, + boardDimensions, boardOrientation, boardWidth, currentPosition, @@ -25,6 +25,11 @@ export function Squares() { showBoardNotation, } = useChessboard(); + const dynamicColumns = Array.from( + { length: boardDimensions.columns }, + (_, i) => String.fromCharCode(97 + i) // 97 is 'a' + ); + const premovesHistory: PremovesHistory = useMemo(() => { const result: PremovesHistory = []; // if premoves aren't allowed, don't waste time on calculations @@ -58,7 +63,7 @@ export function Squares() { return (
- {[...Array(8)].map((_, r) => { + {[...Array(boardDimensions.rows)].map((_, r) => { return (
- {[...Array(8)].map((_, c) => { + {[...Array(boardDimensions.columns)].map((_, c) => { const square = boardOrientation === "black" - ? ((COLUMNS[7 - c] + (r + 1)) as Sq) - : ((COLUMNS[c] + (8 - r)) as Sq); - const squareColor = c % 2 === r % 2 ? "white" : "black"; - const squareHasPremove = premoves.find( + ? ((dynamicColumns[boardDimensions.columns - 1 - c] + (r + 1)) as Sq) + : ((dynamicColumns[c] + (boardDimensions.rows - r)) as Sq); + + const squareColor = (r + c) % 2 === 0 === (boardOrientation === "white" ? boardDimensions.rows % 2 !== 0 : boardDimensions.columns % 2 !== 0) ? "black" : "white"; + const squareHasPremove = premoves.some( (p) => p.sourceSq === square || p.targetSq === square ); - const squareHasPremoveTarget = premovesHistory - .filter( - ({ premovesRoute }) => - premovesRoute.at(-1)?.targetSq === square - ) - //the premoved piece with the higher index will be shown, as it is the latest one + .filter(({ premovesRoute }) => premovesRoute.at(-1)?.targetSq === square) .sort( (a, b) => - b.premovesRoute.at(-1)?.index! - - a.premovesRoute.at(-1)?.index! + b.premovesRoute.at(-1)?.index! - a.premovesRoute.at(-1)?.index! ) .at(0); diff --git a/src/chessboard/consts.ts b/src/chessboard/consts.ts index 9c0a0c2..e4e6fef 100644 --- a/src/chessboard/consts.ts +++ b/src/chessboard/consts.ts @@ -1,7 +1,5 @@ import { BoardPosition } from "./types"; -export const COLUMNS = "abcdefgh".split(""); - export const START_POSITION_OBJECT: BoardPosition = { a8: "bR", b8: "bN", @@ -35,28 +33,4 @@ export const START_POSITION_OBJECT: BoardPosition = { f1: "wB", g1: "wN", h1: "wR", -}; - -export const WHITE_COLUMN_VALUES: { [col in string]: number } = { - a: 0, - b: 1, - c: 2, - d: 3, - e: 4, - f: 5, - g: 6, - h: 7, -}; -export const BLACK_COLUMN_VALUES: { [col in string]: number } = { - a: 7, - b: 6, - c: 5, - d: 4, - e: 3, - f: 2, - g: 1, - h: 0, -}; - -export const WHITE_ROWS = [7, 6, 5, 4, 3, 2, 1, 0]; -export const BLACK_ROWS = [0, 1, 2, 3, 4, 5, 6, 7]; +}; \ No newline at end of file diff --git a/src/chessboard/context/chessboard-context.tsx b/src/chessboard/context/chessboard-context.tsx index 1706ed5..fb2cafa 100644 --- a/src/chessboard/context/chessboard-context.tsx +++ b/src/chessboard/context/chessboard-context.tsx @@ -46,6 +46,7 @@ interface ChessboardProviderContext { arePiecesDraggable: RequiredChessboardProps["arePiecesDraggable"]; arePremovesAllowed: RequiredChessboardProps["arePremovesAllowed"]; autoPromoteToQueen: RequiredChessboardProps["autoPromoteToQueen"]; + boardDimensions: RequiredChessboardProps["boardDimensions"]; boardOrientation: RequiredChessboardProps["boardOrientation"]; boardWidth: RequiredChessboardProps["boardWidth"]; customArrowColor: RequiredChessboardProps["customArrowColor"]; @@ -124,6 +125,7 @@ export const ChessboardProvider = forwardRef( arePiecesDraggable = true, arePremovesAllowed = false, autoPromoteToQueen = false, + boardDimensions = { rows: 8, columns: 8 }, boardOrientation = "white", boardWidth, children, @@ -158,11 +160,11 @@ export const ChessboardProvider = forwardRef( onPromotionCheck = (sourceSquare, targetSquare, piece) => { return ( ((piece === "wP" && - sourceSquare[1] === "7" && - targetSquare[1] === "8") || + sourceSquare.slice(1,3) === (boardDimensions.rows - 1).toString() && + targetSquare.slice(1,3) === (boardDimensions.rows).toString()) || (piece === "bP" && - sourceSquare[1] === "2" && - targetSquare[1] === "1")) && + sourceSquare.slice(1,3) === "2" && + targetSquare.slice(1,3) === "1")) && Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1 ); }, @@ -179,9 +181,19 @@ export const ChessboardProvider = forwardRef( }: ChessboardProviderProps, ref ) => { + + useEffect(() => { + if (boardDimensions.rows <= 0 || boardDimensions.columns <= 0) { + throw new Error("Board Dimensions Out of Range. Min dimensions are 1x1."); + } + else if (boardDimensions.rows > 16 || boardDimensions.columns > 16) { + throw new Error("Board Dimensions Out of Range. Max Dimensions are 16x16."); + } + }, [boardDimensions]); + // position stored and displayed on board const [currentPosition, setCurrentPosition] = useState( - convertPositionToObject(position) + convertPositionToObject(position, boardDimensions) ); // calculated differences between current and incoming positions @@ -257,7 +269,7 @@ export const ChessboardProvider = forwardRef( // clear any open promotion dialogs clearPromotion(); - const newPosition = convertPositionToObject(position); + const newPosition = convertPositionToObject(position, boardDimensions); const differences = getPositionDifferences(currentPosition, newPosition); const newPieceColour = Object.keys(differences.added)?.length <= 2 @@ -488,6 +500,7 @@ export const ChessboardProvider = forwardRef( autoPromoteToQueen, boardOrientation, boardWidth, + boardDimensions, chessPieces, clearArrows, clearCurrentRightClickDown, diff --git a/src/chessboard/functions.ts b/src/chessboard/functions.ts index e65f1f6..114cd8a 100644 --- a/src/chessboard/functions.ts +++ b/src/chessboard/functions.ts @@ -1,31 +1,30 @@ -import { BoardOrientation, BoardPosition, Piece, Square } from "./types"; +import { BoardDimensions, BoardOrientation, BoardPosition, Piece, Square } from "./types"; import { - BLACK_COLUMN_VALUES, - BLACK_ROWS, - COLUMNS, START_POSITION_OBJECT, - WHITE_COLUMN_VALUES, - WHITE_ROWS, } from "./consts"; /** * Retrieves the coordinates at the centre of the requested square, relative to the top left of the board (0, 0). */ export function getRelativeCoords( + boardDimensions: BoardDimensions = { rows: 8, columns: 8 }, boardOrientation: BoardOrientation, boardWidth: number, square: Square -): { - x: number; - y: number; -} { - const squareWidth = boardWidth / 8; - const columns = - boardOrientation === "white" ? WHITE_COLUMN_VALUES : BLACK_COLUMN_VALUES; - const rows = boardOrientation === "white" ? WHITE_ROWS : BLACK_ROWS; +): { x: number; y: number } { + const squareWidth = boardWidth / boardDimensions.columns; + + const columnIndex = boardOrientation === "white" + ? square[0].charCodeAt(0) - "a".charCodeAt(0) + : boardDimensions.columns - 1 - (square[0].charCodeAt(0) - "a".charCodeAt(0)); + + const match = square.match(/\d+/); + const rowIndex = boardOrientation === "white" + ? boardDimensions.rows - (match ? parseInt(match[0], 10) : 8) + : (match ? parseInt(match[0], 10) : 8) - 1; - const x = columns[square[0]] * squareWidth + squareWidth / 2; - const y = rows[parseInt(square[1], 10) - 1] * squareWidth + squareWidth / 2; + const x = columnIndex * squareWidth + squareWidth / 2; + const y = rowIndex * squareWidth + squareWidth / 2; return { x, y }; } @@ -92,7 +91,8 @@ export function getPositionDifferences( * Converts a fen string or existing position object to a position object. */ export function convertPositionToObject( - position: string | BoardPosition + position: string | BoardPosition, + boardDimensions?: BoardDimensions ): BoardPosition { if (position === "start") { return START_POSITION_OBJECT; @@ -100,7 +100,7 @@ export function convertPositionToObject( if (typeof position === "string") { // attempt to convert fen to position object - return fenToObj(position); + return fenToObj(position, boardDimensions); } return position; @@ -109,28 +109,44 @@ export function convertPositionToObject( /** * Converts a fen string to a position object. */ -function fenToObj(fen: string): BoardPosition { - if (!isValidFen(fen)) return {}; +function fenToObj(fen: string, boardDimensions: BoardDimensions = { rows: 8, columns: 8}): BoardPosition { + if (!isValidFen(fen, boardDimensions)) return {}; + + fen = expandFenEmptySquares(fen); // cut off any move, castling, etc info from the end. we're only interested in position information fen = fen.replace(/ .+$/, ""); const rows = fen.split("/"); const position: BoardPosition = {}; - let currentRow = 8; + let currentRow = boardDimensions.rows; - for (let i = 0; i < 8; i++) { + const numericRegex = /^\d+$/; + + for (let i = 0; i < boardDimensions.rows; i++) { const row = rows[i].split(""); let colIdx = 0; // loop through each character in the FEN section for (let j = 0; j < row.length; j++) { // number / empty squares - if (row[j].search(/[1-8]/) !== -1) { + if (numericRegex.test(row[j])) { const numEmptySquares = parseInt(row[j], 10); - colIdx = colIdx + numEmptySquares; + + // Validate the range explicitly + if (numEmptySquares >= 1 && numEmptySquares <= boardDimensions.columns) { + colIdx += numEmptySquares; + } else { + throw new Error( + `Invalid FEN: empty square count (${numEmptySquares}) exceeds board dimensions (${boardDimensions.columns})` + ); + } } else { // piece - const square = COLUMNS[colIdx] + currentRow; + const dynamicColumns = Array.from( + { length: boardDimensions.columns }, + (_, i) => String.fromCharCode(97 + i) // 97 is 'a' + ); + const square = dynamicColumns[colIdx] + currentRow; position[square as Square] = fenToPieceCode(row[j]); colIdx = colIdx + 1; } @@ -143,20 +159,20 @@ function fenToObj(fen: string): BoardPosition { /** * Returns whether string is valid fen notation. */ -function isValidFen(fen: string): boolean { +function isValidFen(fen: string, boardDimensions: BoardDimensions = { rows: 8, columns: 8}): boolean { // cut off any move, castling, etc info from the end. we're only interested in position information fen = fen.replace(/ .+$/, ""); // expand the empty square numbers to just 1s fen = expandFenEmptySquares(fen); - // fen should be 8 sections separated by slashes + // there should be a section seperated by a slash for each row on the board (8 for standard chess) const chunks = fen.split("/"); - if (chunks.length !== 8) return false; + if (chunks.length !== boardDimensions.rows) return false; // check each section - for (let i = 0; i < 8; i++) { - if (chunks[i].length !== 8 || chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) { + for (let i = 0; i < boardDimensions.rows; i++) { + if (chunks[i].length !== boardDimensions.columns || chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) { return false; } } @@ -168,14 +184,7 @@ function isValidFen(fen: string): boolean { * Expand out fen notation to countable characters for validation */ function expandFenEmptySquares(fen: string): string { - return fen - .replace(/8/g, "11111111") - .replace(/7/g, "1111111") - .replace(/6/g, "111111") - .replace(/5/g, "11111") - .replace(/4/g, "1111") - .replace(/3/g, "111") - .replace(/2/g, "11"); + return fen.replace(/\d+/g, (match) => "1".repeat(parseInt(match, 10))); } /** diff --git a/src/chessboard/types/index.ts b/src/chessboard/types/index.ts index 94ea1b5..8b91c8d 100644 --- a/src/chessboard/types/index.ts +++ b/src/chessboard/types/index.ts @@ -1,71 +1,11 @@ import type { FC, ReactElement, ReactNode, Ref, RefObject } from "react"; import { BackendFactory } from "dnd-core"; -export type Square = - | "a8" - | "b8" - | "c8" - | "d8" - | "e8" - | "f8" - | "g8" - | "h8" - | "a7" - | "b7" - | "c7" - | "d7" - | "e7" - | "f7" - | "g7" - | "h7" - | "a6" - | "b6" - | "c6" - | "d6" - | "e6" - | "f6" - | "g6" - | "h6" - | "a5" - | "b5" - | "c5" - | "d5" - | "e5" - | "f5" - | "g5" - | "h5" - | "a4" - | "b4" - | "c4" - | "d4" - | "e4" - | "f4" - | "g4" - | "h4" - | "a3" - | "b3" - | "c3" - | "d3" - | "e3" - | "f3" - | "g3" - | "h3" - | "a2" - | "b2" - | "c2" - | "d2" - | "e2" - | "f2" - | "g2" - | "h2" - | "a1" - | "b1" - | "c1" - | "d1" - | "e1" - | "f1" - | "g1" - | "h1"; +type SquareColumn = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p'; +type SquareRow = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16; + +// Combine column and row to create the full square type +export type Square = `${SquareColumn}${SquareRow}`; export type Piece = | "wP" @@ -126,6 +66,8 @@ export type CustomSquareStyles = { export type BoardOrientation = "white" | "black"; +export type BoardDimensions = { rows: number; columns: number }; + export type DropOffBoardAction = "snapback" | "trash"; export type Coords = { x: number; y: number }; @@ -163,6 +105,11 @@ export type ChessboardProps = { * @default false */ autoPromoteToQueen?: boolean; + /** + * The number of squares on the board represented by rows, and columns (For some chess variants not using standard board sizes). + * @default { rows: 8, columns: 8 } + */ + boardDimensions?: BoardDimensions; /** * The orientation of the board, the chosen colour will be at the bottom of the board. * @default white @@ -328,8 +275,8 @@ export type ChessboardProps = { onSparePieceDrop?: (piece: Piece, targetSquare: Square) => boolean; /** * User function that is run when piece is dropped. Must return whether the move results in a promotion or not. - * @default (sourceSquare, targetSquare, piece) => (((piece === "wP" && sourceSquare[1] === "7" && targetSquare[1] === "8") || - * (piece === "bP" && sourceSquare[1] === "2" && targetSquare[1] === "1")) && + * @default (sourceSquare, targetSquare, piece) => (((piece === "wP" && sourceSquare.slice(1,3) === (boardDimensions.rows - 1).toString() && targetSquare.slice(1,3) === (boardDimensions.rows).toString())) || + * (piece === "bP" && sourceSquare.slice(1,3) === "2" && targetSquare.slice(1,3) === "1")) && * Math.abs(sourceSquare.charCodeAt(0) - targetSquare.charCodeAt(0)) <= 1) */ onPromotionCheck?: ( diff --git a/stories/Chessboard.stories.tsx b/stories/Chessboard.stories.tsx index 8622f6e..12e341c 100644 --- a/stories/Chessboard.stories.tsx +++ b/stories/Chessboard.stories.tsx @@ -847,6 +847,80 @@ export const CustomSquare = () => { ); }; +export const VaryingBoardDimensions = () => { + const [boardRows, setBoardRows] = useState(10); + const [boardColumns, setBoardColumns] = useState(10); + + const handleRowsChange = (event) => { + let inputValue = parseInt(event.target.value, 10) || 8; + const clampedValue = Math.max(1, Math.min(inputValue, 16)); + setBoardRows(clampedValue); + }; + + const handleColumnsChange = (event) => { + let inputValue = parseInt(event.target.value, 10) || 8; + const clampedValue = Math.max(1, Math.min(inputValue, 16)); + setBoardColumns(clampedValue); + }; + + return ( +
+

Adjust Dimensions:

+
+
+ + +
+
+ + +
+
+ +
+ ); +}; + export const AnalysisBoard = () => { const engine = useMemo(() => new Engine(), []); const game = useMemo(() => new Chess(), []);