diff --git a/.prettierrc.js b/.prettierrc.js index 65e18f5ff..a037d25c9 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -7,4 +7,5 @@ module.exports = { bracketSpacing: true, arrowParens: "avoid", htmlWhitespaceSensitivity: "ignore", + endOfLine: "auto", // для своместной разработки и с linux и с windows }; diff --git a/README.md b/README.md index 9b90842c4..dfb7cb167 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +## Оценка времени выполнения работы "Вводный урок" + +- **Ожидаемое время:** 6 часов +- **Фактическое время:** 6 часов + # MVP Карточная игра "Мемо" В этом репозитории реализован MVP карточкой игры "Мемо" по [тех.заданию](./docs/mvp-spec.md) diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..604d1df23 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -1,17 +1,18 @@ +// src/components/Cards/Cards.jsx + import { shuffle } from "lodash"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useContext } 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 { 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) { @@ -26,9 +27,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, @@ -41,53 +42,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; @@ -101,56 +120,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; - } - - // Открытые карты на игровом поле - const openCards = nextCards.filter(card => card.open); + if (nextSelectedCards.length === 2) { + setIsProcessing(true); + const [firstCard, secondCard] = nextSelectedCards; - // Ищем открытые карты, у которых нет пары среди других открытых - const openCardsWithoutPair = openCards.filter(card => { - const sameCards = openCards.filter(openCard => card.suit === openCard.suit && card.rank === openCard.rank); + const isMatch = firstCard.rank === secondCard.rank && firstCard.suit === secondCard.suit; - if (sameCards.length < 2) { - return true; - } - - return false; - }); + if (isMatch) { + // Карты совпали + setSelectedCards([]); - const playerLost = openCardsWithoutPair.length >= 2; + // Проверяем, выиграл ли игрок + const isPlayerWon = nextCards.every(card => card.open); + if (isPlayerWon) { + finishGame(STATUS_WON); + } + setIsProcessing(false); + } else { + // Карты не совпали + const nextLives = lives - 1; + setLives(nextLives); - // "Игрок проиграл", т.к на поле есть две открытые карты без пары - 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; - // Игровой цикл + // Инициализация игры useEffect(() => { - // В статусах кроме превью доп логики не требуется if (status !== STATUS_PREVIEW) { return; } - // В статусе превью мы if (pairsCount > 36) { alert("Столько пар сделать невозможно"); return; } setCards(() => { - return shuffle(generateDeck(pairsCount, 10)); + return shuffle(generateDeck(pairsCount)); }); const timerId = setTimeout(() => { @@ -162,7 +197,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { }; }, [status, pairsCount, previewSeconds]); - // Обновляем значение таймера в интервале + // Обновление таймера useEffect(() => { const intervalId = setInterval(() => { setTimer(getTimerValue(gameStartDate, gameEndDate)); @@ -185,17 +220,26 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { <>
Жизни: {lives}
+