Skip to content
Open

Hw 2 #77

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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ https://skypro-web-developer.github.io/react-memo/

Настроены eslint и prettier. Корректность кода проверяется автоматически перед каждым коммитом с помощью lefthook (аналог husky). Закомитить код, который не проходит проверку eslint не получится.

### Затраченное время на разработку дополнительных функций

1.1 Реализация упрощенного режима "3 попытки"
Планируемое время 16 часов

Факт 14 часов

2.1 Реализация Лидерборда
Планируемое время 24 часов

Факт 18 часов

### Доступные команды

#### `npm start`
Expand Down
7 changes: 4 additions & 3 deletions docs/mvp-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
Количество карточек для каждого уровня сложности можете назначать и свои или выбрать готовый пресет.

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

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

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

Expand Down
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.
14 changes: 14 additions & 0 deletions src/api/getLeaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const apiUrl = "https://wedev-api.sky.pro/api/v2/leaderboard";

export const getLeaderboard = async () => {
// Запрос к API получения списка победителей
const response = await fetch(apiUrl, {
method: "GET",
});

if (!response.ok) {
throw new Error(`Не удалось получить данные с сервера! status: ${response.status}`);
}

return await response.json();
};
23 changes: 23 additions & 0 deletions src/api/postLeaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const apiUrl = "https://wedev-api.sky.pro/api/leaderboard";

export const postLeaderboard = async ({ userName, userTime }) => {
// Запрос к API отправки победителя
const response = await fetch(apiUrl, {
method: "POST",
body: JSON.stringify({ name: goodByeHacker(userName), time: userTime }),
});

if (!response.ok) {
throw new Error(`Не удалось отправить данные на сервер! status: ${response.status}`);
}

return await response.json();
};

function goodByeHacker(text) {
return text
.replaceAll("<", "&lt")
.replaceAll(">", "&gt")
.replaceAll("QUOTE_BEGIN", "<div class='quote'>")
.replaceAll("QUOTE_END", "</div>");
}
103 changes: 92 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 All @@ -19,6 +23,7 @@ function getTimerValue(startDate, endDate) {
return {
minutes: 0,
seconds: 0,
diffInSecconds: 0,
};
}

Expand All @@ -27,11 +32,13 @@ function getTimerValue(startDate, endDate) {
}

const diffInSecconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000);

const minutes = Math.floor(diffInSecconds / 60);
const seconds = diffInSecconds % 60;
return {
minutes,
seconds,
diffInSecconds,
};
}

Expand All @@ -41,8 +48,18 @@ function getTimerValue(startDate, endDate) {
* previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры
*/
export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// Когда игра окончена, переход на главную страницу
const navigate = useNavigate();
function goTo() {
navigate("/");
}

// Обработка количества попыток
const { tries, setTries, isEasyMode, checkedLevel, leadrs, setLeaders } = useContext(EasyContext);

// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
const [cards, setCards] = useState([]);

// Текущий статус игры
const [status, setStatus] = useState(STATUS_PREVIEW);

Expand All @@ -55,18 +72,35 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
const [timer, setTimer] = useState({
seconds: 0,
minutes: 0,
diffInSecconds: 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 +109,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 +175,34 @@ 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;
// Проверка на попадание в топ 10 игроков
function isTopTen() {
const isTenPlayers = leadrs.length === 10;
if (status === STATUS_WON && checkedLevel === 3) {
if (leadrs.at(-1).time > timer.diffInSecconds || (isTenPlayers && leadrs[9].time > timer.diffInSecconds)) {
return true;
}
}
return false;
}

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

// Игровой цикл
useEffect(() => {
Expand Down Expand Up @@ -167,6 +235,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
const intervalId = setInterval(() => {
setTimer(getTimerValue(gameStartDate, gameEndDate));
}, 300);

return () => {
clearInterval(intervalId);
};
Expand Down Expand Up @@ -195,7 +264,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 +280,24 @@ 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}
checkedLevel={checkedLevel}
isTopTen={isTopTen()}
leadrs={leadrs}
setLeaders={setLeaders}
diffInSecconds={timer.diffInSecconds}
/>
</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;
}
Loading