Previous parts: Part 1, Part 2, Part 3
Hi everybody. In the previous part, we’ve prepared logic which gives us the ability to create figures. So right now we can use it to fill a game board. How can we do this? We know that in Checkers (standard variant) we have 12 figures for every player and every figure is placed on the dark cell. This knowledge is enough to fill the board.
Lets update method addFigure
to addFigures
:
// src/models/BoardModel
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
}
});
});
}
We iterate through our cells (which is an array of arrays) and check the row index and cell label. We need dark cells and the first 3 rows for one side and the last 3 rows for another side of the board. And than in App.tsx
we use this function.
Final result with the filled game board:
The game board is ready and we can start to create game logic. But before this, I want to update a little bit of the game board design and add labels. In chess and checkers, rows are labelled from 1 to 8 and columns from A to H. I want to add such labels, this will help us with more easiest visualization.
I decided to use the next approach - in Board.tsx
, when we render cells, we know rows and cell indexes. Let’s pass them to the Cell component as props:
// src/components/Board/Board.tsx
{board.cells.map((row, rowIndex) => (
<Fragment key={rowIndex}>
{row.map((cell, cellIndex) => (
<Cell
cell={cell}
key={cell.key}
rowIndex={rowIndex}
cellIndex={cellIndex}
/>
))}
</Fragment>
))}
Now when the Cell component is rendered we need to check indexes and add additional divs to all cells in the first and the last row, and for all cells in the first and last column. Also let’s add additional classes based on cell position, to understand where this label will be: at the top, bottom, left or right part of the board:
// src/components/Cell/Cell.tsx
{(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>
)}
Letters for labels I added to new enum:
// src/models/Letters.ts
export enum Letters {
A,
B,
C,
D,
E,
F,
G,
H,
}
And the last step is to add styles to knew classes, and position: relative to base .cell class:
// src/components/Cell/Cell.css
.board-label {
position: absolute;
}
.top {
transform: translateY(-50px);
}
.bottom {
transform: translateY(50px);
}
.left {
transform: translateX(-50px);
}
.right {
transform: translateX(50px);
}
After all these steps everything should work as expected and we will see labels around the game board:
So, it seems that the base design is complete, and we can start to create game logic. I want to start with figure selection.
In Board.tsx
we will create the component state. It will be used to save selected Cells. And a handler will check if cell have figure, than it will be saved to state:
// src/components/Board/Board.tsx
const [selected, setSelected] = useState<CellModel>();
const handleFigureClick = (cell: CellModel) => {
if (cell.figure) {
setSelected(cell);
}
};
Than we will pass handleFigureClick
and selected
to cells.
Full Board.tsx
// src/components/Board.tsx
export const Board = ({ board }: BoardProps): ReactElement => {
const [selected, setSelected] = useState<CellModel>();
const handleFigureClick = (cell: CellModel) => {
if (cell.figure) {
setSelected(cell);
}
};
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
onFigureClick={handleFigureClick}
/>
))}
</Fragment>
))}
</div>
);
};
Cell component will take these props and set handler to img
element. Also, we will check if Cell
is selected, then add selected
class to the element.
Full Cell.tsx
component:
// src/components/Cell/Cell.tsx
type CellProps = {
cell: CellModel;
rowIndex: number;
cellIndex: number;
selected: boolean;
onFigureClick: (cell: CellModel) => void;
};
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' : '')}>
{figure?.imageSrc && (
<img
className="icon"
src={figure.imageSrc}
alt={figure.name}
onClick={handleFigureClick}
/>
)}
{(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>
);
};
In Cell styles I want to add some animation to animate selected figure and add cursor: pointer
to icon class:
// src/components/Cell/Cell.css
.icon {
width: 64px;
height: 64px;
cursor: pointer;
}
.selected .icon {
animation: scaling 0.5s infinite alternate;
}
@keyframes scaling {
0% {
transform: scale(1);
}
100% {
transform: scale(1.3);
}
}
Now when we select the figure - it will be scaling a little bit: