Skip to content
Open

0.1 #85

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 часов
- **Фактическое время:** 6 часов

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

В этом репозитории реализован MVP карточкой игры "Мемо" по [тех.заданию](./docs/mvp-spec.md)
Expand Down
156 changes: 100 additions & 56 deletions src/components/Cards/Cards.jsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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(() => {
Expand All @@ -162,7 +197,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
};
}, [status, pairsCount, previewSeconds]);

// Обновляем значение таймера в интервале
// Обновление таймера
useEffect(() => {
const intervalId = setInterval(() => {
setTimer(getTimerValue(gameStartDate, gameEndDate));
Expand All @@ -185,17 +220,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 Down
19 changes: 18 additions & 1 deletion src/components/Cards/Cards.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
@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 @@ -69,4 +86,4 @@
line-height: 32px;

margin-bottom: -12px;
}
}
19 changes: 19 additions & 0 deletions src/context/GameModeContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createContext, useState, useEffect } from "react";

export const GameModeContext = createContext({
livesMode: false,
setLivesMode: () => {},
});

export function GameModeProvider({ children }) {
const [livesMode, setLivesMode] = useState(() => {
const savedMode = localStorage.getItem("livesMode");
return savedMode === "true";
});

useEffect(() => {
localStorage.setItem("livesMode", livesMode);
}, [livesMode]);

return <GameModeContext.Provider value={{ livesMode, setLivesMode }}>{children}</GameModeContext.Provider>;
}
12 changes: 11 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import { GameModeProvider } from "./context/GameModeContext"; // Импортируем обновленный провайдер

const root = ReactDOM.createRoot(document.getElementById("root"));

function App() {
return (
<GameModeProvider>
<RouterProvider router={router} />
</GameModeProvider>
);
}

root.render(
<React.StrictMode>
<RouterProvider router={router}></RouterProvider>
<App />
</React.StrictMode>,
);
13 changes: 13 additions & 0 deletions src/pages/SelectLevelPage/SelectLevelPage.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
// src/pages/SelectLevelPage/SelectLevelPage.jsx

import { Link } from "react-router-dom";
import styles from "./SelectLevelPage.module.css";
import { useContext } from "react";
import { GameModeContext } from "../../context/GameModeContext";

export function SelectLevelPage() {
const { livesMode, setLivesMode } = useContext(GameModeContext);

const handleCheckboxChange = event => {
setLivesMode(event.target.checked);
};
return (
<div className={styles.container}>
<div className={styles.modal}>
<h1 className={styles.title}>Выбери сложность</h1>
<label className={styles.checkboxContainer}>
<input type="checkbox" checked={livesMode} onChange={handleCheckboxChange} />
Режим с тремя жизнями
</label>
<ul className={styles.levels}>
<li className={styles.level}>
<Link className={styles.levelLink} to="/game/3">
Expand Down
Loading