2025-12-31 17:57:11 +00:00
|
|
|
|
import { useState, useImperativeHandle, forwardRef, useEffect } from 'react'
|
2025-12-31 16:53:26 +00:00
|
|
|
|
import Question from './Question'
|
2025-12-31 17:05:48 +00:00
|
|
|
|
import Players from './Players'
|
|
|
|
|
|
import PlayersModal from './PlayersModal'
|
2025-12-31 17:13:24 +00:00
|
|
|
|
import QuestionsModal from './QuestionsModal'
|
2025-12-31 17:57:11 +00:00
|
|
|
|
import { getCookie, setCookie, deleteCookie } from '../utils/cookies'
|
2025-12-31 16:53:26 +00:00
|
|
|
|
import './Game.css'
|
|
|
|
|
|
|
2025-12-31 17:40:34 +00:00
|
|
|
|
const Game = forwardRef(({
|
|
|
|
|
|
questions = [],
|
|
|
|
|
|
currentQuestionIndex = 0,
|
|
|
|
|
|
onQuestionIndexChange,
|
|
|
|
|
|
onQuestionsChange,
|
|
|
|
|
|
}, ref) => {
|
2025-12-31 17:57:11 +00:00
|
|
|
|
const [players, setPlayers] = useState(() => {
|
|
|
|
|
|
const savedPlayers = getCookie('gamePlayers')
|
|
|
|
|
|
return savedPlayers || []
|
|
|
|
|
|
})
|
|
|
|
|
|
const [currentPlayerId, setCurrentPlayerId] = useState(() => {
|
|
|
|
|
|
const savedId = getCookie('gameCurrentPlayerId')
|
|
|
|
|
|
return savedId !== null ? savedId : null
|
|
|
|
|
|
})
|
|
|
|
|
|
const [playerScores, setPlayerScores] = useState(() => {
|
|
|
|
|
|
const savedScores = getCookie('gamePlayerScores')
|
|
|
|
|
|
return savedScores || {}
|
|
|
|
|
|
})
|
|
|
|
|
|
const [gameOver, setGameOver] = useState(() => {
|
|
|
|
|
|
const savedGameOver = getCookie('gameOver')
|
|
|
|
|
|
return savedGameOver === true
|
|
|
|
|
|
})
|
|
|
|
|
|
const [revealedAnswers, setRevealedAnswers] = useState(() => {
|
|
|
|
|
|
const savedAnswers = getCookie('gameRevealedAnswers')
|
|
|
|
|
|
return savedAnswers || {}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Получаем открытые ответы для текущего вопроса
|
|
|
|
|
|
const getCurrentRevealedAnswers = () => {
|
|
|
|
|
|
return revealedAnswers[currentQuestionIndex] || []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновляем открытые ответы для текущего вопроса
|
|
|
|
|
|
const updateRevealedAnswers = (newAnswers) => {
|
|
|
|
|
|
setRevealedAnswers({
|
|
|
|
|
|
...revealedAnswers,
|
|
|
|
|
|
[currentQuestionIndex]: newAnswers,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-12-31 17:05:48 +00:00
|
|
|
|
const [isPlayersModalOpen, setIsPlayersModalOpen] = useState(false)
|
2025-12-31 17:13:24 +00:00
|
|
|
|
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false)
|
2025-12-31 16:53:26 +00:00
|
|
|
|
|
2025-12-31 17:57:11 +00:00
|
|
|
|
// Сохраняем состояние в cookies при изменении
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (players.length > 0) {
|
|
|
|
|
|
setCookie('gamePlayers', players)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
deleteCookie('gamePlayers')
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [players])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (currentPlayerId !== null) {
|
|
|
|
|
|
setCookie('gameCurrentPlayerId', currentPlayerId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
deleteCookie('gameCurrentPlayerId')
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [currentPlayerId])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (Object.keys(playerScores).length > 0) {
|
|
|
|
|
|
setCookie('gamePlayerScores', playerScores)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
deleteCookie('gamePlayerScores')
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [playerScores])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setCookie('gameRevealedAnswers', revealedAnswers)
|
|
|
|
|
|
}, [revealedAnswers])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setCookie('gameOver', gameOver)
|
|
|
|
|
|
}, [gameOver])
|
|
|
|
|
|
|
|
|
|
|
|
const currentQuestion = questions[currentQuestionIndex]
|
|
|
|
|
|
const isLastQuestion = currentQuestionIndex === questions.length - 1
|
|
|
|
|
|
|
|
|
|
|
|
const handleShowAllAnswers = () => {
|
|
|
|
|
|
if (!currentQuestion) return
|
|
|
|
|
|
const allAnswerIndices = currentQuestion.answers.map((_, index) => index)
|
|
|
|
|
|
updateRevealedAnswers(allAnswerIndices)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 17:40:34 +00:00
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
|
|
openPlayersModal: () => setIsPlayersModalOpen(true),
|
|
|
|
|
|
openQuestionsModal: () => setIsQuestionsModalOpen(true),
|
2025-12-31 17:57:11 +00:00
|
|
|
|
newGame: () => {
|
|
|
|
|
|
setPlayers([])
|
|
|
|
|
|
setCurrentPlayerId(null)
|
|
|
|
|
|
setPlayerScores({})
|
|
|
|
|
|
setGameOver(false)
|
|
|
|
|
|
setRevealedAnswers({})
|
|
|
|
|
|
},
|
|
|
|
|
|
showAllAnswers: handleShowAllAnswers,
|
2025-12-31 17:40:34 +00:00
|
|
|
|
}))
|
|
|
|
|
|
|
2025-12-31 17:05:48 +00:00
|
|
|
|
const handleAddPlayer = (name) => {
|
|
|
|
|
|
const newPlayer = {
|
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
}
|
|
|
|
|
|
const updatedPlayers = [...players, newPlayer]
|
|
|
|
|
|
setPlayers(updatedPlayers)
|
|
|
|
|
|
|
|
|
|
|
|
// Если это первый участник, делаем его текущим
|
|
|
|
|
|
if (updatedPlayers.length === 1) {
|
|
|
|
|
|
setCurrentPlayerId(newPlayer.id)
|
|
|
|
|
|
setPlayerScores({ [newPlayer.id]: 0 })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setPlayerScores({ ...playerScores, [newPlayer.id]: 0 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectPlayer = (playerId) => {
|
|
|
|
|
|
setCurrentPlayerId(playerId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleRemovePlayer = (playerId) => {
|
|
|
|
|
|
const updatedPlayers = players.filter(p => p.id !== playerId)
|
|
|
|
|
|
setPlayers(updatedPlayers)
|
|
|
|
|
|
|
|
|
|
|
|
const updatedScores = { ...playerScores }
|
|
|
|
|
|
delete updatedScores[playerId]
|
|
|
|
|
|
setPlayerScores(updatedScores)
|
|
|
|
|
|
|
|
|
|
|
|
// Если удалили текущего участника, выбираем другого
|
|
|
|
|
|
if (currentPlayerId === playerId) {
|
|
|
|
|
|
if (updatedPlayers.length > 0) {
|
|
|
|
|
|
setCurrentPlayerId(updatedPlayers[0].id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setCurrentPlayerId(null)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getNextPlayerId = () => {
|
|
|
|
|
|
if (players.length === 0) return null
|
|
|
|
|
|
if (players.length === 1) return currentPlayerId
|
|
|
|
|
|
|
|
|
|
|
|
const currentIndex = players.findIndex(p => p.id === currentPlayerId)
|
|
|
|
|
|
const nextIndex = (currentIndex + 1) % players.length
|
|
|
|
|
|
return players[nextIndex].id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 16:53:26 +00:00
|
|
|
|
const handleAnswerClick = (answerIndex, points) => {
|
2025-12-31 17:57:11 +00:00
|
|
|
|
const currentRevealed = getCurrentRevealedAnswers()
|
|
|
|
|
|
if (currentRevealed.includes(answerIndex)) return
|
2025-12-31 17:05:48 +00:00
|
|
|
|
if (!currentPlayerId) return
|
2025-12-31 17:13:24 +00:00
|
|
|
|
if (!currentQuestion) return
|
2025-12-31 16:53:26 +00:00
|
|
|
|
|
2025-12-31 17:57:11 +00:00
|
|
|
|
const isLastAnswer = currentRevealed.length === currentQuestion.answers.length - 1
|
2025-12-31 17:05:48 +00:00
|
|
|
|
|
2025-12-31 17:57:11 +00:00
|
|
|
|
updateRevealedAnswers([...currentRevealed, answerIndex])
|
2025-12-31 17:05:48 +00:00
|
|
|
|
|
|
|
|
|
|
// Добавляем очки текущему участнику
|
|
|
|
|
|
setPlayerScores({
|
|
|
|
|
|
...playerScores,
|
|
|
|
|
|
[currentPlayerId]: (playerScores[currentPlayerId] || 0) + points,
|
|
|
|
|
|
})
|
2025-12-31 16:53:26 +00:00
|
|
|
|
|
2025-12-31 17:05:48 +00:00
|
|
|
|
// Переходим к следующему участнику только если это не последний ответ
|
|
|
|
|
|
if (!isLastAnswer) {
|
|
|
|
|
|
const nextPlayerId = getNextPlayerId()
|
|
|
|
|
|
if (nextPlayerId) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setCurrentPlayerId(nextPlayerId)
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Если это последний ответ, переходим к следующему участнику перед следующим вопросом
|
2025-12-31 16:53:26 +00:00
|
|
|
|
setTimeout(() => {
|
2025-12-31 17:05:48 +00:00
|
|
|
|
const nextPlayerId = getNextPlayerId()
|
|
|
|
|
|
if (nextPlayerId) {
|
|
|
|
|
|
setCurrentPlayerId(nextPlayerId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 16:53:26 +00:00
|
|
|
|
if (isLastQuestion) {
|
|
|
|
|
|
setGameOver(true)
|
|
|
|
|
|
} else {
|
2025-12-31 17:05:48 +00:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
nextQuestion()
|
|
|
|
|
|
}, 500)
|
2025-12-31 16:53:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nextQuestion = () => {
|
2025-12-31 17:40:34 +00:00
|
|
|
|
if (onQuestionIndexChange) {
|
|
|
|
|
|
onQuestionIndexChange(currentQuestionIndex + 1)
|
|
|
|
|
|
}
|
2025-12-31 17:57:11 +00:00
|
|
|
|
// Не сбрасываем открытые ответы - они сохраняются для каждого вопроса отдельно
|
2025-12-31 16:53:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const restartGame = () => {
|
2025-12-31 17:40:34 +00:00
|
|
|
|
if (onQuestionIndexChange) {
|
|
|
|
|
|
onQuestionIndexChange(0)
|
|
|
|
|
|
}
|
2025-12-31 16:53:26 +00:00
|
|
|
|
setGameOver(false)
|
2025-12-31 17:57:11 +00:00
|
|
|
|
setRevealedAnswers({})
|
2025-12-31 17:05:48 +00:00
|
|
|
|
const initialScores = {}
|
|
|
|
|
|
players.forEach(player => {
|
|
|
|
|
|
initialScores[player.id] = 0
|
|
|
|
|
|
})
|
|
|
|
|
|
setPlayerScores(initialScores)
|
|
|
|
|
|
if (players.length > 0) {
|
|
|
|
|
|
setCurrentPlayerId(players[0].id)
|
|
|
|
|
|
}
|
2025-12-31 16:53:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 17:57:11 +00:00
|
|
|
|
const newGame = () => {
|
|
|
|
|
|
setPlayers([])
|
|
|
|
|
|
setCurrentPlayerId(null)
|
|
|
|
|
|
setPlayerScores({})
|
|
|
|
|
|
setGameOver(false)
|
|
|
|
|
|
setRevealedAnswers({})
|
|
|
|
|
|
if (onQuestionIndexChange) {
|
|
|
|
|
|
onQuestionIndexChange(0)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handlePreviousQuestion = () => {
|
|
|
|
|
|
if (currentQuestionIndex > 0 && onQuestionIndexChange) {
|
|
|
|
|
|
onQuestionIndexChange(currentQuestionIndex - 1)
|
|
|
|
|
|
// Открытые ответы сохраняются для каждого вопроса отдельно
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleNextQuestion = () => {
|
|
|
|
|
|
if (currentQuestionIndex < questions.length - 1 && onQuestionIndexChange) {
|
|
|
|
|
|
onQuestionIndexChange(currentQuestionIndex + 1)
|
|
|
|
|
|
// Открытые ответы сохраняются для каждого вопроса отдельно
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 16:53:26 +00:00
|
|
|
|
if (gameOver) {
|
2025-12-31 17:05:48 +00:00
|
|
|
|
// Находим победителя(ей)
|
|
|
|
|
|
const scores = Object.values(playerScores)
|
|
|
|
|
|
const maxScore = scores.length > 0 ? Math.max(...scores) : 0
|
|
|
|
|
|
const winners = players.filter(p => playerScores[p.id] === maxScore)
|
|
|
|
|
|
|
2025-12-31 16:53:26 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="game-over">
|
|
|
|
|
|
<div className="game-over-content">
|
|
|
|
|
|
<h2 className="game-over-title">🎉 Игра окончена! 🎉</h2>
|
2025-12-31 17:05:48 +00:00
|
|
|
|
<div className="final-scores">
|
|
|
|
|
|
<h3 className="final-scores-title">Итоговые результаты:</h3>
|
|
|
|
|
|
{players
|
|
|
|
|
|
.sort((a, b) => (playerScores[b.id] || 0) - (playerScores[a.id] || 0))
|
|
|
|
|
|
.map((player) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={player.id}
|
|
|
|
|
|
className={`final-score-item ${
|
|
|
|
|
|
winners.includes(player) ? 'final-score-winner' : ''
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="final-score-name">{player.name}</span>
|
|
|
|
|
|
<span className="final-score-value">
|
|
|
|
|
|
{playerScores[player.id] || 0} очков
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2025-12-31 16:53:26 +00:00
|
|
|
|
<button className="restart-button" onClick={restartGame}>
|
|
|
|
|
|
Играть снова
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="game">
|
2025-12-31 17:05:48 +00:00
|
|
|
|
<PlayersModal
|
|
|
|
|
|
isOpen={isPlayersModalOpen}
|
|
|
|
|
|
onClose={() => setIsPlayersModalOpen(false)}
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
onAddPlayer={handleAddPlayer}
|
|
|
|
|
|
onRemovePlayer={handleRemovePlayer}
|
2025-12-31 16:53:26 +00:00
|
|
|
|
/>
|
2025-12-31 17:05:48 +00:00
|
|
|
|
|
2025-12-31 17:13:24 +00:00
|
|
|
|
<QuestionsModal
|
|
|
|
|
|
isOpen={isQuestionsModalOpen}
|
|
|
|
|
|
onClose={() => setIsQuestionsModalOpen(false)}
|
|
|
|
|
|
questions={questions}
|
2025-12-31 17:40:34 +00:00
|
|
|
|
onUpdateQuestions={onQuestionsChange}
|
2025-12-31 17:13:24 +00:00
|
|
|
|
/>
|
|
|
|
|
|
|
2025-12-31 17:05:48 +00:00
|
|
|
|
<div className="game-header">
|
|
|
|
|
|
{players.length > 0 && (
|
|
|
|
|
|
<Players
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
currentPlayerId={currentPlayerId}
|
|
|
|
|
|
playerScores={playerScores}
|
|
|
|
|
|
onSelectPlayer={handleSelectPlayer}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{players.length > 0 && currentPlayerId ? (
|
2025-12-31 17:40:34 +00:00
|
|
|
|
<div className="game-content">
|
2025-12-31 17:13:24 +00:00
|
|
|
|
{questions.length === 0 ? (
|
|
|
|
|
|
<div className="no-players-message">
|
|
|
|
|
|
<p>Добавьте вопросы, чтобы начать игру</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : currentQuestion ? (
|
2025-12-31 17:40:34 +00:00
|
|
|
|
<Question
|
|
|
|
|
|
question={currentQuestion}
|
|
|
|
|
|
questionNumber={currentQuestionIndex + 1}
|
|
|
|
|
|
onAnswerClick={handleAnswerClick}
|
2025-12-31 17:57:11 +00:00
|
|
|
|
revealedAnswers={getCurrentRevealedAnswers()}
|
|
|
|
|
|
onPreviousQuestion={handlePreviousQuestion}
|
|
|
|
|
|
onNextQuestion={handleNextQuestion}
|
|
|
|
|
|
canGoPrevious={currentQuestionIndex > 0}
|
|
|
|
|
|
canGoNext={currentQuestionIndex < questions.length - 1}
|
2025-12-31 17:40:34 +00:00
|
|
|
|
/>
|
2025-12-31 17:13:24 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="no-players-message">
|
|
|
|
|
|
<p>Ошибка: вопрос не найден</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-12-31 17:40:34 +00:00
|
|
|
|
</div>
|
2025-12-31 17:05:48 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="no-players-message">
|
|
|
|
|
|
<p>Добавьте участников, чтобы начать игру</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-12-31 16:53:26 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
2025-12-31 17:40:34 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Game.displayName = 'Game'
|
2025-12-31 16:53:26 +00:00
|
|
|
|
|
|
|
|
|
|
export default Game
|
|
|
|
|
|
|