LoginSignup
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-27

環境の準備

①ターミナルでreactアプリケーションを作成する。

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

② 必要な環境を整える。

ESLintとPrettierとを基本からまとめてみた【React+TypeScriptのアプリにESLintとPrettierを導入】

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
Webの素材屋「カラフル」

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1