Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
"eqeqeq": ["error", "always"],
"no-unused-vars": ["error"]
}
}
}
2 changes: 1 addition & 1 deletion .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ module.exports = {
bracketSpacing: true,
arrowParens: "avoid",
htmlWhitespaceSensitivity: "ignore",
};
};
9 changes: 5 additions & 4 deletions docs/mvp-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Вам предстоит реализовать следующий функционал: выбор сложности, основную логику игры, вывод результата. Ниже вы найдёте подробное описание каждого пункта.

Expand All @@ -14,9 +14,10 @@
Количество карточек для каждого уровня сложности можете назначать и свои или выбрать готовый пресет.

Предлагаем следующее пресеты:
- Легкий уровень - 6 карточек (3 пары)
- Средний уровень - 12 карточек (6 пар)
- Сложный уровень - 18 карточек (9 пар)

- Легкий уровень - 6 карточек (3 пары)
- Средний уровень - 12 карточек (6 пар)
- Сложный уровень - 18 карточек (9 пар)

Как только уровень сложности выбран, игроку показывается на игровой поле.

Expand Down
2 changes: 1 addition & 1 deletion lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ pre-commit:
commands:
eslint:
glob: "*.{js,jsx}"
run: npm run lint
run: npm run lint
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions public/Vector.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
}
2 changes: 1 addition & 1 deletion public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
Disallow:
2 changes: 1 addition & 1 deletion src/components/Button/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@

.button:hover {
background: #7ac100cc;
}
}
2 changes: 1 addition & 1 deletion src/components/Card/Card.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,4 @@
.back {
z-index: 2;
transform: rotateY(180deg);
}
}
82 changes: 71 additions & 11 deletions src/components/Cards/Cards.jsx
Original file line number Diff line number Diff line change
@@ -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";
// Начало игры: игрок видит все карты в течении нескольких секунд
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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();
}
}

/**
* Обработка основного действия в игре - открытие карты.
* После открытия карты игра может пепереходит в следующие состояния
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -195,7 +248,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
</>
)}
</div>
{status === STATUS_IN_PROGRESS ? <Button onClick={resetGame}>Начать заново</Button> : null}
<div className={styles.buttonContainer}>
{isEasyMode && status === STATUS_IN_PROGRESS && (
<span className={styles.attempt}>Осталось {tries} попытки!</span>
)}
{status === STATUS_IN_PROGRESS ? <Button onClick={resetGame}>Начать заново</Button> : null}
</div>
</div>

<div className={styles.cards}>
Expand All @@ -206,17 +264,19 @@ 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}
/>
))}
</div>

{isGameEnded ? (
<div className={styles.modalContainer}>
<EndGameModal
isWon={status === STATUS_WON}
isWon={status}
gameDurationSeconds={timer.seconds}
gameDurationMinutes={timer.minutes}
onClick={resetGame}
onClick={whatsNext}
tries={tries}
/>
</div>
) : null}
Expand Down
17 changes: 17 additions & 0 deletions src/components/Cards/Cards.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
59 changes: 49 additions & 10 deletions src/components/EndGameModal/EndGameModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={styles.modal}>
<img className={styles.image} src={imgSrc} alt={imgAlt} />
<h2 className={styles.title}>{title}</h2>
<p className={styles.description}>Затраченное время:</p>
<div className={styles.time}>
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
</div>
{isWon === "Вы допустили ошибку" && <p className={styles.description}>Оставшеся количество попыток:</p>}
{isWon === "Вы победили!" && <p className={styles.description}>Затраченное время:</p>}
{isWon === "Вы проиграли!" && <p className={styles.description}>Затраченное время:</p>}

{isWon === "Вы допустили ошибку" && (
<div className={styles.time}>
<p>{tries}</p>
</div>
)}
{isWon === "Вы победили!" && (
<div className={styles.time}>
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
</div>
)}

{isWon === "Вы проиграли!" && (
<div className={styles.time}>
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
</div>
)}

<Button onClick={onClick}>Начать сначала</Button>
{isWon === "Вы допустили ошибку" && <Button onClick={onClick}>Вернуться к игре</Button>}
{isWon === "Вы победили!" && <Button onClick={onClick}>Начать сначала</Button>}
{isWon === "Вы проиграли!" && <Button onClick={onClick}>Игра окончена</Button>}
</div>
);
}
}
3 changes: 2 additions & 1 deletion src/components/EndGameModal/EndGameModal.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

.modal {
width: 480px;
height: 459px;
Expand Down Expand Up @@ -48,4 +49,4 @@
line-height: 72px;

margin-bottom: 40px;
}
}
11 changes: 11 additions & 0 deletions src/contexte/contexte.jsx
Original file line number Diff line number Diff line change
@@ -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 <EasyContext.Provider value={{ tries, setTries, isEasyMode, setIsEasyMode }}>{children}</EasyContext.Provider>;
};
2 changes: 1 addition & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ ol {
local("Arial");
font-weight: 400;
font-style: normal;
}
}
Loading