Skip to content
Open

0.3 #89

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5d326c8
добавил логику трех жизней
TipsssWebdev Sep 14, 2024
b3c74ed
добавил время выполнения работы в readme
TipsssWebdev Sep 15, 2024
34d49b5
появился выбор режимов
TipsssWebdev Sep 16, 2024
b0136ed
бомбовая стилистика
TipsssWebdev Sep 16, 2024
513fe8b
стилизация ну вообще бомбовая
TipsssWebdev Sep 16, 2024
b2ff97c
режим игры сохраняется в контекст
TipsssWebdev Sep 16, 2024
8762859
черновая версия лидерборда
TipsssWebdev Sep 19, 2024
bbd5162
leaderboard работает корректно без ошибок
TipsssWebdev Sep 19, 2024
ec8c581
финальная верстка
TipsssWebdev Sep 19, 2024
4e0bd78
фича: добавлена кнопка для выхода в окно выбора режима в окне game (G…
TipsssWebdev Sep 19, 2024
f68fd71
обновил файл readme
TipsssWebdev Sep 19, 2024
31f61d9
сортировка лидерборда по времени
TipsssWebdev Sep 19, 2024
ea26884
в лидербборд прихяд только результаты третьего уровня сложносьти
TipsssWebdev Sep 19, 2024
eae33c1
логика супер-силы работает без ошибок
TipsssWebdev Sep 22, 2024
34334d8
готова вёрстка супер-силы на странице cards.jsx
TipsssWebdev Sep 23, 2024
7f6b2aa
багофикс: заголовок 'Content-Type' в cardpage
TipsssWebdev Sep 23, 2024
7b9e539
в лидерборд добавлены суперсила и hardmode
TipsssWebdev Sep 23, 2024
abe2849
весрстка лидерборда
TipsssWebdev Sep 23, 2024
6a2135c
Исправлен баг. fix: сброс счетчика жизней после перезапуска игры
TipsssWebdev Sep 23, 2024
28c3831
тест + readme
TipsssWebdev Sep 23, 2024
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 @@
## Оценка времени выполнения работы "Курсовая работа 4"

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

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

В этом репозитории реализован MVP карточкой игры "Мемо" по [тех.заданию](./docs/mvp-spec.md)
Expand Down
246 changes: 142 additions & 104 deletions src/components/Cards/Cards.jsx
Original file line number Diff line number Diff line change
@@ -1,157 +1,180 @@
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";
import { ReactComponent as PowerIcon } from "../../components/EndGameModal/images/power.svg";

// Игра закончилась
// Константы статусов игры
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) {
if (!startDate && !endDate) {
return {
minutes: 0,
seconds: 0,
};
}

if (endDate === null) {
endDate = new Date();
}

const diffInSecconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000);
const minutes = Math.floor(diffInSecconds / 60);
const seconds = diffInSecconds % 60;
function getTimerValue(secondsElapsed) {
const minutes = Math.floor(secondsElapsed / 60);
const seconds = secondsElapsed % 60;
return {
minutes,
seconds,
};
}

/**
* Основной компонент игры, внутри него находится вся игровая механика и логика.
* pairsCount - сколько пар будет в игре
* previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры
*/
export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
const { livesMode } = useContext(GameModeContext);

// Состояние для игровых карт
const [cards, setCards] = useState([]);
// Текущий статус игры
const [initialCardsState, setInitialCardsState] = useState([]);
const [status, setStatus] = useState(STATUS_PREVIEW);
const [secondsElapsed, setSecondsElapsed] = useState(0);
const [timer, setTimer] = useState({ minutes: 0, seconds: 0 });
const [intervalId, setIntervalId] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [isSuperPowerUsed, setIsSuperPowerUsed] = useState(false);

// Дата начала игры
const [gameStartDate, setGameStartDate] = useState(null);
// Дата конца игры
const [gameEndDate, setGameEndDate] = useState(null);
const initialLives = livesMode ? 3 : 1;
const [lives, setLives] = useState(initialLives);
const [selectedCards, setSelectedCards] = useState([]);

// Стейт для таймера, высчитывается в setInteval на основе gameStartDate и gameEndDate
const [timer, setTimer] = useState({
seconds: 0,
minutes: 0,
});
// Обновляем количество жизней при изменении режима игры
useEffect(() => {
setLives(initialLives);
}, [initialLives]);

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

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

const newIntervalId = setInterval(() => {
setSecondsElapsed(prev => prev + 1);
}, 1000);

setIntervalId(newIntervalId);
}

useEffect(() => {
if (status === STATUS_IN_PROGRESS) {
setTimer(getTimerValue(secondsElapsed));
}
}, [secondsElapsed, status]);

// Функция для перезапуска игры
function resetGame() {
setGameStartDate(null);
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
setSecondsElapsed(0);
setTimer({ minutes: 0, seconds: 0 });
setSelectedCards([]);
setIsProcessing(false);
setIsSuperPowerUsed(false);
clearInterval(intervalId);

// Добавляем сброс жизней на начальное количество
setLives(initialLives);
}

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

const nextCards = cards.map(card => {
if (card.id !== clickedCard.id) {
return card;
}

return {
...card,
open: true,
};
return { ...card, open: true };
});

setCards(nextCards);
const nextSelectedCards = [...selectedCards, clickedCard];
setSelectedCards(nextSelectedCards);

const isPlayerWon = nextCards.every(card => card.open);
if (nextSelectedCards.length === 2) {
setIsProcessing(true);
const [firstCard, secondCard] = nextSelectedCards;
const isMatch = firstCard.rank === secondCard.rank && firstCard.suit === secondCard.suit;

// Победа - все карты на поле открыты
if (isPlayerWon) {
finishGame(STATUS_WON);
return;
if (isMatch) {
setSelectedCards([]);
const isPlayerWon = nextCards.every(card => card.open);
if (isPlayerWon) {
finishGame(STATUS_WON);
}
setIsProcessing(false);
} else {
const nextLives = lives - 1;
setLives(nextLives);
if (nextLives === 0) {
finishGame(STATUS_LOST);
} 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);
}
}
}
};

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

// Ищем открытые карты, у которых нет пары среди других открытых
const openCardsWithoutPair = openCards.filter(card => {
const sameCards = openCards.filter(openCard => card.suit === openCard.suit && card.rank === openCard.rank);
const handleSuperPower = () => {
if (!isSuperPowerUsed) {
setIsSuperPowerUsed(true);
setInitialCardsState(cards);

if (sameCards.length < 2) {
return true;
}

return false;
});
const openedCards = cards.map(card => ({ ...card, open: true }));
setCards(openedCards);

const playerLost = openCardsWithoutPair.length >= 2;
clearInterval(intervalId);

// "Игрок проиграл", т.к на поле есть две открытые карты без пары
if (playerLost) {
finishGame(STATUS_LOST);
return;
setTimeout(() => {
setCards(initialCardsState);
const newIntervalId = setInterval(() => {
setSecondsElapsed(prev => prev + 1);
}, 1000);
setIntervalId(newIntervalId);
}, 5000);
}

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

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));
});
const deck = shuffle(generateDeck(pairsCount));
setCards(deck);
setInitialCardsState(deck);

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

// Обновляем значение таймера в интервале
useEffect(() => {
const intervalId = setInterval(() => {
setTimer(getTimerValue(gameStartDate, gameEndDate));
}, 300);
return () => {
clearInterval(intervalId);
};
}, [gameStartDate, gameEndDate]);
const handleStartGame = () => {
navigate("/"); // Выполняем переход на главную страницу
};

return (
<div className={styles.container}>
Expand All @@ -185,38 +202,59 @@ 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>
<div className={styles.superPowerContainer}>
<button onClick={handleSuperPower} disabled={isSuperPowerUsed} className={styles.superPowerButton}>
<PowerIcon width="68" height="68" />
</button>
<div className={styles.tooltip}>
<div className={styles.title}>Прозрение</div>
<p>На 5 секунд показываются все карты. Таймер длительности игры на это время останавливается.</p>
</div>
</div>
</>
) : null}
</div>

<div className={styles.cards}>
{cards.map(card => (
<Card
key={card.id}
onClick={() => openCard(card)}
open={status !== STATUS_IN_PROGRESS ? true : card.open}
open={status !== STATUS_IN_PROGRESS || card.open}
suit={card.suit}
rank={card.rank}
/>
))}
</div>

<button className={styles.buttonEsc} onClick={handleStartGame}>
Вернуться к выбору режима
</button>
{isGameEnded ? (
<div className={styles.modalContainer}>
<EndGameModal
isWon={status === STATUS_WON}
gameDurationSeconds={timer.seconds}
gameDurationMinutes={timer.minutes}
onClick={resetGame}
isSuperPowerUsed={isSuperPowerUsed} // Передаем состояние использования суперсилы
/>
</div>
) : null}
Expand Down
Loading