Hi community. After a pause for a couple of weeks, while I was busy, I’ve come back to my pet project. In this section, I want to create the ability to move figures. And also it will be good to highlight available cells where we can make a movement.
We will start with FigureModel
and add a new method canMove
. It will return true
or false
based on can this figure is moved to the target cell or not. In the first simple implementation, we will add a simple check here and will return false
if the target cell already has a figure.
// src/models/FigureModel.ts
class FigureModel {
label: Labels;
imageSrc: string;
isDame: boolean;
cell: CellModel;
name: FigureNames;
constructor(label: Labels, cell: CellModel) {
this.label = label;
this.cell = cell;
this.cell.figure = this;
this.isDame = false;
this.name = FigureNames.Piece;
this.imageSrc = label === Labels.Light ? pieceImgLight : pieceImgDark;
}
canMove(targetCell: CellModel): boolean {
return !targetCell.figure;
}
}
The next step is to create method moveFigure
in CellModel
. The logic is easy. It will take targetCell
as an argument. Then we check if our selected cell has a figure and this figure can be moved to the target cell - then we will save the figure to a new cell and clean our current (selected) cell:
// src/models/CellModel.ts
class CellModel {
readonly x: number;
readonly y: number;
readonly label: Labels;
figure: FigureModel | null; // our figure
board: BoardModel;
available: boolean;
key: string;
constructor(x: number, y: number, label: Labels, board: BoardModel) {
this.x = x; // x coord
this.y = y; // y coord
this.label = label;
this.board = board;
this.available = false; // is it free for figure
this.key = `${String(x)}${String(y)}`;
this.figure = null; // null by default
}
moveFigure(targetCell: CellModel) {
if (this.figure && this.figure.canMove(targetCell)) {
targetCell.figure = this.figure; // set figure on target cell
this.figure = null; // clean current cell
}
}
}
I also want to highlight cells where we can move the selected figures. To do this we can use property available
in our CellModel
which is false
by default. So let’s add a simple div with class available in our Cell component if it doesn’t have a figure and the cell is available: cell.available && !cell.figure && <div className="available" />
Full Cell.tsx component:
// src/components/Cell/Cell.tsx
export const Cell = ({
cell,
rowIndex,
cellIndex,
selected,
onFigureClick,
}: CellProps): ReactElement => {
const { figure, label } = cell;
const handleFigureClick = () => onFigureClick(cell);
return (
<div
className={mergeClasses('cell', label, selected ? 'selected' : '')}
onClick={handleFigureClick}
>
{figure?.imageSrc && <img className="icon" src={figure.imageSrc} alt={figure.name} />}
{cell.available && !cell.figure && <div className="available" />}
{(rowIndex === 0 || rowIndex === 7) && (
<div className={mergeClasses('board-label', rowIndex === 0 ? 'top' : 'bottom')}>
{Letters[cellIndex]}
</div>
)}
{(cellIndex === 0 || cellIndex === 7) && (
<div className={mergeClasses('board-label', cellIndex === 0 ? 'left' : 'right')}>
{8 - rowIndex}
</div>
)}
</div>
);
};
And some styles for highlighting:
// src/components/Cell/Cell.css
.available {
background-color: #fff;
width: 10px;
height: 10px;
border-radius: 50%;
}
Cool. Let’s check and set temporary true
for available property in CellModel
this.available = true;
and we can see that all cells without figures are highlighted:
Perfect. Let’s again switch available property to false by default this.available = false
because we want to highlight available cells for movement when we select a figure. To do this we need to create a function which we will call on every selection. Let’s do this in BoardModel
:
highlightCells(selectedCell: CellModel | null) {
this.cells.forEach((row) => {
row.forEach((cell) => {
cell.available = !!selectedCell?.figure?.canMove(cell);
});
});
}
It will take a target cell as an argument, then iterate on every cell in the cell array and set the available property for every cell based on if this cell has a figure and this figure can move. Then we need to create a highlight function in our Board
component which will call the same function from the model and pass the selected cell as an argument. And we will run it when our selected cell is changed, so we can use useEffect
hook here:
// src/components/Board/Board.tsx
const highlightCells = () => {
board.highlightCells(selected);
};
useEffect(() => {
highlightCells();
}, [selected]);
But when we run our project and select a figure (A3 is selected) we don’t see any highlights. Why this is happened?
This is an important moment. On every selection, we run highlight functions in BoardModel
and it updates available
properties. But these updates are inside the model, so our React component doesn’t track these changes and doesn’t re-render the board. To fix this we need to update our App.tsx
component state with a new Board model. Let’s create getNewModel
method in BoardModel
. It will create a new BoardModel instance, save the existing cells array to it and return the instance:
Full BoardModel.tsx
// src/models/BoardModel.ts
class BoardModel {
cells: CellModel[][] = [];
cellsInRow = 8;
createCells() {
for (let i = 0; i < this.cellsInRow; i += 1) {
const row: CellModel[] = [];
for (let j = 0; j < this.cellsInRow; j += 1) {
if ((i + j) % 2 !== 0) {
row.push(new CellModel(i, j, Labels.Dark, this)); // black
} else {
row.push(new CellModel(i, j, Labels.Light, this)); // white
}
}
this.cells.push(row);
}
}
highlightCells(selectedCell: CellModel | null) {
this.cells.forEach((row) => {
row.forEach((cell) => {
cell.available = !!selectedCell?.figure?.canMove(cell);
});
});
}
getNewBoard(): BoardModel {
const newBoard = new BoardModel();
newBoard.cells = this.cells;
return newBoard;
}
getCell(x: number, y: number): CellModel {
return this.cells[y][x];
}
addFigures() {
this.cells.forEach((row, rowIndex) => {
row.forEach((cell, cellIndex) => {
if (rowIndex <= 2 && cell.label === Labels.Dark) {
new FigureModel(Labels.Dark, this.getCell(cellIndex, rowIndex)); // add dark pieces to first 3 rows
} else if (rowIndex >= this.cells.length - 3 && cell.label === Labels.Dark) {
new FigureModel(Labels.Light, this.getCell(cellIndex, rowIndex)); // add light pieces to last 3 rows
}
});
});
}
}
In Board
component we just need to create an update function and get new BoardModel
and call it in highlightCells
(onSetBoard
we take as a prop from App.tsx
):
// src/components/Board/Board.tsx
const updateBoard = () => {
const updatedBoard = board.getNewBoard();
onSetBoard(updatedBoard);
};
const highlightCells = () => {
board.highlightCells(selected);
updateBoard();
};
Now highlighting will work as components will be re-rendered on every cell selection.
The last step is to move our figure with our moveFigure
function from CellModel
. We will rename handleFigureClick
to handleCellClick
and update its logic. It will check if we selected cell in component state and the new target cell is not the same as selected and our selected figure can move to the target cell - then call moveFigure
. And clear selectedCell
in state and update the Board:
// src/components/Board/Board.tsx
const handleCellClick = (cell: CellModel) => {
if (selected && selected !== cell && selected.figure?.canMove(cell)) {
selected.moveFigure(cell);
setSelected(null);
updateBoard();
} else {
setSelected(cell);
}
};
Full Board.tsx:
// src/components/Board/Board.tsx
export const Board = ({ board, onSetBoard }: BoardProps): ReactElement => {
const [selected, setSelected] = useState<CellModel | null>(null);
const updateBoard = () => {
const updatedBoard = board.getNewBoard();
onSetBoard(updatedBoard);
};
const highlightCells = () => {
board.highlightCells(selected);
updateBoard();
};
const handleCellClick = (cell: CellModel) => {
if (selected && selected !== cell && selected.figure?.canMove(cell)) {
selected.moveFigure(cell);
setSelected(null);
updateBoard();
} else {
setSelected(cell);
}
};
useEffect(() => {
highlightCells();
}, [selected]);
return (
<div className="board">
{board.cells.map((row, rowIndex) => (
<Fragment key={rowIndex}>
{row.map((cell, cellIndex) => (
<Cell
cell={cell}
key={cell.key}
rowIndex={rowIndex}
cellIndex={cellIndex}
selected={selected?.x === cell.x && selected.y === cell.y} // check if selected cell coords equal to rendered cell
onCellClick={handleCellClick}
/>
))}
</Fragment>
))}
</div>
);
};
That’s all. Our base movement logic is ready. We can select a figure and move it on any empty cell: