React(Redux Toolkit, Typescript and Tailwind CSS)の構成でアプリを作成をしました【Candy Crush】

## 環境の準備
①ターミナルでreactアプリケーションを作成する。

```
npx create-react-app@latest <プロジェクト名> --template typescript
cd <プロジェクト名>
yarn start
```

② 必要な環境を整える。

[ESLintとPrettierとを基本からまとめてみた【React＋TypeScriptのアプリにESLintとPrettierを導入】](https://qiita.com/kanfutrooper/items/afd44e1a339ff6963ebf)

```
npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
```
npm i @reduxjs/toolkit
npm i react-redux

```

③不要なファイルを削除する。

```
App.test.ts
logo.svg
reportWebVitals.ts
setupTests.ts
```

## コンポーネント・ファイル構成

```
  src
   ├─ assets //画像を8個入れる
   ├─ components
         ├── Board.tsx
         └── Tile.tsx
   ├─ store
        ├─ reducers
              ├── hooks.ts
              └── moveBelow.ts
        ├── hooks.ts
        └── index.ts
   ├─ utils
         ├── Movie.tsx
         ├── Movie.tsx
         ├── formulas.ts
         └── moveCheckLogic.ts
   ├── App.tsx
   ├── index.css
   └── index.tsx
├── tailwind.config.ts
```
```src/components/Board.tsx
import { useAppSelector } from "../store/hooks";
import Tile from "./Tile";

function Board() {
  const board: string[] = useAppSelector(({ candyCrush: { board } }) => board);
  const boardSize: number = useAppSelector(
    ({ candyCrush: { boardSize } }) => boardSize
  );
  return (
    <div
      className="flex flex-wrap rounded-lg"
      style={{
        width: `${6.25 * boardSize}rem`,
      }}
    >
      {board.map((candy: string, index: number) => (
        <Tile candy={candy} key={index} candyId={index} />
      ))}
    </div>
  );
}

export default Board;
```
```src/components/Tile.tsx 
import { dragDrop, dragEnd, dragStart } from "../store";
import { useAppDispatch } from "../store/hooks";

function Tile({ candy, candyId }: { candy: string; candyId: number }) {
  const dispatch = useAppDispatch();

  return (
    <div
      className="h-24 w-24 flex justify-center items-center m-0.5 rounded-lg select-none"
      style={{
        boxShadow: "inset 5px 5px 15px #062525,inset -5px -5px 15px #aaaab7bb",
      }}
    >
      {candy && (
        <img
          src={candy}
          alt=""
          className="h-20 w-20"
          draggable={true}
          onDragStart={(e) => dispatch(dragStart(e.target))}
          onDragOver={(e) => e.preventDefault()}
          onDragEnter={(e) => e.preventDefault()}
          onDragLeave={(e) => e.preventDefault()}
          onDrop={(e) => dispatch(dragDrop(e.target))}
          onDragEnd={() => dispatch(dragEnd())}
          candy-id={candyId}
        />
      )}
    </div>
  );
}

export default Tile;
```
```src/store/reducers/dragEnd.ts
import { WritableDraft } from "immer/dist/types/types-external";
import {
  formulaForColumnOfFour,
  formulaForColumnOfThree,
  generateInvalidMoves,
} from "../../utils/formulas";
import {
  checkForColumnOfThree,
  checkForRowOfFour,
  checkForRowOfThree,
  isColumnOfFour,
} from "../../utils/moveCheckLogic";

export const dragEndReducer = (
  state: WritableDraft<{
    board: string[];
    boardSize: number;
    squareBeingReplaced: Element | undefined;
    squareBeingDragged: Element | undefined;
  }>
) => {
  const newBoard = [...state.board];
  let { boardSize, squareBeingDragged, squareBeingReplaced } = state;
  const squareBeingDraggedId: number = parseInt(
    squareBeingDragged?.getAttribute("candy-id") as string
  );
  const squareBeingReplacedId: number = parseInt(
    squareBeingReplaced?.getAttribute("candy-id") as string
  );

  newBoard[squareBeingReplacedId] = squareBeingDragged?.getAttribute(
    "src"
  ) as string;
  newBoard[squareBeingDraggedId] = squareBeingReplaced?.getAttribute(
    "src"
  ) as string;

  const validMoves: number[] = [
    squareBeingDraggedId - 1,
    squareBeingDraggedId - boardSize,
    squareBeingDraggedId + 1,
    squareBeingDraggedId + boardSize,
  ];

  const validMove: boolean = validMoves.includes(squareBeingReplacedId);

  const isAColumnOfFour: boolean | undefined = isColumnOfFour(
    newBoard,
    boardSize,
    formulaForColumnOfFour(boardSize)
  );

  const isARowOfFour: boolean | undefined = checkForRowOfFour(
    newBoard,
    boardSize,
    generateInvalidMoves(boardSize, true)
  );

  const isAColumnOfThree: boolean | undefined = checkForColumnOfThree(
    newBoard,
    boardSize,
    formulaForColumnOfThree(boardSize)
  );

  const isARowOfThree: boolean | undefined = checkForRowOfThree(
    newBoard,
    boardSize,
    generateInvalidMoves(boardSize)
  );

  if (
    squareBeingReplacedId &&
    validMove &&
    (isARowOfThree || isARowOfFour || isAColumnOfFour || isAColumnOfThree)
  ) {
    squareBeingDragged = undefined;
    squareBeingReplaced = undefined;
  } else {
    newBoard[squareBeingReplacedId] = squareBeingReplaced?.getAttribute(
      "src"
    ) as string;
    newBoard[squareBeingDraggedId] = squareBeingDragged?.getAttribute(
      "src"
    ) as string;
  }
  state.board = newBoard;
};
```
```src/store/reducers/moveBelow.ts
import { candies } from "../../utils/candyData";
import { formulaForMoveBelow } from "../../utils/formulas";
import { WritableDraft } from "immer/dist/types/types-external";

export const moveBelowReducer = (
  state: WritableDraft<{
    board: string[];
    boardSize: number;
    squareBeingReplaced: Element | undefined;
    squareBeingDragged: Element | undefined;
  }>
) => {
  const newBoard: string[] = [...state.board];
  const { boardSize } = state;
  let boardChanges: boolean = false;
  const formulaForMove: number = formulaForMoveBelow(boardSize);
  for (let i = 0; i <= formulaForMove; i++) {
    const firstRow = Array(boardSize)
      .fill(0)
      .map((_value: number, index: number) => index);

    const isFirstRow = firstRow.includes(i);

    if (isFirstRow && newBoard[i] === "") {
      let randomNumber = Math.floor(Math.random() * candies.length);
      newBoard[i] = candies[randomNumber];
      boardChanges = true;
    }

    if (newBoard[i + boardSize] === "") {
      newBoard[i + boardSize] = newBoard[i];
      newBoard[i] = "";
      boardChanges = true;
    }
    if (boardChanges) state.board = newBoard;
  }
};
```
```src/store/hooks.ts 
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux/es/exports";
import { TypedUseSelectorHook } from "react-redux/es/types";
import { AppDispatch, RootState } from "./index";

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```
```src/store/index.ts
import { createSlice, configureStore, PayloadAction } from "@reduxjs/toolkit";
import { dragEndReducer } from "./reducers/dragEnd";
import { moveBelowReducer } from "./reducers/moveBelow";

const initialState: {
  board: string[];
  boardSize: number;
  squareBeingReplaced: Element | undefined;
  squareBeingDragged: Element | undefined;
} = {
  board: [],
  boardSize: 8,
  squareBeingDragged: undefined,
  squareBeingReplaced: undefined,
};

const candyCrushSlice = createSlice({
  name: "candyCrush",
  initialState,
  reducers: {
    updateBoard: (state, action: PayloadAction<string[]>) => {
      state.board = action.payload;
    },
    dragStart: (state, action: PayloadAction<any>) => {
      state.squareBeingDragged = action.payload;
    },
    dragDrop: (state, action: PayloadAction<any>) => {
      state.squareBeingReplaced = action.payload;
    },
    dragEnd: dragEndReducer,
    moveBelow: moveBelowReducer,
  },
});

export const store = configureStore({
  reducer: {
    candyCrush: candyCrushSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
});

export const { updateBoard, moveBelow, dragDrop, dragEnd, dragStart } =
  candyCrushSlice.actions;

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```
```src/utils/candyData.ts
import candyOne from "../assets/candyOne.png";
import candyTwo from "../assets/candyTwo.png";
import candyThree from "../assets/candyThree.png";
import candyFour from "../assets/candyFour.png";
import candyFive from "../assets/candyFive.png";
import candySix from "../assets/candySix.png";
import candySeven from "../assets/candySeven.png";

export const candies = [
  candyOne,
  candyTwo,
  candyThree,
  candyFour,
  candyFive,
  candySix,
  candySeven,
];
```
```src/utils/createBoard.ts
import { candies } from "./candyData";

export const createBoard = (baordSize: number = 8) =>
  Array(baordSize * baordSize)
    .fill(null)
    .map(() => candies[Math.floor(Math.random() * candies.length)]);
```
```src/utils/formulas.ts
export const formulaForColumnOfFour = (boardSize: number) =>
  boardSize * boardSize - (boardSize + boardSize + boardSize) - 1;

export const formulaForColumnOfThree = (boardSize: number) =>
  boardSize * boardSize - (boardSize + boardSize) - 1;

export const formulaForMoveBelow = (boardSize: number) =>
  boardSize * boardSize - boardSize - 1;

export const generateInvalidMoves = (
  boardSize: number,
  isFour: boolean = false
) => {
  const invalidMoves: Array<number> = [];
  for (let i: number = boardSize; i <= boardSize * boardSize; i += boardSize) {
    if (isFour) invalidMoves.push(i - 3);
    invalidMoves.push(i - 2);
    invalidMoves.push(i - 1);
  }
  return invalidMoves;
};
```
```src/utils/moveCheckLogic.ts
export const isColumnOfFour = (
  newBoard: string[],
  boardSize: number,
  formulaForColumnOfFour: number
) => {
  for (let i: number = 0; i <= formulaForColumnOfFour; i++) {
    const columnOfFour: number[] = [
      i,
      i + boardSize,
      i + boardSize * 2,
      i + boardSize * 3,
    ];
    const decidedColor: string = newBoard[i];

    const isBlank: boolean = newBoard[i] === "";

    if (
      columnOfFour.every(
        (square: number) => newBoard[square] === decidedColor && !isBlank
      )
    ) {
      columnOfFour.forEach((square: number) => (newBoard[square] = ""));
      return true;
    }
  }
};

export const checkForRowOfFour = (
  newBoard: String[],
  boardSize: number,
  invalidMovesForColumnOfFour: number[]
) => {
  for (let i = 0; i < boardSize * boardSize; i++) {
    const rowOfFour = [i, i + 1, i + 2, i + 3];
    const decidedColor = newBoard[i];

    const isBlank = newBoard[i] === "";

    if (invalidMovesForColumnOfFour.includes(i)) continue;
    if (
      rowOfFour.every((square) => newBoard[square] === decidedColor && !isBlank)
    ) {
      rowOfFour.forEach((square) => (newBoard[square] = ""));
      return true;
    }
  }
};

export const checkForColumnOfThree = (
  newBoard: String[],
  boardSize: number,
  formulaForColumnOfThree: number
) => {
  for (let i = 0; i <= formulaForColumnOfThree; i++) {
    const columnOfThree = [i, i + boardSize, i + boardSize * 2];
    const decidedColor = newBoard[i];
    const isBlank = newBoard[i] === "";

    if (
      columnOfThree.every(
        (square) => newBoard[square] === decidedColor && !isBlank
      )
    ) {
      columnOfThree.forEach((square) => (newBoard[square] = ""));
      return true;
    }
  }
};

export const checkForRowOfThree = (
  newBoard: string[],
  boardSize: number,
  invalidMovesForColumnOfThree: number[]
) => {
  for (let i = 0; i < boardSize * boardSize; i++) {
    const rowOfThree = [i, i + 1, i + 2];
    const decidedColor = newBoard[i];

    const isBlank = newBoard[i] === "";

    if (invalidMovesForColumnOfThree.includes(i)) continue;

    if (
      rowOfThree.every(
        (square) => newBoard[square] === decidedColor && !isBlank
      )
    ) {
      rowOfThree.forEach((square) => (newBoard[square] = ""));
      return true;
    }
  }
};
```


```src/App.tsx
import { useEffect } from "react";
import Board from "./components/Board";
import { moveBelow, updateBoard } from "./store";
import { useAppDispatch, useAppSelector } from "./store/hooks";
import { createBoard } from "./utils/createBoard";
import {
  formulaForColumnOfFour,
  formulaForColumnOfThree,
  generateInvalidMoves,
} from "./utils/formulas";
import {
  checkForColumnOfThree,
  checkForRowOfFour,
  checkForRowOfThree,
  isColumnOfFour,
} from "./utils/moveCheckLogic";

function App() {
  const dispatch = useAppDispatch();
  const board = useAppSelector(({ candyCrush: { board } }) => board);
  const boardSize = useAppSelector(
    ({ candyCrush: { boardSize } }) => boardSize
  );

  useEffect(() => {
    dispatch(updateBoard(createBoard(boardSize)));
  }, [dispatch, boardSize]);

  useEffect(() => {
    const timeout = setTimeout(() => {
      const newBoard = [...board];
      isColumnOfFour(newBoard, boardSize, formulaForColumnOfFour(boardSize));
      checkForRowOfFour(
        newBoard,
        boardSize,
        generateInvalidMoves(boardSize, true)
      );
      checkForColumnOfThree(
        newBoard,
        boardSize,
        formulaForColumnOfThree(boardSize)
      );
      checkForRowOfThree(newBoard, boardSize, generateInvalidMoves(boardSize));
      dispatch(updateBoard(newBoard));
      dispatch(moveBelow());
    }, 150);
    return () => clearInterval(timeout);
  }, [board, dispatch, boardSize]);

  return (
    <div className="flex items-center justify-center h-screen">
      <Board />
    </div>
  );
}

export default App;
```

```src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  background-image: url("./assets/background.png");
  background-size: cover;
}
```
```src/index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { store } from "./store";
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
```








## 参考サイト
[Candy Crush using React, Redux Toolkit, Typescript and Tailwind CSS | React Projects | React Game](https://www.youtube.com/watch?v=tfV421C2r2A)
[Webの素材屋「カラフル」
](http://sozai.zoku-sei.com/%E3%83%9C%E3%82%BF%E3%83%B3-%E3%82%AF%E3%83%AA%E3%82%A2%E3%83%BB%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E5%BD%A2-/%E7%84%A1%E5%9C%B0%E6%AD%A3%E6%96%B9%E5%BD%A2%E3%82%AF%E3%83%AA%E3%82%A2%E3%83%9C%E3%82%BF%E3%83%B3%E7%B4%A0%E6%9D%90%E3%82%BB%E3%83%83%E3%83%88)