Skip to content
Open

0.2 #88

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
1 change: 1 addition & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ module.exports = {
bracketSpacing: true,
arrowParens: "avoid",
htmlWhitespaceSensitivity: "ignore",
endOfLine: "auto", // для своместной разработки и с linux и с windows
};
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Оценка времени выполнения работы "Внедрение лидерборда"

- **Ожидаемое время:** 6 часов
- **Фактическое время:** 5 часов

# MVP Карточная игра "Мемо"

В этом репозитории реализован MVP карточкой игры "Мемо" по [тех.заданию](./docs/mvp-spec.md)
Expand Down
167 changes: 109 additions & 58 deletions src/components/Cards/Cards.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
// src/components/Cards/Cards.jsx

import { shuffle } from "lodash";
import { useEffect, useState } from "react";
import { useEffect, useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
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 { GameModeContext } from "../../context/GameModeContext";

// Игра закончилась
// Константы статусов игры
const STATUS_LOST = "STATUS_LOST";
const STATUS_WON = "STATUS_WON";
// Идет игра: карты закрыты, игрок может их открыть
const STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS";
// Начало игры: игрок видит все карты в течении нескольких секунд
const STATUS_PREVIEW = "STATUS_PREVIEW";

function getTimerValue(startDate, endDate) {
Expand All @@ -26,9 +28,9 @@ function getTimerValue(startDate, endDate) {
endDate = new Date();
}

const diffInSecconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000);
const minutes = Math.floor(diffInSecconds / 60);
const seconds = diffInSecconds % 60;
const diffInSeconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000);
const minutes = Math.floor(diffInSeconds / 60);
const seconds = diffInSeconds % 60;
return {
minutes,
seconds,
Expand All @@ -41,53 +43,71 @@ function getTimerValue(startDate, endDate) {
* previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры
*/
export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
const { livesMode } = useContext(GameModeContext); // Используем контекст

// Состояние для игровых карт
const [cards, setCards] = useState([]);
// Текущий статус игры
const [status, setStatus] = useState(STATUS_PREVIEW);

// Дата начала игры
// Дата начала и окончания игры
const [gameStartDate, setGameStartDate] = useState(null);
// Дата конца игры
const [gameEndDate, setGameEndDate] = useState(null);

// Стейт для таймера, высчитывается в setInteval на основе gameStartDate и gameEndDate
// Состояние таймера
const [timer, setTimer] = useState({
seconds: 0,
minutes: 0,
});
// Количество оставшихся жизней
const initialLives = livesMode ? 3 : 1;
const [lives, setLives] = useState(initialLives);
// Выбранные в данный момент карты
const [selectedCards, setSelectedCards] = useState([]);
// Флаг для блокировки кликов во время проверки пар
const [isProcessing, setIsProcessing] = useState(false);

// Обновляем количество жизней при изменении режима игры
useEffect(() => {
setLives(initialLives);
}, [initialLives]);

function finishGame(status = STATUS_LOST) {
// Функция для завершения игры
function finishGame(gameStatus = STATUS_LOST) {
setGameEndDate(new Date());
setStatus(status);
setStatus(gameStatus);
}

// Функция для старта игры
function startGame() {
const startDate = new Date();
setGameEndDate(null);
setGameStartDate(startDate);
setTimer(getTimerValue(startDate, null));
setStatus(STATUS_IN_PROGRESS);
setLives(initialLives);
setSelectedCards([]);
setIsProcessing(false);
}

// Функция для перезапуска игры
function resetGame() {
setGameStartDate(null);
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
setLives(initialLives);
setSelectedCards([]);
setIsProcessing(false);
}

/**
* Обработка основного действия в игре - открытие карты.
* После открытия карты игра может пепереходит в следующие состояния
* - "Игрок выиграл", если на поле открыты все карты
* - "Игрок проиграл", если на поле есть две открытые карты без пары
* - "Игра продолжается", если не случилось первых двух условий
* Обработка открытия карты
*/
const openCard = clickedCard => {
// Если карта уже открыта, то ничего не делаем
if (clickedCard.open) {
if (isProcessing || clickedCard.open) {
return;
}
// Игровое поле после открытия кликнутой карты

// Открываем кликнутую карту
const nextCards = cards.map(card => {
if (card.id !== clickedCard.id) {
return card;
Expand All @@ -101,56 +121,72 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {

setCards(nextCards);

const isPlayerWon = nextCards.every(card => card.open);
// Добавляем карту в выбранные
const nextSelectedCards = [...selectedCards, clickedCard];
setSelectedCards(nextSelectedCards);

// Победа - все карты на поле открыты
if (isPlayerWon) {
finishGame(STATUS_WON);
return;
}
if (nextSelectedCards.length === 2) {
setIsProcessing(true);
const [firstCard, secondCard] = nextSelectedCards;

// Открытые карты на игровом поле
const openCards = nextCards.filter(card => card.open);
const isMatch = firstCard.rank === secondCard.rank && firstCard.suit === secondCard.suit;

// Ищем открытые карты, у которых нет пары среди других открытых
const openCardsWithoutPair = openCards.filter(card => {
const sameCards = openCards.filter(openCard => card.suit === openCard.suit && card.rank === openCard.rank);
if (isMatch) {
// Карты совпали
setSelectedCards([]);

if (sameCards.length < 2) {
return true;
}
// Проверяем, выиграл ли игрок
const isPlayerWon = nextCards.every(card => card.open);
if (isPlayerWon) {
finishGame(STATUS_WON);
}
setIsProcessing(false);
} else {
// Карты не совпали
const nextLives = lives - 1;
setLives(nextLives);

return false;
});

const playerLost = openCardsWithoutPair.length >= 2;

// "Игрок проиграл", т.к на поле есть две открытые карты без пары
if (playerLost) {
finishGame(STATUS_LOST);
return;
if (nextLives === 0) {
// Жизни закончились, игрок проиграл
finishGame(STATUS_LOST);
setIsProcessing(false);
} else {
// Закрываем карты обратно после задержки
setTimeout(() => {
setCards(currentCards =>
currentCards.map(card => {
if (card.id === firstCard.id || card.id === secondCard.id) {
return {
...card,
open: false,
};
}
return card;
}),
);
setSelectedCards([]);
setIsProcessing(false);
}, 1000); // Задержка в 1 секунду
}
}
}

// ... игра продолжается
};

const isGameEnded = status === STATUS_LOST || status === STATUS_WON;

// Игровой цикл
const navigate = useNavigate();
// Инициализация игры
useEffect(() => {
// В статусах кроме превью доп логики не требуется
if (status !== STATUS_PREVIEW) {
return;
}

// В статусе превью мы
if (pairsCount > 36) {
alert("Столько пар сделать невозможно");
return;
}

setCards(() => {
return shuffle(generateDeck(pairsCount, 10));
return shuffle(generateDeck(pairsCount));
});

const timerId = setTimeout(() => {
Expand All @@ -162,7 +198,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
};
}, [status, pairsCount, previewSeconds]);

// Обновляем значение таймера в интервале
// Обновление таймера
useEffect(() => {
const intervalId = setInterval(() => {
setTimer(getTimerValue(gameStartDate, gameEndDate));
Expand All @@ -172,6 +208,10 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
};
}, [gameStartDate, gameEndDate]);

const handleStartGame = () => {
navigate("/"); // Выполняем переход на главную страницу
};

return (
<div className={styles.container}>
<div className={styles.header}>
Expand All @@ -185,17 +225,26 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
<>
<div className={styles.timerValue}>
<div className={styles.timerDescription}>min</div>
<div>{timer.minutes.toString().padStart("2", "0")}</div>
<div>{timer.minutes.toString().padStart(2, "0")}</div>
</div>
.
<div className={styles.timerValue}>
<div className={styles.timerDescription}>sec</div>
<div>{timer.seconds.toString().padStart("2", "0")}</div>
<div>{timer.seconds.toString().padStart(2, "0")}</div>
</div>
</>
)}
</div>
{status === STATUS_IN_PROGRESS ? <Button onClick={resetGame}>Начать заново</Button> : null}
{status === STATUS_IN_PROGRESS ? (
<>
{livesMode && (
<div className={`${styles.lives} ${lives === 1 ? styles.livesCritical : ""}`}>
<p>Жизни: {lives}</p>
</div>
)}
<Button onClick={resetGame}>Начать заново</Button>
</>
) : null}
</div>

<div className={styles.cards}>
Expand All @@ -209,7 +258,9 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
/>
))}
</div>

<button className={styles.buttonEsc} onClick={handleStartGame}>
Вернуться к выбору режима
</button>
{isGameEnded ? (
<div className={styles.modalContainer}>
<EndGameModal
Expand Down
36 changes: 36 additions & 0 deletions src/components/Cards/Cards.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
@import url('https://fonts.googleapis.com/css2?family=Handjet:[email protected]&display=swap');

.lives {
font-size: 32px;
margin-left: 20px;
display: flex;
align-items: center;
font-family: "Handjet", sans-serif;
color: #fff;
/* По умолчанию белый цвет */
}

.livesCritical {
color: rgb(255, 55, 55);
/* Красный цвет при 1 жизни */
}



.container {
width: 672px;
margin: 0 auto;
Expand Down Expand Up @@ -50,6 +69,7 @@
margin-top: 34px;
margin-bottom: 10px;
}

.previewDescription {
font-size: 18px;
line-height: 18px;
Expand All @@ -70,3 +90,19 @@

margin-bottom: -12px;
}

.buttonEsc {
background-color: #7ac100;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-family: StratosSkyeng;
margin-top: 64px;
}

.buttonEsc:hover {
background-color: #45a049;
}
Loading