add OPTIONS page, add size, difficult level mode
This commit is contained in:
parent
1a778e680d
commit
e2c70835e2
@ -4,4 +4,5 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
font-family: "Pacifico", sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import MatchGame from 'components/MatchGame/MatchGame';
|
import MatchGame from 'components/../pages/MatchGame/MatchGame';
|
||||||
import MatchMenu from 'components/MatchMenu/MatchMenu';
|
import MatchMenu from 'components/../pages/MatchMenu/MatchMenu';
|
||||||
import { useMatchGame } from 'context/MatchGameContext';
|
import { useMatchGame } from 'context/MatchGameContext';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Options from 'src/pages/Options/Options';
|
||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
const {currentMenuItem} = useMatchGame();
|
const {currentMenuItem} = useMatchGame();
|
||||||
@ -17,6 +18,9 @@ const AppRoutes = () => {
|
|||||||
case 'new-game':
|
case 'new-game':
|
||||||
return <MatchGame isNewGame={true}/>;
|
return <MatchGame isNewGame={true}/>;
|
||||||
|
|
||||||
|
case 'options':
|
||||||
|
return <Options/>
|
||||||
|
|
||||||
|
|
||||||
default: return <MatchMenu/>;
|
default: return <MatchMenu/>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,99 +0,0 @@
|
|||||||
import { useMatchGame } from 'context/MatchGameContext';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import Button from 'components/UI/Button/Button';
|
|
||||||
import Timer from 'components/UI/Timer/Timer';
|
|
||||||
|
|
||||||
import cls from './BottomPanel.module.scss';
|
|
||||||
|
|
||||||
type BottomPanelProps = {
|
|
||||||
startGame: () => void;
|
|
||||||
retryGame: () => void;
|
|
||||||
isFinishedGame?: boolean;
|
|
||||||
shuffleCards: () => void;
|
|
||||||
isNewGame: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PanelState = 'start' | 'retry';
|
|
||||||
|
|
||||||
const BottomPanel: React.FC<BottomPanelProps> = (props) => {
|
|
||||||
const [isTimerStarted, setIsTimerStarted] = useState(false);
|
|
||||||
const [isTimerStopped, setIsTimerStopped] = useState(false);
|
|
||||||
const [panelState, setPanelState] = useState<PanelState>('start');
|
|
||||||
const {gameTime, changeGameTime, setCurrentMenuItem, setIsGameStarted} = useMatchGame();
|
|
||||||
|
|
||||||
function onStartBtnClickHandler(event: React.MouseEvent<HTMLButtonElement>) {
|
|
||||||
setIsTimerStarted(true);
|
|
||||||
props.startGame();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.isFinishedGame) {
|
|
||||||
setIsTimerStopped(true);
|
|
||||||
localStorage.removeItem('match-game:cards');
|
|
||||||
}
|
|
||||||
setPanelState(props.isFinishedGame ? 'retry' : 'start');
|
|
||||||
}, [props.isFinishedGame])
|
|
||||||
|
|
||||||
function onRetryBtnClickHandler() {
|
|
||||||
setCurrentMenuItem?.('new-game');
|
|
||||||
|
|
||||||
setIsTimerStarted(false);
|
|
||||||
setIsTimerStopped(false);
|
|
||||||
setPanelState('start');
|
|
||||||
|
|
||||||
props.retryGame?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRetryStatePanel() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={goToMenu}>⇦</Button>
|
|
||||||
<Button onClick={onRetryBtnClickHandler}>RETRY</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onShuffleBtnClickHandler() {
|
|
||||||
props.shuffleCards?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToMenu() {
|
|
||||||
setIsGameStarted?.(false);
|
|
||||||
setCurrentMenuItem?.('menu');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStartStatePanel() {
|
|
||||||
return isTimerStarted
|
|
||||||
? <>
|
|
||||||
<Button onClick={goToMenu}>⇦</Button>
|
|
||||||
<Timer isStarted={isTimerStarted} startTime={gameTime || 0} isFinished={isTimerStopped} changeTime={changeGameTime}/>
|
|
||||||
</>
|
|
||||||
: <>
|
|
||||||
<Button onClick={goToMenu}>⇦</Button>
|
|
||||||
<Button onClick={onStartBtnClickHandler}>{props.isNewGame ? 'START' : 'Continue'}</Button>
|
|
||||||
{props.isNewGame && <Button onClick={onShuffleBtnClickHandler}>SHUFFLE</Button>}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPanelByState(state: PanelState) {
|
|
||||||
if (!state) {
|
|
||||||
console.error('Нет состояния у панели')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 'start': return renderStartStatePanel();
|
|
||||||
case 'retry': return renderRetryStatePanel();
|
|
||||||
default: return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cls.BottomPanel}>
|
|
||||||
{ renderPanelByState(panelState) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BottomPanel;
|
|
||||||
@ -115,7 +115,7 @@ const CardsField: React.FC<CardsFieldProps> = ({cards, isGameStarted = false, fi
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCardsState(prev => (prev.map(el => ({...el, disabled: !isGameStarted, toggled: !isGameStarted && isNewGame}))));
|
setCardsState(prev => (prev.map(el => ({...el, disabled: !isGameStarted, toggled: !isGameStarted && isNewGame}))));
|
||||||
if (isGameStarted) localStorage.setItem('match-game:cards', JSON.stringify(cardsState));
|
if (isGameStarted) localStorage.setItem('match-game:cards', JSON.stringify(cardsState));
|
||||||
}, [isGameStarted])
|
}, [isGameStarted, isNewGame])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNewGame) setCardsState(cards.map(el => createCard(el)))
|
if (isNewGame) setCardsState(cards.map(el => createCard(el)))
|
||||||
|
|||||||
@ -1,6 +1,29 @@
|
|||||||
.FinishedField {
|
.FinishedField {
|
||||||
|
background: #283593;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
max-width: 320px;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-family: 'Pacifico', sans-serif;
|
font-family: 'Pacifico', sans-serif;
|
||||||
|
height: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px 50px;
|
||||||
|
position: relative;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { DiffLevel, FieldSize } from 'src/pages/Options/Options';
|
||||||
import { getTimeString } from 'src/utils';
|
import { getTimeString } from 'src/utils';
|
||||||
|
|
||||||
import cls from './FinishedField.module.scss';
|
import cls from './FinishedField.module.scss';
|
||||||
@ -6,16 +7,18 @@ import cls from './FinishedField.module.scss';
|
|||||||
type FinishedFieldProps = {
|
type FinishedFieldProps = {
|
||||||
mistakesCount?: number;
|
mistakesCount?: number;
|
||||||
gameTime?: number;
|
gameTime?: number;
|
||||||
|
difficultyLevel?: number;
|
||||||
|
fieldSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FinishedField: React.FC<FinishedFieldProps> = ({mistakesCount, gameTime}) => {
|
const FinishedField: React.FC<FinishedFieldProps> = ({mistakesCount, gameTime, difficultyLevel, fieldSize}) => {
|
||||||
return (
|
return (
|
||||||
<div className={cls.FinishedField}>
|
<div className={cls.FinishedField}>
|
||||||
<h1>GAME IS FINISHED !</h1>
|
<h1>GAME IS FINISHED !</h1>
|
||||||
<p>Время: <span>{getTimeString(new Date(gameTime || 0))}</span></p>
|
<p>Время: <span>{getTimeString(new Date(gameTime || 0))}</span></p>
|
||||||
<p>Количество ошибок: <span>{mistakesCount}</span></p>
|
<p>Количество ошибок: <span>{mistakesCount}</span></p>
|
||||||
<p>Сложность: <span>difficultyLevel</span></p>
|
<p>Сложность: <span>{DiffLevel[difficultyLevel as any]}</span></p>
|
||||||
<p>Размер поля: <span>fieldSize</span></p>
|
<p>Размер поля: <span>{FieldSize[fieldSize as any]}</span></p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
.FieldFinish {
|
|
||||||
width: 500px;
|
|
||||||
height: 500px;
|
|
||||||
background: #283593;
|
|
||||||
position: relative;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 40px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 24px;
|
|
||||||
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import BottomPanel from 'components/BottomPanel/BottomPanel';
|
|
||||||
import CardsField from 'components/CardsField/CardsField';
|
|
||||||
import FinishedField from 'components/FinishedField/FinishedField';
|
|
||||||
import { cardImages } from 'src/constants';
|
|
||||||
import { randomizeElementInArray } from 'src/utils';
|
|
||||||
import { useMatchGame } from 'context/MatchGameContext';
|
|
||||||
|
|
||||||
import cls from './MatchGame.module.scss'
|
|
||||||
|
|
||||||
interface MatchGameProps {
|
|
||||||
isNewGame: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MatchGame: React.FC<MatchGameProps> = ({isNewGame}) => {
|
|
||||||
// TODO replace to CardsField
|
|
||||||
const [cards, setCards] = useState<any[]>(randomizeElementInArray([
|
|
||||||
{code: 'react', imageURL: cardImages.react},
|
|
||||||
{code: 'angular', imageURL: cardImages.angular},
|
|
||||||
{code: 'js', imageURL: cardImages.js},
|
|
||||||
{code: 'react', imageURL: cardImages.react},
|
|
||||||
{code: 'angular', imageURL: cardImages.angular},
|
|
||||||
{code: 'js', imageURL: cardImages.js},
|
|
||||||
]));
|
|
||||||
|
|
||||||
const {gameTime,
|
|
||||||
mistakesCount,
|
|
||||||
resetGameProgress,
|
|
||||||
setIsGameStarted,
|
|
||||||
setIsGameFinished,
|
|
||||||
isGameFinished,
|
|
||||||
isGameStarted
|
|
||||||
} = useMatchGame();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isNewGame) resetGameProgress?.();
|
|
||||||
}, [isNewGame])
|
|
||||||
|
|
||||||
function startGame() {
|
|
||||||
setIsGameStarted?.(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryGame() {
|
|
||||||
setIsGameFinished?.(false);
|
|
||||||
setIsGameStarted?.(false);
|
|
||||||
setCards(prev => randomizeElementInArray(prev.map(el => ({...el, disabled: true, isMatched: false, toggled: true}))))
|
|
||||||
|
|
||||||
resetGameProgress?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishGame() {
|
|
||||||
setIsGameFinished?.(true);
|
|
||||||
localStorage.removeItem('match-game:cards');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO replace to CardsField
|
|
||||||
function shuffleCards() {
|
|
||||||
setCards(randomizeElementInArray([...cards]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWrapperClassList() {
|
|
||||||
let classes = [cls.Field];
|
|
||||||
|
|
||||||
if (isGameFinished) classes = [cls.FieldFinish];
|
|
||||||
|
|
||||||
return classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={getWrapperClassList().join(' ')} style={{marginBottom: 20}}>
|
|
||||||
{
|
|
||||||
isGameFinished
|
|
||||||
? <FinishedField gameTime={gameTime} mistakesCount={mistakesCount}/>
|
|
||||||
: <CardsField cards={cards} isGameStarted={isGameStarted} finishGameCallback={finishGame} isNewGame={isNewGame}/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<BottomPanel startGame={startGame} isFinishedGame={isGameFinished} retryGame={retryGame} shuffleCards={shuffleCards} isNewGame={isNewGame}/>
|
|
||||||
</>
|
|
||||||
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MatchGame;
|
|
||||||
15
src/components/UI/BottomPanel/BottomPanel.tsx
Normal file
15
src/components/UI/BottomPanel/BottomPanel.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import cls from './BottomPanel.module.scss';
|
||||||
|
|
||||||
|
type BottomPanelProps = {}
|
||||||
|
|
||||||
|
const BottomPanel: React.FC<BottomPanelProps> = ({children}) => {
|
||||||
|
return (
|
||||||
|
<div className={cls.BottomPanel}>
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BottomPanel;
|
||||||
@ -2,4 +2,6 @@ export const cardImages = {
|
|||||||
react: 'https://w7.pngwing.com/pngs/79/518/png-transparent-js-react-js-logo-react-react-native-logos-icon-thumbnail.png',
|
react: 'https://w7.pngwing.com/pngs/79/518/png-transparent-js-react-js-logo-react-react-native-logos-icon-thumbnail.png',
|
||||||
js: 'https://www.freepnglogos.com/uploads/javascript-png/javascript-logo-transparent-logo-javascript-images-3.png',
|
js: 'https://www.freepnglogos.com/uploads/javascript-png/javascript-logo-transparent-logo-javascript-images-3.png',
|
||||||
angular: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1200px-Angular_full_color_logo.svg.png',
|
angular: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1200px-Angular_full_color_logo.svg.png',
|
||||||
|
typescript: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Typescript_logo_2020.svg/1024px-Typescript_logo_2020.svg.png',
|
||||||
|
webpack: 'https://cdn.iconscout.com/icon/free/png-256/webpack-3629741-3030792.png',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { getTimeString } from 'src/utils';
|
import { DiffLevel, FieldSize, OptionsType } from 'src/pages/Options/Options';
|
||||||
|
|
||||||
export type MenuItem = 'menu' | 'new-game' | 'continue-game' | 'options' | 'exit';
|
export type MenuItem = 'menu' | 'new-game' | 'continue-game' | 'options' | 'exit';
|
||||||
|
|
||||||
@ -15,6 +15,9 @@ interface MatchGameContextType {
|
|||||||
setIsGameStarted?: (status: boolean) => void;
|
setIsGameStarted?: (status: boolean) => void;
|
||||||
isGameFinished?: boolean;
|
isGameFinished?: boolean;
|
||||||
setIsGameFinished?: (status: boolean) => void;
|
setIsGameFinished?: (status: boolean) => void;
|
||||||
|
filedSize?: number;
|
||||||
|
diffLevel?: number;
|
||||||
|
saveOptions?: (options: OptionsType) => void;
|
||||||
}
|
}
|
||||||
const MatchGameContext = React.createContext<MatchGameContextType>({});
|
const MatchGameContext = React.createContext<MatchGameContextType>({});
|
||||||
export const useMatchGame = () => useContext(MatchGameContext);
|
export const useMatchGame = () => useContext(MatchGameContext);
|
||||||
@ -25,6 +28,8 @@ export const MatchGameProvider: React.FC = ({children}) => {
|
|||||||
const [currentMenuItem, setCurrentMenuItem] = useState<MenuItem>('menu');
|
const [currentMenuItem, setCurrentMenuItem] = useState<MenuItem>('menu');
|
||||||
const [isGameStarted, setIsGameStarted] = useState(false);
|
const [isGameStarted, setIsGameStarted] = useState(false);
|
||||||
const [isGameFinished, setIsGameFinished] = useState(false);
|
const [isGameFinished, setIsGameFinished] = useState(false);
|
||||||
|
const [filedSize, setFieldSize] = useState(FieldSize.Small);
|
||||||
|
const [diffLevel, setDiffLevel] = useState(DiffLevel.Easy);
|
||||||
|
|
||||||
const addMistakesCount = () => {
|
const addMistakesCount = () => {
|
||||||
return setMistakesCount(prev => prev + 1)
|
return setMistakesCount(prev => prev + 1)
|
||||||
@ -39,6 +44,12 @@ export const MatchGameProvider: React.FC = ({children}) => {
|
|||||||
setGameTime(0);
|
setGameTime(0);
|
||||||
setIsGameStarted(false);
|
setIsGameStarted(false);
|
||||||
setIsGameFinished(false);
|
setIsGameFinished(false);
|
||||||
|
localStorage.removeItem('match-game:cards');
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveOptions = (options: OptionsType) => {
|
||||||
|
setFieldSize(options.size);
|
||||||
|
setDiffLevel(options.diffLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,7 +64,10 @@ export const MatchGameProvider: React.FC = ({children}) => {
|
|||||||
isGameStarted,
|
isGameStarted,
|
||||||
setIsGameStarted,
|
setIsGameStarted,
|
||||||
isGameFinished,
|
isGameFinished,
|
||||||
setIsGameFinished
|
setIsGameFinished,
|
||||||
|
filedSize,
|
||||||
|
diffLevel,
|
||||||
|
saveOptions
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</MatchGameContext.Provider>
|
</MatchGameContext.Provider>
|
||||||
|
|||||||
129
src/pages/MatchGame/MatchGame.tsx
Normal file
129
src/pages/MatchGame/MatchGame.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import CardsField from 'components/CardsField/CardsField';
|
||||||
|
import FinishedField from 'components/FinishedField/FinishedField';
|
||||||
|
|
||||||
|
import BottomPanel from 'components/UI/BottomPanel/BottomPanel';
|
||||||
|
import Button from 'components/UI/Button/Button';
|
||||||
|
import Timer from 'components/UI/Timer/Timer';
|
||||||
|
import { useMatchGame } from 'context/MatchGameContext';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { createCards, randomizeElementInArray } from 'src/utils';
|
||||||
|
|
||||||
|
interface MatchGameProps {
|
||||||
|
isNewGame: boolean;
|
||||||
|
}
|
||||||
|
type PanelState = 'start' | 'retry';
|
||||||
|
|
||||||
|
const MatchGame: React.FC<MatchGameProps> = ({isNewGame}) => {
|
||||||
|
const {
|
||||||
|
gameTime,
|
||||||
|
mistakesCount,
|
||||||
|
resetGameProgress,
|
||||||
|
setIsGameStarted,
|
||||||
|
setIsGameFinished,
|
||||||
|
isGameFinished,
|
||||||
|
isGameStarted,
|
||||||
|
changeGameTime,
|
||||||
|
setCurrentMenuItem,
|
||||||
|
diffLevel,
|
||||||
|
filedSize
|
||||||
|
} = useMatchGame();
|
||||||
|
|
||||||
|
// TODO replace to CardsField
|
||||||
|
const [cards, setCards] = useState<any[]>(randomizeElementInArray(createCards(diffLevel, filedSize)));
|
||||||
|
const [isTimerStarted, setIsTimerStarted] = useState(false);
|
||||||
|
const [isTimerStopped, setIsTimerStopped] = useState(false);
|
||||||
|
const [panelState, setPanelState] = useState<PanelState>('start');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNewGame) resetGameProgress?.();
|
||||||
|
}, [isNewGame, resetGameProgress])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isGameFinished) {
|
||||||
|
setIsTimerStopped(true);
|
||||||
|
localStorage.removeItem('match-game:cards');
|
||||||
|
}
|
||||||
|
setPanelState(isGameFinished ? 'retry' : 'start');
|
||||||
|
}, [isGameFinished])
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
setIsGameStarted?.(true);
|
||||||
|
setIsTimerStarted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryGame() {
|
||||||
|
setIsGameFinished?.(false);
|
||||||
|
setIsGameStarted?.(false);
|
||||||
|
setIsTimerStarted?.(false);
|
||||||
|
setCards(prev => randomizeElementInArray(prev.map(el => ({...el, disabled: true, isMatched: false, toggled: true}))))
|
||||||
|
|
||||||
|
resetGameProgress?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishGame() {
|
||||||
|
setIsGameFinished?.(true);
|
||||||
|
localStorage.removeItem('match-game:cards');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shuffleCards() {
|
||||||
|
setCards(randomizeElementInArray([...cards]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToMenu() {
|
||||||
|
setIsGameStarted?.(false);
|
||||||
|
setCurrentMenuItem?.('menu');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRetryStatePanel() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={goToMenu}>⇦</Button>
|
||||||
|
<Button onClick={retryGame}>RETRY</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStartStatePanel() {
|
||||||
|
return isTimerStarted
|
||||||
|
? <>
|
||||||
|
<Button onClick={goToMenu}>⇦</Button>
|
||||||
|
<Timer isStarted={isTimerStarted} startTime={gameTime || 0} isFinished={isTimerStopped} changeTime={changeGameTime}/>
|
||||||
|
</>
|
||||||
|
: <>
|
||||||
|
<Button onClick={goToMenu}>⇦</Button>
|
||||||
|
<Button onClick={startGame}>{isNewGame ? 'START' : 'Continue'}</Button>
|
||||||
|
{isNewGame && <Button onClick={shuffleCards}>SHUFFLE</Button>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPanelByState(state: PanelState) {
|
||||||
|
if (!state) {
|
||||||
|
console.error('Нет состояния у панели')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'start': return renderStartStatePanel();
|
||||||
|
case 'retry': return renderRetryStatePanel();
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{marginBottom: 20}}>
|
||||||
|
{
|
||||||
|
isGameFinished
|
||||||
|
? <FinishedField gameTime={gameTime} mistakesCount={mistakesCount} difficultyLevel={diffLevel} fieldSize={filedSize}/>
|
||||||
|
: <CardsField cards={cards} isGameStarted={isGameStarted} finishGameCallback={finishGame} isNewGame={isNewGame}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<BottomPanel>
|
||||||
|
{ renderPanelByState(panelState) }
|
||||||
|
</BottomPanel>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MatchGame;
|
||||||
0
src/pages/MatchMenu/MatchMenu.module.scss
Normal file
0
src/pages/MatchMenu/MatchMenu.module.scss
Normal file
@ -18,6 +18,7 @@ const MatchMenu: React.FC<GameMenuProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<h2>Match game menu</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
canContinueGame() ? <li onClick={() => clickItemHandler('continue-game')}>Continue</li> : null
|
canContinueGame() ? <li onClick={() => clickItemHandler('continue-game')}>Continue</li> : null
|
||||||
28
src/pages/Options/Option.tsx
Normal file
28
src/pages/Options/Option.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Button from 'components/UI/Button/Button';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import cls from './Options.module.scss'
|
||||||
|
|
||||||
|
interface OptionProps {
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (type: 'next' | 'prev') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Option:React.FC<OptionProps> = ({value, text, onChange}) => {
|
||||||
|
return (
|
||||||
|
<div className={cls.Option}>
|
||||||
|
<div>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
<div className={cls.selectGroup}>
|
||||||
|
<Button className={cls.button} onClick={() => onChange('prev')}>❮</Button>
|
||||||
|
<span>{value}</span>
|
||||||
|
<Button className={cls.button} onClick={() => onChange('next')}>❯</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Option;
|
||||||
48
src/pages/Options/Options.module.scss
Normal file
48
src/pages/Options/Options.module.scss
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
.Options {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
background: #283593;
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 30px;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
margin: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectGroup {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
72
src/pages/Options/Options.tsx
Normal file
72
src/pages/Options/Options.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import BottomPanel from 'components/UI/BottomPanel/BottomPanel';
|
||||||
|
import Button from 'components/UI/Button/Button';
|
||||||
|
import { useMatchGame } from 'context/MatchGameContext';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import Option from 'src/pages/Options/Option';
|
||||||
|
|
||||||
|
import cls from './Options.module.scss'
|
||||||
|
|
||||||
|
export type OptionsType = {
|
||||||
|
diffLevel: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DiffLevel {
|
||||||
|
Easy,
|
||||||
|
Normal,
|
||||||
|
Hard
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FieldSize {
|
||||||
|
Small,
|
||||||
|
Normal,
|
||||||
|
Big
|
||||||
|
}
|
||||||
|
|
||||||
|
const Options: React.FC = () => {
|
||||||
|
const {saveOptions, setCurrentMenuItem, diffLevel, resetGameProgress, filedSize} = useMatchGame();
|
||||||
|
|
||||||
|
const [size, setSize] = useState(filedSize || FieldSize.Small);
|
||||||
|
const [difficultLevel, setDifficultLevel] = useState(diffLevel || DiffLevel.Easy);
|
||||||
|
|
||||||
|
function saveOptionHandler() {
|
||||||
|
saveOptions?.({
|
||||||
|
diffLevel: difficultLevel,
|
||||||
|
size
|
||||||
|
});
|
||||||
|
resetGameProgress?.();
|
||||||
|
setCurrentMenuItem?.('menu');
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeFieldSize(type: 'prev' | 'next') {
|
||||||
|
const size = Object.values(FieldSize).length/2;
|
||||||
|
|
||||||
|
if (type === 'next') setSize(prev => prev + 1 === size ? 0 : prev + 1);
|
||||||
|
if (type === 'prev') setSize(prev => prev === 0 ? size - 1 : prev - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDiffLevel(type: 'prev' | 'next') {
|
||||||
|
const size = Object.values(DiffLevel).length/2;
|
||||||
|
|
||||||
|
if (type === 'next') setDifficultLevel(prev => prev + 1 === size ? 0 : prev + 1);
|
||||||
|
if (type === 'prev') setDifficultLevel(prev => prev === 0 ? size - 1 : prev - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cls.Options}>
|
||||||
|
<h2>Options</h2>
|
||||||
|
<ul>
|
||||||
|
<li><Option text={'Size'} value={FieldSize[size]} onChange={changeFieldSize}/></li>
|
||||||
|
<li><Option text={'Difficult level'} value={DiffLevel[difficultLevel]} onChange={changeDiffLevel}/></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<BottomPanel>
|
||||||
|
<Button onClick={() => setCurrentMenuItem?.('menu')}>⇦</Button>
|
||||||
|
<Button onClick={saveOptionHandler}>Save</Button>
|
||||||
|
</BottomPanel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Options;
|
||||||
81
src/utils.ts
81
src/utils.ts
@ -1,5 +1,10 @@
|
|||||||
|
import { cardImages } from 'src/constants';
|
||||||
|
import { DiffLevel, FieldSize } from 'src/pages/Options/Options';
|
||||||
|
|
||||||
|
export type CardType = {code: string, imageURL: string};
|
||||||
|
|
||||||
export function randomizeElementInArray (arr: any[]) {
|
export function randomizeElementInArray (arr: any[]) {
|
||||||
return arr.sort(() => {
|
return [...arr].sort(() => {
|
||||||
return Math.random() > 0.5 ? 1 : -1;
|
return Math.random() > 0.5 ? 1 : -1;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -16,3 +21,77 @@ export function getTimeString(time?: Date): string {
|
|||||||
return `${minutes}:${seconds}`
|
return `${minutes}:${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCards(diffLevel: DiffLevel = DiffLevel.Easy, size: FieldSize = FieldSize.Small): CardType[] {
|
||||||
|
let arrSize = 6;
|
||||||
|
switch (diffLevel) {
|
||||||
|
case DiffLevel.Easy:
|
||||||
|
switch (size) {
|
||||||
|
case FieldSize.Small: {
|
||||||
|
arrSize = 6;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldSize.Normal:{
|
||||||
|
arrSize = 12;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldSize.Big:{
|
||||||
|
arrSize = 18;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array(arrSize).fill('').map((_, i) => {
|
||||||
|
if (i < arrSize / 3) return {code: 'js', imageURL: cardImages.js};
|
||||||
|
if (i >= arrSize / 3 && i < arrSize * 2/3) return {code: 'react', imageURL: cardImages.react};
|
||||||
|
return {code: 'angular', imageURL: cardImages.angular}
|
||||||
|
});
|
||||||
|
|
||||||
|
case DiffLevel.Normal:
|
||||||
|
switch (size) {
|
||||||
|
case FieldSize.Small: {
|
||||||
|
arrSize = 8;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldSize.Normal:{
|
||||||
|
arrSize = 16;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldSize.Big:{
|
||||||
|
arrSize = 24;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array(arrSize).fill('').map((_, i) => {
|
||||||
|
if (i < arrSize / 4) return {code: 'js', imageURL: cardImages.js};
|
||||||
|
if (i >= arrSize / 4 && i < arrSize / 2) return {code: 'react', imageURL: cardImages.react};
|
||||||
|
if (i >= arrSize / 2 && i < arrSize * 3/4) return {code: 'angular', imageURL: cardImages.angular};
|
||||||
|
return {code: 'typescript', imageURL: cardImages.typescript}
|
||||||
|
});
|
||||||
|
|
||||||
|
case DiffLevel.Hard:
|
||||||
|
switch (size) {
|
||||||
|
case FieldSize.Small: {
|
||||||
|
arrSize = 10;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldSize.Normal:{
|
||||||
|
arrSize = 20;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldSize.Big:{
|
||||||
|
arrSize = 30;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array(arrSize).fill('').map((_, i) => {
|
||||||
|
if (i < arrSize / 5) return {code: 'js', imageURL: cardImages.js};
|
||||||
|
if (i >= arrSize / 5 && i < arrSize * 2/5) return {code: 'react', imageURL: cardImages.react};
|
||||||
|
if (i >= arrSize * 2/5 && i < arrSize * 3/5) return {code: 'angular', imageURL: cardImages.angular};
|
||||||
|
if (i >= arrSize * 3/5 && i < arrSize * 4/5) return {code: 'typescript', imageURL: cardImages.typescript};
|
||||||
|
return {code: 'webpack', imageURL: cardImages.webpack}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array(6).fill({code: 'angular', imageURL: cardImages.angular});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user