v3.1.4: Улучшено равномерное распределение карточек в тесте
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 28s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 28s
- Добавлен учёт текущей карточки при перераспределении (не показывается следующей) - Исправлен алгоритм равномерного распределения для предотвращения подряд идущих одинаковых карточек - Исправлен правый счётчик: показывает текущий размер пула + показанные карточки (не больше maxCards)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "3.1.3",
|
||||
"version": "3.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -14,7 +14,7 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
|
||||
const [words, setWords] = useState([]) // Начальный пул всех слов (для статистики)
|
||||
const [testWords, setTestWords] = useState([]) // Пул слов для показа
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [currentWord, setCurrentWord] = useState(null) // Текущее слово, которое показывается (уже удалено из пула)
|
||||
const [flippedCards, setFlippedCards] = useState(new Set())
|
||||
const [wordStats, setWordStats] = useState({}) // Локальная статистика
|
||||
const [cardsShown, setCardsShown] = useState(0) // Левый счётчик: кол-во показанных карточек
|
||||
@@ -28,11 +28,102 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
const wordStatsRef = useRef({})
|
||||
const processingRef = useRef(false)
|
||||
|
||||
// Функция равномерного распределения слов в пуле с гарантией максимального расстояния между одинаковыми словами
|
||||
// excludeFirstWordId - ID слова, которое не должно быть первым в пуле (только что показанная карточка)
|
||||
const redistributeWordsEvenly = (currentPool, allWords, excludeFirstWordId = null) => {
|
||||
if (currentPool.length === 0 || allWords.length === 0) {
|
||||
return currentPool
|
||||
}
|
||||
|
||||
// Подсчитываем, сколько раз каждое слово встречается в текущем пуле
|
||||
const wordCounts = {}
|
||||
currentPool.forEach(word => {
|
||||
wordCounts[word.id] = (wordCounts[word.id] || 0) + 1
|
||||
})
|
||||
|
||||
// Получаем список уникальных слов, которые есть в пуле
|
||||
const uniqueWordIds = Object.keys(wordCounts).map(id => parseInt(id))
|
||||
const uniqueWords = allWords.filter(word => uniqueWordIds.includes(word.id))
|
||||
|
||||
if (uniqueWords.length === 0) {
|
||||
return currentPool
|
||||
}
|
||||
|
||||
// Перемешиваем уникальные слова для случайности порядка
|
||||
const shuffledUniqueWords = [...uniqueWords]
|
||||
for (let i = shuffledUniqueWords.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[shuffledUniqueWords[i], shuffledUniqueWords[j]] = [shuffledUniqueWords[j], shuffledUniqueWords[i]]
|
||||
}
|
||||
|
||||
// Создаём массив с информацией о каждом слове и его количестве вхождений
|
||||
const wordEntries = shuffledUniqueWords.map(word => ({
|
||||
word,
|
||||
count: wordCounts[word.id]
|
||||
}))
|
||||
|
||||
// Сортируем по убыванию количества вхождений (слова с большим количеством вхождений размещаем первыми)
|
||||
wordEntries.sort((a, b) => b.count - a.count)
|
||||
|
||||
const totalSlots = currentPool.length
|
||||
const newPool = new Array(totalSlots).fill(null)
|
||||
|
||||
// Размещаем каждое слово с максимально возможным расстоянием между его вхождениями
|
||||
for (const entry of wordEntries) {
|
||||
const { word, count } = entry
|
||||
|
||||
// Вычисляем идеальный интервал между вхождениями этого слова
|
||||
const interval = totalSlots / count
|
||||
|
||||
// Размещаем каждое вхождение слова
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Начинаем с идеальной позиции
|
||||
let idealPos = Math.floor(i * interval + Math.random() * interval * 0.5)
|
||||
idealPos = Math.min(idealPos, totalSlots - 1)
|
||||
|
||||
// Если это исключаемое слово, не размещаем его на позиции 0
|
||||
if (word.id === excludeFirstWordId && idealPos === 0) {
|
||||
idealPos = 1
|
||||
}
|
||||
|
||||
// Ищем ближайшую свободную позицию
|
||||
let placed = false
|
||||
|
||||
// Сначала ищем вперёд
|
||||
for (let offset = 0; offset < totalSlots && !placed; offset++) {
|
||||
const pos = (idealPos + offset) % totalSlots
|
||||
// Пропускаем позицию 0 для исключаемого слова
|
||||
if (word.id === excludeFirstWordId && pos === 0) {
|
||||
continue
|
||||
}
|
||||
if (newPool[pos] === null) {
|
||||
newPool[pos] = word
|
||||
placed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Финальная проверка: если на позиции 0 оказалось исключаемое слово, меняем его с другим
|
||||
if (excludeFirstWordId !== null && newPool[0] && newPool[0].id === excludeFirstWordId) {
|
||||
// Ищем первое слово, которое не является исключаемым
|
||||
for (let i = 1; i < newPool.length; i++) {
|
||||
if (newPool[i] && newPool[i].id !== excludeFirstWordId) {
|
||||
// Меняем местами
|
||||
;[newPool[0], newPool[i]] = [newPool[i], newPool[0]]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newPool
|
||||
}
|
||||
|
||||
// Загрузка слов при монтировании
|
||||
useEffect(() => {
|
||||
setWords([])
|
||||
setTestWords([])
|
||||
setCurrentIndex(0)
|
||||
setCurrentWord(null)
|
||||
setFlippedCards(new Set())
|
||||
setWordStats({})
|
||||
wordStatsRef.current = {}
|
||||
@@ -81,16 +172,13 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
const n = Math.max(1, Math.floor(0.7 * cardsCount / wordsCount))
|
||||
|
||||
// Создаем пул, где каждое слово повторяется n раз
|
||||
const wordPool = []
|
||||
let wordPool = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
wordPool.push(...data)
|
||||
}
|
||||
|
||||
// Перемешиваем пул случайным образом
|
||||
for (let i = wordPool.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[wordPool[i], wordPool[j]] = [wordPool[j], wordPool[i]]
|
||||
}
|
||||
// Равномерно распределяем слова в пуле
|
||||
wordPool = redistributeWordsEvenly(wordPool, data)
|
||||
|
||||
setTestWords(wordPool)
|
||||
setWordStats(stats)
|
||||
@@ -111,16 +199,9 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
loadWords()
|
||||
}, [wordCount, configId])
|
||||
|
||||
const getCurrentWord = () => {
|
||||
if (currentIndex < testWords.length && testWords.length > 0) {
|
||||
return testWords[currentIndex]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Правый счётчик: кол-во полученных ответов + кол-во слов в пуле (не больше maxCards)
|
||||
// Правый счётчик: текущий размер пула + показанные карточки, но не больше maxCards
|
||||
const getRightCounter = () => {
|
||||
const total = totalAnswers + testWords.length
|
||||
const total = testWords.length + cardsShown
|
||||
if (maxCards !== null && maxCards > 0) {
|
||||
return Math.min(total, maxCards)
|
||||
}
|
||||
@@ -205,34 +286,39 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка условий завершения и показ следующей карточки
|
||||
const showNextCardOrFinish = (newTestWords, currentCardsShown) => {
|
||||
// Проверяем, не завершился ли тест (на случай если finishTest уже был вызван)
|
||||
// Берём карточку из пула (getAndDelete) и показываем её
|
||||
const showNextCard = () => {
|
||||
// Проверяем, не завершился ли тест
|
||||
if (isFinishingRef.current || showResults) {
|
||||
return
|
||||
}
|
||||
|
||||
// Условие 1: Достигли максимума карточек
|
||||
if (maxCards !== null && maxCards > 0 && currentCardsShown >= maxCards) {
|
||||
finishTest()
|
||||
return
|
||||
}
|
||||
// Используем функциональное обновление для получения актуального состояния пула
|
||||
setTestWords(prevPool => {
|
||||
// Условие 1: Достигли максимума карточек
|
||||
const nextCardsShown = cardsShown + 1
|
||||
if (maxCards !== null && maxCards > 0 && nextCardsShown > maxCards) {
|
||||
finishTest()
|
||||
return prevPool
|
||||
}
|
||||
|
||||
// Условие 2: Пул слов пуст
|
||||
if (newTestWords.length === 0) {
|
||||
finishTest()
|
||||
return
|
||||
}
|
||||
// Условие 2: Пул слов пуст
|
||||
if (prevPool.length === 0) {
|
||||
finishTest()
|
||||
return prevPool
|
||||
}
|
||||
|
||||
// Показываем следующую карточку и увеличиваем левый счётчик
|
||||
// Но сначала проверяем, не достигнем ли мы максимума после увеличения
|
||||
const nextCardsShown = currentCardsShown + 1
|
||||
if (maxCards !== null && maxCards > 0 && nextCardsShown > maxCards) {
|
||||
finishTest()
|
||||
return
|
||||
}
|
||||
// getAndDelete: берём слово из пула и удаляем его
|
||||
const nextWord = prevPool[0]
|
||||
const updatedPool = prevPool.slice(1)
|
||||
|
||||
setCardsShown(nextCardsShown)
|
||||
// showCard: показываем карточку
|
||||
setCurrentWord(nextWord)
|
||||
setCardsShown(nextCardsShown)
|
||||
setFlippedCards(new Set())
|
||||
|
||||
return updatedPool
|
||||
})
|
||||
}
|
||||
|
||||
const handleSuccess = (wordId) => {
|
||||
@@ -265,25 +351,9 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
const newTotalAnswers = totalAnswers + 1
|
||||
setTotalAnswers(newTotalAnswers)
|
||||
|
||||
// Убираем только один экземпляр слова из пула (по текущему индексу)
|
||||
const newTestWords = [...testWords]
|
||||
newTestWords.splice(currentIndex, 1)
|
||||
|
||||
// Обновляем индекс: если удалили последний элемент, переходим к предыдущему
|
||||
let newIndex = currentIndex
|
||||
if (newTestWords.length === 0) {
|
||||
newIndex = 0
|
||||
} else if (currentIndex >= newTestWords.length) {
|
||||
newIndex = newTestWords.length - 1
|
||||
}
|
||||
|
||||
// Обновляем состояние
|
||||
setTestWords(newTestWords)
|
||||
setCurrentIndex(newIndex)
|
||||
setFlippedCards(new Set())
|
||||
|
||||
// Проверяем условия завершения или показываем следующую карточку
|
||||
showNextCardOrFinish(newTestWords, cardsShown)
|
||||
// onSuccess: просто повторяем (showNextCard)
|
||||
// Карточка уже удалена из пула при показе, просто показываем следующую
|
||||
showNextCard()
|
||||
|
||||
// Если тест завершился, не сбрасываем processingRef
|
||||
if (isFinishingRef.current) {
|
||||
@@ -323,18 +393,20 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
const newTotalAnswers = totalAnswers + 1
|
||||
setTotalAnswers(newTotalAnswers)
|
||||
|
||||
// Слово остаётся в пуле, переходим к следующему
|
||||
let newIndex = currentIndex + 1
|
||||
if (newIndex >= testWords.length) {
|
||||
newIndex = 0
|
||||
}
|
||||
// onFailure: возвращаем карточку в пул, сортируем, повторяем
|
||||
setTestWords(prevPool => {
|
||||
// cards.add(currentCard): возвращаем слово обратно в пул
|
||||
let newTestWords = [...prevPool, word]
|
||||
|
||||
setCurrentIndex(newIndex)
|
||||
setFlippedCards(new Set())
|
||||
// cards.sort(): равномерно перераспределяем слова в пуле
|
||||
// Передаём wordId, чтобы текущая карточка не оказалась первой (следующей для показа)
|
||||
newTestWords = redistributeWordsEvenly(newTestWords, words, wordId)
|
||||
|
||||
// Проверяем условия завершения или показываем следующую карточку
|
||||
// При failure пул не изменяется
|
||||
showNextCardOrFinish(testWords, cardsShown)
|
||||
return newTestWords
|
||||
})
|
||||
|
||||
// repeat(): показываем следующую карточку
|
||||
showNextCard()
|
||||
|
||||
// Если тест завершился, не сбрасываем processingRef
|
||||
if (isFinishingRef.current) {
|
||||
@@ -350,8 +422,8 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
|
||||
const handleStartTest = () => {
|
||||
setShowPreview(false)
|
||||
// Показываем первую карточку и увеличиваем левый счётчик
|
||||
setCardsShown(1)
|
||||
// Показываем первую карточку (берём из пула)
|
||||
showNextCard()
|
||||
}
|
||||
|
||||
const handleFinish = () => {
|
||||
@@ -431,8 +503,8 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
||||
{error && (
|
||||
<div className="test-error">{error}</div>
|
||||
)}
|
||||
{!loading && !error && !isFinishingRef.current && getCurrentWord() && (() => {
|
||||
const word = getCurrentWord()
|
||||
{!loading && !error && !isFinishingRef.current && currentWord && (() => {
|
||||
const word = currentWord
|
||||
const isFlipped = flippedCards.has(word.id)
|
||||
const showSide = getRandomSide(word)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user