diff --git a/.eslintrc.json b/.eslintrc.json index e37e1e072..df89451f1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,4 +7,4 @@ "eqeqeq": ["error", "always"], "no-unused-vars": ["error"] } -} +} \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index 65e18f5ff..99cd00a0a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -7,4 +7,4 @@ module.exports = { bracketSpacing: true, arrowParens: "avoid", htmlWhitespaceSensitivity: "ignore", -}; +}; \ No newline at end of file diff --git a/docs/mvp-spec.md b/docs/mvp-spec.md index fab47685e..8b695eb9c 100644 --- a/docs/mvp-spec.md +++ b/docs/mvp-spec.md @@ -4,7 +4,7 @@ [Ссылка на макет (страница «Инструменты разработки»).](https://www.figma.com/file/Xk8ocvZA9NlMmA0szZeI5h/%D0%B1%D0%B0%D0%B7%D0%BE%D0%B2%D1%8B%D0%B9-JS?node-id=4325%3A2) -## Описание игры +## Описание игры 1 Вам предстоит реализовать следующий функционал: выбор сложности, основную логику игры, вывод результата. Ниже вы найдёте подробное описание каждого пункта. @@ -14,9 +14,10 @@ Количество карточек для каждого уровня сложности можете назначать и свои или выбрать готовый пресет. Предлагаем следующее пресеты: - - Легкий уровень - 6 карточек (3 пары) - - Средний уровень - 12 карточек (6 пар) - - Сложный уровень - 18 карточек (9 пар) + +- Легкий уровень - 6 карточек (3 пары) +- Средний уровень - 12 карточек (6 пар) +- Сложный уровень - 18 карточек (9 пар) Как только уровень сложности выбран, игроку показывается на игровой поле. diff --git a/lefthook.yml b/lefthook.yml index 87752f862..bf049dd55 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -17,4 +17,4 @@ pre-commit: commands: eslint: glob: "*.{js,jsx}" - run: npm run lint + run: npm run lint \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index edaf5083f..dd3e2dd10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5961,9 +5961,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001522", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz", - "integrity": "sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==", + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "funding": [ { "type": "opencollective", @@ -22559,9 +22559,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001522", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz", - "integrity": "sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==" + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", diff --git a/public/Vector.svg b/public/Vector.svg new file mode 100644 index 000000000..dc94e72f4 --- /dev/null +++ b/public/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/manifest.json b/public/manifest.json index 3bd1fa1b8..3a670d468 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -22,4 +22,4 @@ "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" -} +} \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt index e9e57dc4d..5537f0739 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,3 +1,3 @@ # https://www.robotstxt.org/robotstxt.html User-agent: * -Disallow: +Disallow: \ No newline at end of file diff --git a/src/components/Button/Button.module.css b/src/components/Button/Button.module.css index 5d3f1f80e..132022b6f 100644 --- a/src/components/Button/Button.module.css +++ b/src/components/Button/Button.module.css @@ -23,4 +23,4 @@ .button:hover { background: #7ac100cc; -} +} \ No newline at end of file diff --git a/src/components/Card/Card.module.css b/src/components/Card/Card.module.css index 86c3fbb5b..9705b2737 100644 --- a/src/components/Card/Card.module.css +++ b/src/components/Card/Card.module.css @@ -114,4 +114,4 @@ .back { z-index: 2; transform: rotateY(180deg); -} +} \ No newline at end of file diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..e856fcabf 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -1,14 +1,18 @@ import { shuffle } from "lodash"; -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { generateDeck } from "../../utils/cards"; import styles from "./Cards.module.css"; import { EndGameModal } from "../../components/EndGameModal/EndGameModal"; import { Button } from "../../components/Button/Button"; import { Card } from "../../components/Card/Card"; +import { EasyContext } from "../../contexte/contexte"; +import { useNavigate } from "react-router-dom"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; const STATUS_WON = "STATUS_WON"; +// Пауза игры при допускании ошибки выбора карточки +const STATUS_PAUSED = "STATUS_PAUSED"; // Идет игра: карты закрыты, игрок может их открыть const STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS"; // Начало игры: игрок видит все карты в течении нескольких секунд @@ -41,8 +45,18 @@ function getTimerValue(startDate, endDate) { * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { + // Когда игра окончена, переход на главную страницу + const navigate = useNavigate(); + function goTo() { + navigate("/"); + } + + // Обработка количества попыток + const { tries, setTries, isEasyMode } = useContext(EasyContext); + // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); + // Текущий статус игры const [status, setStatus] = useState(STATUS_PREVIEW); @@ -57,16 +71,32 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { minutes: 0, }); - function finishGame(status = STATUS_LOST) { + // Если количество попыток равно 0 устанавливается стату проиграл и игра заканчивается + useEffect(() => { + if (tries === 0) { + finishGame(STATUS_LOST); + } + }, [tries]); + + function finishGame(status) { setGameEndDate(new Date()); setStatus(status); } + + function pausedGame(status = STATUS_PAUSED) { + setStatus(status); + } + function startGame() { const startDate = new Date(); setGameEndDate(null); setGameStartDate(startDate); setTimer(getTimerValue(startDate, null)); setStatus(STATUS_IN_PROGRESS); + // Добавлена проверка на включенный режим 3-х попыток + if (!isEasyMode) { + setTries(1); + } } function resetGame() { setGameStartDate(null); @@ -75,6 +105,24 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setStatus(STATUS_PREVIEW); } + function сontinueGame(status = STATUS_IN_PROGRESS) { + setStatus(status); + } + + // Функция запускает разные сценарии для кнопки в модальном окне + function whatsNext() { + if (status === STATUS_PAUSED) { + сontinueGame(STATUS_IN_PROGRESS); + } + if (status === STATUS_LOST) { + goTo(); + setTries(3); + } + if (status === STATUS_WON) { + resetGame(); + } + } + /** * Обработка основного действия в игре - открытие карты. * После открытия карты игра может пепереходит в следующие состояния @@ -123,18 +171,23 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { return false; }); - const playerLost = openCardsWithoutPair.length >= 2; + const havMistake = openCardsWithoutPair.length >= 2; - // "Игрок проиграл", т.к на поле есть две открытые карты без пары - if (playerLost) { - finishGame(STATUS_LOST); - return; + // Если на поле есть две открытые карты без пары, то игра паузится и уменьшается количество попыток + function minusTries() { + setTries(prev => prev - 1); + } + + // "Игрок допустил ошибку", т.к на поле есть две открытые карты без пары + if (havMistake) { + minusTries(); + pausedGame(STATUS_PAUSED); } // ... игра продолжается }; - const isGameEnded = status === STATUS_LOST || status === STATUS_WON; + const isGameEnded = status === STATUS_LOST || status === STATUS_WON || status === STATUS_PAUSED; // Игровой цикл useEffect(() => { @@ -195,7 +248,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { )} - {status === STATUS_IN_PROGRESS ? : null} +
+ {isEasyMode && status === STATUS_IN_PROGRESS && ( + Осталось {tries} попытки! + )} + {status === STATUS_IN_PROGRESS ? : null} +
@@ -206,6 +264,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { open={status !== STATUS_IN_PROGRESS ? true : card.open} suit={card.suit} rank={card.rank} + status={STATUS_IN_PROGRESS} /> ))}
@@ -213,10 +272,11 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { {isGameEnded ? (
) : null} diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..21bf750aa 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -70,3 +70,20 @@ margin-bottom: -12px; } + +.attempt { + color: #fff; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 32px; +} + +.buttonContainer { + display: flex; + flex-direction: column; + align-items: flex-end; + row-gap: 12px; +} \ No newline at end of file diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..f5f71385d 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -5,23 +5,62 @@ import { Button } from "../Button/Button"; import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; +export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick, tries }) { + let imgSrc; + let imgAlt; - const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; + if (isWon === "STATUS_PAUSED") { + isWon = "Вы допустили ошибку"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } + if (isWon === "STATUS_WON") { + isWon = "Вы победили!"; + imgSrc = celebrationImageUrl; + imgAlt = "celebration emodji"; + } + if (isWon === "STATUS_LOST") { + isWon = "Вы проиграли!"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } - const imgAlt = isWon ? "celebration emodji" : "dead emodji"; + if (tries === 0) { + isWon = "Вы проиграли!"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } + + const title = isWon; return (
{imgAlt}

{title}

-

Затраченное время:

-
- {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} -
+ {isWon === "Вы допустили ошибку" &&

Оставшеся количество попыток:

} + {isWon === "Вы победили!" &&

Затраченное время:

} + {isWon === "Вы проиграли!" &&

Затраченное время:

} + + {isWon === "Вы допустили ошибку" && ( +
+

{tries}

+
+ )} + {isWon === "Вы победили!" && ( +
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} +
+ )} + + {isWon === "Вы проиграли!" && ( +
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} +
+ )} - + {isWon === "Вы допустили ошибку" && } + {isWon === "Вы победили!" && } + {isWon === "Вы проиграли!" && }
); -} +} \ No newline at end of file diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css index 9368cb8b5..f250e3ed2 100644 --- a/src/components/EndGameModal/EndGameModal.module.css +++ b/src/components/EndGameModal/EndGameModal.module.css @@ -1,3 +1,4 @@ + .modal { width: 480px; height: 459px; @@ -48,4 +49,4 @@ line-height: 72px; margin-bottom: 40px; -} +} \ No newline at end of file diff --git a/src/contexte/contexte.jsx b/src/contexte/contexte.jsx new file mode 100644 index 000000000..ef3efe956 --- /dev/null +++ b/src/contexte/contexte.jsx @@ -0,0 +1,11 @@ + +import { createContext, useState } from "react"; + +export const EasyContext = createContext(false); + +export const EasyProvider = ({ children }) => { + const [tries, setTries] = useState(3); + const [isEasyMode, setIsEasyMode] = useState(false); + + return {children}; +}; diff --git a/src/index.css b/src/index.css index 78f0d3a2b..f8ab8c16e 100644 --- a/src/index.css +++ b/src/index.css @@ -34,4 +34,4 @@ ol { local("Arial"); font-weight: 400; font-style: normal; -} +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index f689c5f0b..d39f1dd9d 100644 --- a/src/index.js +++ b/src/index.js @@ -3,10 +3,13 @@ import ReactDOM from "react-dom/client"; import "./index.css"; import { RouterProvider } from "react-router-dom"; import { router } from "./router"; +import { EasyProvider } from "./contexte/contexte"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - + + + , ); diff --git a/src/pages/GamePage/GamePage.jsx b/src/pages/GamePage/GamePage.jsx index a4be871db..2cbbd0a15 100644 --- a/src/pages/GamePage/GamePage.jsx +++ b/src/pages/GamePage/GamePage.jsx @@ -10,4 +10,4 @@ export function GamePage() { ); -} +} \ No newline at end of file diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..95801c211 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,7 +1,11 @@ import { Link } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useContext } from "react"; +import { EasyContext } from "../../contexte/contexte"; export function SelectLevelPage() { + const { isEasyMode, setIsEasyMode } = useContext(EasyContext); + return (
@@ -23,7 +27,16 @@ export function SelectLevelPage() { +
+ + setIsEasyMode(e.target.checked)} + /> +
); -} +} \ No newline at end of file diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..59627c011 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -62,3 +62,30 @@ .levelLink:visited { color: #0080c1; } + +.isEasyMode { + display: flex; + align-items: center; + box-sizing: border-box; + padding: 8px 16px 8px 16px; + border-radius: 12px; + background-color: #fff; +} + +.isEasyModeTitle { + color: #0080c1; + text-align: center; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 48px; + display: flex; +} + +.checkbox { + margin-left: 16px; + width: 24px; + height: 24px; +} \ No newline at end of file diff --git a/src/router.js b/src/router.js index da6e94b51..a81f2bc1e 100644 --- a/src/router.js +++ b/src/router.js @@ -18,4 +18,4 @@ export const router = createBrowserRouter( * он же указан в homepage package.json и в index.html */ { basename: "/react-memo" }, -); +); \ No newline at end of file diff --git a/src/utils/cards.js b/src/utils/cards.js index 16fa6ccd9..bed790eb4 100644 --- a/src/utils/cards.js +++ b/src/utils/cards.js @@ -54,4 +54,4 @@ export function generateDeck(pairsCount = 3) { } return deck; -} +} \ No newline at end of file