Checkers on React - Part 4 - Figures, Selection

Written by rzhelieznov | Published 2022/08/30
Tech Story Tags: react | typescript | game-development | tutorial | learn-to-code | checkers-game | react-tutorial | programming

TLDRIn the previous part, we’ve prepared logic which gives us the ability to create figures. We know that in Checkers (standard variant) we have 12 figures for every player and every figure is placed on the dark cell. 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. The game board is ready and we can start to create game logic. We need dark cells and the first 3 rows for one side and the last 3 rows. And than in `App.tsx` we use this function.via the TL;DR App

Previous parts: Part 1Part 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:

Link to the repo


Written by rzhelieznov | Javascript and React fan. Also love reading, traveling, pc gaming :)
Published by HackerNoon on 2022/08/30