Skip to content
Open

Hw1 #86

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",
};
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
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.
2 changes: 1 addition & 1 deletion public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
Disallow:
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/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>");
}
104 changes: 93 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 All @@ -160,13 +228,15 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
return () => {
clearTimeout(timerId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status, pairsCount, previewSeconds]);

// Обновляем значение таймера в интервале
useEffect(() => {
const intervalId = setInterval(() => {
setTimer(getTimerValue(gameStartDate, gameEndDate));
}, 300);

return () => {
clearInterval(intervalId);
};
Expand Down Expand Up @@ -195,7 +265,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 +281,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