環境の準備
①ターミナルで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の素材屋「カラフル」