paint-brush
Checkers on React - Part 5 - Simple movementby@rzhelieznov
681 reads
681 reads

Checkers on React - Part 5 - Simple movement

by RomanSeptember 29th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this section, I want to create the ability to move figures. 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. The next step is to create method `moveFigure` in `CellModel. It will take `targetCell` as an argument. Then we check if our selected cell has a figure and this figure can be moved to a target cell - then we will save the figure to a new cell and clean our current cell:

Company Mentioned

Mention Thumbnail
featured image - Checkers on React - Part 5 - Simple movement
Roman HackerNoon profile picture

Previous parts: Part 1Part 2Part 3, Part 4


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:

Link to the repo