|
|
@@ -29,7 +29,6 @@ import type { FC } from 'react'
|
|
|
|
import type { GekanatorAnswerLog,
|
|
|
|
import type { GekanatorAnswerLog,
|
|
|
|
GekanatorAnswerValue,
|
|
|
|
GekanatorAnswerValue,
|
|
|
|
GekanatorExtraQuestion,
|
|
|
|
GekanatorExtraQuestion,
|
|
|
|
GekanatorPerformanceMode,
|
|
|
|
|
|
|
|
GekanatorQuestionCondition,
|
|
|
|
GekanatorQuestionCondition,
|
|
|
|
GekanatorQuestionKind,
|
|
|
|
GekanatorQuestionKind,
|
|
|
|
GekanatorQuestion,
|
|
|
|
GekanatorQuestion,
|
|
|
@@ -162,7 +161,6 @@ const confidenceTemperature = 6
|
|
|
|
const gameStorageKey = 'gekanator:game:v1'
|
|
|
|
const gameStorageKey = 'gekanator:game:v1'
|
|
|
|
const recentGamesStorageKey = 'gekanator:recent-games:v1'
|
|
|
|
const recentGamesStorageKey = 'gekanator:recent-games:v1'
|
|
|
|
const backgroundMotionStorageKey = 'gekanator:background-motion:v1'
|
|
|
|
const backgroundMotionStorageKey = 'gekanator:background-motion:v1'
|
|
|
|
const performanceModeStorageKey = 'gekanator:performance-mode:v1'
|
|
|
|
|
|
|
|
const maxQuestionSuggestionsPerGame = 3
|
|
|
|
const maxQuestionSuggestionsPerGame = 3
|
|
|
|
const maxStoredRecentGames = 12
|
|
|
|
const maxStoredRecentGames = 12
|
|
|
|
const mascotAssetByState: Record<MascotState, string> = {
|
|
|
|
const mascotAssetByState: Record<MascotState, string> = {
|
|
|
@@ -391,14 +389,8 @@ const storeRecentGameSummary = (
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const loadBackgroundMotionMode = (
|
|
|
|
const loadBackgroundMotionMode = (): BackgroundMotionMode => {
|
|
|
|
performanceMode?: GekanatorPerformanceMode,
|
|
|
|
const fallbackMode = 'on'
|
|
|
|
): BackgroundMotionMode => {
|
|
|
|
|
|
|
|
const fallbackMode =
|
|
|
|
|
|
|
|
performanceMode === 'lite' ? 'off'
|
|
|
|
|
|
|
|
: performanceMode === 'normal' ? 'on'
|
|
|
|
|
|
|
|
: detectDefaultPerformanceMode () === 'lite' ? 'off'
|
|
|
|
|
|
|
|
: 'on'
|
|
|
|
|
|
|
|
try
|
|
|
|
try
|
|
|
|
{
|
|
|
|
{
|
|
|
|
const raw = localStorage.getItem (backgroundMotionStorageKey)
|
|
|
|
const raw = localStorage.getItem (backgroundMotionStorageKey)
|
|
|
@@ -414,38 +406,6 @@ const loadBackgroundMotionMode = (
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const detectDefaultPerformanceMode = (): GekanatorPerformanceMode => {
|
|
|
|
|
|
|
|
if (typeof window === 'undefined')
|
|
|
|
|
|
|
|
return 'normal'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isMobileWidth =
|
|
|
|
|
|
|
|
typeof window.matchMedia === 'function'
|
|
|
|
|
|
|
|
? window.matchMedia ('(max-width: 767px)').matches
|
|
|
|
|
|
|
|
: window.innerWidth <= 767
|
|
|
|
|
|
|
|
const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory
|
|
|
|
|
|
|
|
if ((typeof memory === 'number' && memory <= 4) || isMobileWidth)
|
|
|
|
|
|
|
|
return 'lite'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return 'normal'
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const loadPerformanceMode = (): GekanatorPerformanceMode => {
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
const raw = localStorage.getItem (performanceModeStorageKey)
|
|
|
|
|
|
|
|
if (raw === 'lite' || raw === 'normal')
|
|
|
|
|
|
|
|
return raw
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return detectDefaultPerformanceMode ()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return detectDefaultPerformanceMode ()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const resettableExtraQuestionState = (): {
|
|
|
|
const resettableExtraQuestionState = (): {
|
|
|
|
extraQuestions: GekanatorExtraQuestion[]
|
|
|
|
extraQuestions: GekanatorExtraQuestion[]
|
|
|
|
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
|
|
|
|
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
|
|
|
@@ -1055,10 +1015,8 @@ const rankedEntriesForCounts = <T extends string | number> (
|
|
|
|
const buildQuestionsForCandidateIds = (
|
|
|
|
const buildQuestionsForCandidateIds = (
|
|
|
|
{ candidateIds,
|
|
|
|
{ candidateIds,
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
acceptedQuestions }: { candidateIds: number[]
|
|
|
|
acceptedQuestions }: { candidateIds: number[]
|
|
|
|
materialIndex: GekanatorQuestionMaterialIndex
|
|
|
|
materialIndex: GekanatorQuestionMaterialIndex
|
|
|
|
performanceMode: GekanatorPerformanceMode
|
|
|
|
|
|
|
|
acceptedQuestions: GekanatorQuestion[] },
|
|
|
|
acceptedQuestions: GekanatorQuestion[] },
|
|
|
|
): GekanatorQuestion[] => {
|
|
|
|
): GekanatorQuestion[] => {
|
|
|
|
const total = candidateIds.length
|
|
|
|
const total = candidateIds.length
|
|
|
@@ -1089,7 +1047,6 @@ const buildQuestionsForCandidateIds = (
|
|
|
|
const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
|
|
|
|
const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
|
|
|
|
if (monthDay)
|
|
|
|
if (monthDay)
|
|
|
|
monthDayCounts.set (monthDay, (monthDayCounts.get (monthDay) ?? 0) + 1)
|
|
|
|
monthDayCounts.set (monthDay, (monthDayCounts.get (monthDay) ?? 0) + 1)
|
|
|
|
if (performanceMode === 'normal')
|
|
|
|
|
|
|
|
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
|
|
|
|
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
|
|
|
|
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
|
|
|
|
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
|
|
|
|
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
|
|
|
|
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
|
|
|
@@ -1098,14 +1055,8 @@ const buildQuestionsForCandidateIds = (
|
|
|
|
++asciiCount
|
|
|
|
++asciiCount
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const tagCap =
|
|
|
|
const tagCap = total >= 120 ? 128 : 96
|
|
|
|
performanceMode === 'lite'
|
|
|
|
const titleTermCap = total >= 80 ? 10 : total >= 24 ? 14 : 20
|
|
|
|
? total >= 80 ? 96 : 64
|
|
|
|
|
|
|
|
: total >= 120 ? 128 : 96
|
|
|
|
|
|
|
|
const titleTermCap =
|
|
|
|
|
|
|
|
performanceMode === 'lite'
|
|
|
|
|
|
|
|
? 0
|
|
|
|
|
|
|
|
: total >= 80 ? 10 : total >= 24 ? 14 : 20
|
|
|
|
|
|
|
|
const factCap = total >= 80 ? 8 : 12
|
|
|
|
const factCap = total >= 80 ? 8 : 12
|
|
|
|
const sortedLengths = [...titleLengths].sort ((a, b) => a - b)
|
|
|
|
const sortedLengths = [...titleLengths].sort ((a, b) => a - b)
|
|
|
|
const titleLengthMedian = sortedLengths[Math.floor (sortedLengths.length / 2)] ?? 0
|
|
|
|
const titleLengthMedian = sortedLengths[Math.floor (sortedLengths.length / 2)] ?? 0
|
|
|
@@ -1181,7 +1132,6 @@ const buildQuestionsForCandidateIds = (
|
|
|
|
materialIndex }))
|
|
|
|
materialIndex }))
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (performanceMode === 'normal')
|
|
|
|
|
|
|
|
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
|
|
|
|
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
|
|
|
|
.filter (([term]) => String (term).length <= 24)
|
|
|
|
.filter (([term]) => String (term).length <= 24)
|
|
|
|
.forEach (([term]) => {
|
|
|
|
.forEach (([term]) => {
|
|
|
@@ -1615,8 +1565,8 @@ const contradictionPenaltyFor = ({
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const chooseQuestion = ({
|
|
|
|
const chooseQuestion = (
|
|
|
|
posts,
|
|
|
|
{ posts,
|
|
|
|
questions,
|
|
|
|
questions,
|
|
|
|
scores,
|
|
|
|
scores,
|
|
|
|
answers,
|
|
|
|
answers,
|
|
|
@@ -1624,11 +1574,8 @@ const chooseQuestion = ({
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex,
|
|
|
|
matchIndex }: { posts: Post[]
|
|
|
|
}: {
|
|
|
|
|
|
|
|
posts: Post[]
|
|
|
|
|
|
|
|
questions: GekanatorQuestion[]
|
|
|
|
questions: GekanatorQuestion[]
|
|
|
|
scores: Map<number, number>
|
|
|
|
scores: Map<number, number>
|
|
|
|
answers: GekanatorAnswerLog[]
|
|
|
|
answers: GekanatorAnswerLog[]
|
|
|
@@ -1636,20 +1583,15 @@ const chooseQuestion = ({
|
|
|
|
gameSeed: string
|
|
|
|
gameSeed: string
|
|
|
|
recentFirstQuestionPenaltyById: Map<string, number>
|
|
|
|
recentFirstQuestionPenaltyById: Map<string, number>
|
|
|
|
userPriorWeights: Map<number, number>
|
|
|
|
userPriorWeights: Map<number, number>
|
|
|
|
performanceMode: GekanatorPerformanceMode
|
|
|
|
|
|
|
|
materialIndex: GekanatorQuestionMaterialIndex
|
|
|
|
materialIndex: GekanatorQuestionMaterialIndex
|
|
|
|
matchIndex: GekanatorMatchIndex
|
|
|
|
matchIndex: GekanatorMatchIndex },
|
|
|
|
}): GekanatorQuestion | null => {
|
|
|
|
): GekanatorQuestion | null => {
|
|
|
|
const candidateIds = posts.map (post => post.id)
|
|
|
|
|
|
|
|
const candidateIdSet = new Set (candidateIds)
|
|
|
|
|
|
|
|
const dynamicMatchIndex = new Map<string, Set<number>> ()
|
|
|
|
const dynamicMatchIndex = new Map<string, Set<number>> ()
|
|
|
|
|
|
|
|
|
|
|
|
const invertedSignature = (signature: string): string =>
|
|
|
|
const invertedSignature = (signature: string): string =>
|
|
|
|
signature.replace (/[01]/g, value => value === '1' ? '0' : '1')
|
|
|
|
signature.replace (/[01]/g, value => value === '1' ? '0' : '1')
|
|
|
|
|
|
|
|
|
|
|
|
const redundantSignatures = (
|
|
|
|
const redundantSignatures = (candidates: Post[]): Set<string> => {
|
|
|
|
candidates: Post[],
|
|
|
|
|
|
|
|
): Set<string> => {
|
|
|
|
|
|
|
|
const signatures = new Set<string> ()
|
|
|
|
const signatures = new Set<string> ()
|
|
|
|
questions
|
|
|
|
questions
|
|
|
|
.filter (question => askedIds.has (question.id))
|
|
|
|
.filter (question => askedIds.has (question.id))
|
|
|
@@ -1668,89 +1610,6 @@ const chooseQuestion = ({
|
|
|
|
return signatures
|
|
|
|
return signatures
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (performanceMode === 'lite')
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
const nonTagCount =
|
|
|
|
|
|
|
|
questions.filter (question => askedIds.has (question.id) && question.kind !== 'tag').length
|
|
|
|
|
|
|
|
const ranked = questions
|
|
|
|
|
|
|
|
.filter (question => !(askedIds.has (question.id)))
|
|
|
|
|
|
|
|
.map (question => {
|
|
|
|
|
|
|
|
if (isQuestionHardFilteredAfterAnswers (question, answers))
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const yes = matchingPostCountInIds ({
|
|
|
|
|
|
|
|
candidateIds,
|
|
|
|
|
|
|
|
candidateIdSet,
|
|
|
|
|
|
|
|
posts,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
|
|
|
|
matchIndex,
|
|
|
|
|
|
|
|
question,
|
|
|
|
|
|
|
|
dynamicMatchIndex })
|
|
|
|
|
|
|
|
const no = posts.length - yes
|
|
|
|
|
|
|
|
if (yes === 0 || no === 0)
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const splitScore = Math.abs (posts.length / 2 - yes) / posts.length
|
|
|
|
|
|
|
|
const minSide = posts.length < 10 ? 1 : Math.max (2, Math.floor (posts.length * .08))
|
|
|
|
|
|
|
|
const narrowPenalty = yes < minSide || no < minSide ? .18 : 0
|
|
|
|
|
|
|
|
const tagPenalty = question.kind === 'tag' && nonTagCount < 3 ? .1 : 0
|
|
|
|
|
|
|
|
const contradictionPenalty = contradictionPenaltyFor ({ question, answers })
|
|
|
|
|
|
|
|
const sourceBonus = sourcePriorityOffset (question)
|
|
|
|
|
|
|
|
const priorityBonus = priorityWeightOffset (question)
|
|
|
|
|
|
|
|
const categoryPenalty = questionCategoryPenalty (question, answers.length, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
question,
|
|
|
|
|
|
|
|
score: splitScore * 100
|
|
|
|
|
|
|
|
+ narrowPenalty
|
|
|
|
|
|
|
|
+ tagPenalty
|
|
|
|
|
|
|
|
+ contradictionPenalty
|
|
|
|
|
|
|
|
+ sourceBonus
|
|
|
|
|
|
|
|
+ priorityBonus
|
|
|
|
|
|
|
|
+ categoryPenalty,
|
|
|
|
|
|
|
|
narrow: narrowPenalty > 0 }
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.filter ((item): item is {
|
|
|
|
|
|
|
|
question: GekanatorQuestion
|
|
|
|
|
|
|
|
score: number
|
|
|
|
|
|
|
|
narrow: boolean } => item !== null && Number.isFinite (item.score))
|
|
|
|
|
|
|
|
.sort ((a, b) => {
|
|
|
|
|
|
|
|
if (a.score !== b.score)
|
|
|
|
|
|
|
|
return a.score - b.score
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return a.question.id.localeCompare (b.question.id)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
const pool = (
|
|
|
|
|
|
|
|
ranked.some (item => !(item.narrow))
|
|
|
|
|
|
|
|
? ranked.filter (item => !(item.narrow))
|
|
|
|
|
|
|
|
: ranked)
|
|
|
|
|
|
|
|
.slice (0, 8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (pool.length === 0)
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const bestScore = pool[0]?.score ?? 0
|
|
|
|
|
|
|
|
const weightedPool = pool.map (item => ({
|
|
|
|
|
|
|
|
...item,
|
|
|
|
|
|
|
|
weight: Math.exp ((bestScore - item.score) / 1.6) }))
|
|
|
|
|
|
|
|
const totalPoolWeight =
|
|
|
|
|
|
|
|
weightedPool.reduce ((sum, item) => sum + item.weight, 0) || 1
|
|
|
|
|
|
|
|
const seed = `${ gameSeed }:lite:${ [...askedIds].sort ().join ('|') }:${
|
|
|
|
|
|
|
|
weightedPool.map (item => `${ item.question.id }:${ item.score.toFixed (4) }`).join ('|')
|
|
|
|
|
|
|
|
}`
|
|
|
|
|
|
|
|
const target = deterministicUnitFloat (seed) * totalPoolWeight
|
|
|
|
|
|
|
|
let cumulative = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of weightedPool)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
cumulative += item.weight
|
|
|
|
|
|
|
|
if (target <= cumulative)
|
|
|
|
|
|
|
|
return item.question
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return weightedPool[weightedPool.length - 1]?.question ?? null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const scoredPosts = posts
|
|
|
|
const scoredPosts = posts
|
|
|
|
.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
|
|
|
|
.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
|
|
|
|
.sort ((a, b) => b.score - a.score)
|
|
|
|
.sort ((a, b) => b.score - a.score)
|
|
|
@@ -2361,7 +2220,6 @@ const nextQuestionPlanFor = (
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex,
|
|
|
|
matchIndex,
|
|
|
|
lastGuessQuestionCount,
|
|
|
|
lastGuessQuestionCount,
|
|
|
@@ -2376,7 +2234,6 @@ const nextQuestionPlanFor = (
|
|
|
|
gameSeed: string
|
|
|
|
gameSeed: string
|
|
|
|
recentFirstQuestionPenaltyById: Map<string, number>
|
|
|
|
recentFirstQuestionPenaltyById: Map<string, number>
|
|
|
|
userPriorWeights: Map<number, number>
|
|
|
|
userPriorWeights: Map<number, number>
|
|
|
|
performanceMode: GekanatorPerformanceMode
|
|
|
|
|
|
|
|
materialIndex: GekanatorQuestionMaterialIndex
|
|
|
|
materialIndex: GekanatorQuestionMaterialIndex
|
|
|
|
matchIndex: GekanatorMatchIndex
|
|
|
|
matchIndex: GekanatorMatchIndex
|
|
|
|
lastGuessQuestionCount: number
|
|
|
|
lastGuessQuestionCount: number
|
|
|
@@ -2439,7 +2296,6 @@ const nextQuestionPlanFor = (
|
|
|
|
buildQuestionsForCandidateIds ({
|
|
|
|
buildQuestionsForCandidateIds ({
|
|
|
|
candidateIds: scopePosts.map (post => post.id),
|
|
|
|
candidateIds: scopePosts.map (post => post.id),
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
acceptedQuestions })
|
|
|
|
acceptedQuestions })
|
|
|
|
|
|
|
|
|
|
|
|
if (eligiblePosts.length === 1)
|
|
|
|
if (eligiblePosts.length === 1)
|
|
|
@@ -2505,7 +2361,6 @@ const nextQuestionPlanFor = (
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex })
|
|
|
|
matchIndex })
|
|
|
|
|
|
|
|
|
|
|
@@ -3108,10 +2963,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
const canPersistGame = user !== null
|
|
|
|
const canPersistGame = user !== null
|
|
|
|
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
|
|
|
|
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
|
|
|
|
() => loadRecentGames ())
|
|
|
|
() => loadRecentGames ())
|
|
|
|
const [performanceMode] =
|
|
|
|
|
|
|
|
useState<GekanatorPerformanceMode> (() => loadPerformanceMode ())
|
|
|
|
|
|
|
|
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
|
|
|
|
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
|
|
|
|
() => loadBackgroundMotionMode (loadPerformanceMode ()))
|
|
|
|
() => loadBackgroundMotionMode ())
|
|
|
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState (false)
|
|
|
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState (false)
|
|
|
|
const [gameSeed, setGameSeed] = useState (
|
|
|
|
const [gameSeed, setGameSeed] = useState (
|
|
|
|
storedGame?.gameSeed ?? createGameSeed ())
|
|
|
|
storedGame?.gameSeed ?? createGameSeed ())
|
|
|
@@ -3328,17 +3181,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [backgroundMotionMode])
|
|
|
|
}, [backgroundMotionMode])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect (() => {
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
localStorage.setItem (performanceModeStorageKey, performanceMode)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}, [performanceMode])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const askedQuestionById = useMemo (
|
|
|
|
const askedQuestionById = useMemo (
|
|
|
|
() => new Map (askedQuestionBank.map (question => [question.id, question])),
|
|
|
|
() => new Map (askedQuestionBank.map (question => [question.id, question])),
|
|
|
|
[askedQuestionBank])
|
|
|
|
[askedQuestionBank])
|
|
|
@@ -3361,9 +3203,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
() => new Map (scoringQuestions.map (question => [question.id, question])),
|
|
|
|
() => new Map (scoringQuestions.map (question => [question.id, question])),
|
|
|
|
[scoringQuestions])
|
|
|
|
[scoringQuestions])
|
|
|
|
const recentFirstQuestionPenaltyById = useMemo (() => {
|
|
|
|
const recentFirstQuestionPenaltyById = useMemo (() => {
|
|
|
|
if (performanceMode === 'lite')
|
|
|
|
|
|
|
|
return new Map<string, number> ()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const penalties = new Map<string, number> ()
|
|
|
|
const penalties = new Map<string, number> ()
|
|
|
|
|
|
|
|
|
|
|
|
recentGames.forEach ((game, index) => {
|
|
|
|
recentGames.forEach ((game, index) => {
|
|
|
@@ -3376,12 +3215,10 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return penalties
|
|
|
|
return penalties
|
|
|
|
}, [performanceMode, recentGames])
|
|
|
|
}, [recentGames])
|
|
|
|
const userPriorWeights = useMemo (
|
|
|
|
const userPriorWeights = useMemo (
|
|
|
|
() => performanceMode === 'lite'
|
|
|
|
() => userPriorWeightsFor (posts, recentGames),
|
|
|
|
? new Map<number, number> ()
|
|
|
|
[posts, recentGames])
|
|
|
|
: userPriorWeightsFor (posts, recentGames),
|
|
|
|
|
|
|
|
[performanceMode, posts, recentGames])
|
|
|
|
|
|
|
|
const availablePosts = useMemo (
|
|
|
|
const availablePosts = useMemo (
|
|
|
|
() => posts.filter (post => !(rejectedPostIds.has (post.id))),
|
|
|
|
() => posts.filter (post => !(rejectedPostIds.has (post.id))),
|
|
|
|
[posts, rejectedPostIds])
|
|
|
|
[posts, rejectedPostIds])
|
|
|
@@ -3397,7 +3234,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
lastGuessQuestionCount,
|
|
|
|
lastGuessQuestionCount,
|
|
|
@@ -3405,7 +3241,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
winningRunStartAnswerCount }),
|
|
|
|
winningRunStartAnswerCount }),
|
|
|
|
[posts, eligiblePosts, availablePosts, acceptedQuestions, scores,
|
|
|
|
[posts, eligiblePosts, availablePosts, acceptedQuestions, scores,
|
|
|
|
answers, askedIds, gameSeed, recentFirstQuestionPenaltyById,
|
|
|
|
answers, askedIds, gameSeed, recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights, performanceMode, materialIndex, acceptedQuestionMatchIndex,
|
|
|
|
userPriorWeights, materialIndex, acceptedQuestionMatchIndex,
|
|
|
|
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
|
|
|
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
|
|
|
|
const winningRunTargetPost = useMemo (
|
|
|
|
const winningRunTargetPost = useMemo (
|
|
|
|
() => questionPlan.winningRunTargetId === null
|
|
|
|
() => questionPlan.winningRunTargetId === null
|
|
|
@@ -3458,35 +3294,28 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
const reviewCorrectPost =
|
|
|
|
const reviewCorrectPost =
|
|
|
|
posts.find (post => post.id === reviewCorrectPostId) ?? null
|
|
|
|
posts.find (post => post.id === reviewCorrectPostId) ?? null
|
|
|
|
const effectiveResultWon =
|
|
|
|
const effectiveResultWon =
|
|
|
|
resultWon ?? (
|
|
|
|
resultWon
|
|
|
|
reviewGuessedPostId !== null
|
|
|
|
?? ((reviewGuessedPostId !== null && reviewCorrectPostId !== null)
|
|
|
|
&& reviewCorrectPostId !== null
|
|
|
|
|
|
|
|
? reviewGuessedPostId === reviewCorrectPostId
|
|
|
|
? reviewGuessedPostId === reviewCorrectPostId
|
|
|
|
: null)
|
|
|
|
: null)
|
|
|
|
const effectiveBackgroundMotionMode =
|
|
|
|
const effectiveBackgroundMotionMode =
|
|
|
|
performanceMode === 'lite'
|
|
|
|
backgroundMotionMode === 'off'
|
|
|
|
? 'off'
|
|
|
|
? 'off'
|
|
|
|
: backgroundMotionMode === 'off'
|
|
|
|
: (prefersReducedMotion
|
|
|
|
? 'off'
|
|
|
|
|
|
|
|
: prefersReducedMotion
|
|
|
|
|
|
|
|
? 'calm'
|
|
|
|
? 'calm'
|
|
|
|
: backgroundMotionMode
|
|
|
|
: backgroundMotionMode)
|
|
|
|
const backgroundPosts = useMemo (
|
|
|
|
const backgroundPosts = useMemo (
|
|
|
|
() => performanceMode === 'lite'
|
|
|
|
() => backgroundPostsFor ({
|
|
|
|
? []
|
|
|
|
|
|
|
|
: backgroundPostsFor ({
|
|
|
|
|
|
|
|
phase,
|
|
|
|
phase,
|
|
|
|
eligiblePosts,
|
|
|
|
eligiblePosts,
|
|
|
|
availablePosts,
|
|
|
|
availablePosts,
|
|
|
|
displayedGuess,
|
|
|
|
displayedGuess,
|
|
|
|
reviewCorrectPost,
|
|
|
|
reviewCorrectPost,
|
|
|
|
reviewGuessedPost }),
|
|
|
|
reviewGuessedPost }),
|
|
|
|
[performanceMode, phase, eligiblePosts, availablePosts, displayedGuess,
|
|
|
|
[phase, eligiblePosts, availablePosts, displayedGuess, reviewCorrectPost,
|
|
|
|
reviewCorrectPost, reviewGuessedPost])
|
|
|
|
reviewGuessedPost])
|
|
|
|
const backgroundVisualSeed =
|
|
|
|
const backgroundVisualSeed =
|
|
|
|
performanceMode === 'lite'
|
|
|
|
`${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
|
|
|
|
? ''
|
|
|
|
|
|
|
|
: `${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
|
|
|
|
|
|
|
|
questionPlan.question?.id ?? ''
|
|
|
|
questionPlan.question?.id ?? ''
|
|
|
|
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
|
|
|
|
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
|
|
|
|
rejectedPostIds.size
|
|
|
|
rejectedPostIds.size
|
|
|
@@ -3617,7 +3446,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
let recoveredQuestions = buildQuestionsForCandidateIds ({
|
|
|
|
let recoveredQuestions = buildQuestionsForCandidateIds ({
|
|
|
|
candidateIds: recoveredEligiblePosts.map (post => post.id),
|
|
|
|
candidateIds: recoveredEligiblePosts.map (post => post.id),
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
acceptedQuestions })
|
|
|
|
acceptedQuestions })
|
|
|
|
let recoveredScoringQuestions = mergeQuestions ([
|
|
|
|
let recoveredScoringQuestions = mergeQuestions ([
|
|
|
|
...recoveredQuestions,
|
|
|
|
...recoveredQuestions,
|
|
|
@@ -3643,7 +3471,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
recoveredQuestions = buildQuestionsForCandidateIds ({
|
|
|
|
recoveredQuestions = buildQuestionsForCandidateIds ({
|
|
|
|
candidateIds: recoveredEligiblePosts.map (post => post.id),
|
|
|
|
candidateIds: recoveredEligiblePosts.map (post => post.id),
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
acceptedQuestions })
|
|
|
|
acceptedQuestions })
|
|
|
|
recoveredScoringQuestions = mergeQuestions ([
|
|
|
|
recoveredScoringQuestions = mergeQuestions ([
|
|
|
|
...recoveredQuestions,
|
|
|
|
...recoveredQuestions,
|
|
|
@@ -3667,7 +3494,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex })
|
|
|
|
matchIndex: acceptedQuestionMatchIndex })
|
|
|
|
const fallbackQuestion = nextQuestion ?? chooseFallbackQuestion ({
|
|
|
|
const fallbackQuestion = nextQuestion ?? chooseFallbackQuestion ({
|
|
|
@@ -3735,7 +3561,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
}, [
|
|
|
|
}, [
|
|
|
|
posts,
|
|
|
|
posts,
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
acceptedQuestions,
|
|
|
|
acceptedQuestions,
|
|
|
|
acceptedQuestionMatchIndex,
|
|
|
|
acceptedQuestionMatchIndex,
|
|
|
@@ -3806,7 +3631,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
lastGuessQuestionCount,
|
|
|
|
lastGuessQuestionCount,
|
|
|
@@ -3840,7 +3664,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
lastGuessQuestionCount,
|
|
|
|
lastGuessQuestionCount,
|
|
|
@@ -4071,7 +3894,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
gameSeed,
|
|
|
|
gameSeed,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
recentFirstQuestionPenaltyById,
|
|
|
|
userPriorWeights,
|
|
|
|
userPriorWeights,
|
|
|
|
performanceMode,
|
|
|
|
|
|
|
|
materialIndex,
|
|
|
|
materialIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
matchIndex: acceptedQuestionMatchIndex,
|
|
|
|
lastGuessQuestionCount,
|
|
|
|
lastGuessQuestionCount,
|
|
|
@@ -4286,7 +4108,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
|
|
|
|
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
|
|
|
|
</Helmet>
|
|
|
|
</Helmet>
|
|
|
|
|
|
|
|
|
|
|
|
{performanceMode !== 'lite' && (
|
|
|
|
|
|
|
|
<GekanatorBackdrop
|
|
|
|
<GekanatorBackdrop
|
|
|
|
posts={backgroundPosts}
|
|
|
|
posts={backgroundPosts}
|
|
|
|
mascotAsset={mascotAsset}
|
|
|
|
mascotAsset={mascotAsset}
|
|
|
@@ -4295,7 +4116,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
visualSeed={backgroundVisualSeed}
|
|
|
|
visualSeed={backgroundVisualSeed}
|
|
|
|
motionMode={effectiveBackgroundMotionMode}
|
|
|
|
motionMode={effectiveBackgroundMotionMode}
|
|
|
|
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
|
|
|
|
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
|
|
|
|
winningRunQuestionCount={winningRunQuestionsAsked}/>)}
|
|
|
|
winningRunQuestionCount={winningRunQuestionsAsked}/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="relative z-10 mx-auto max-w-4xl space-y-6">
|
|
|
|
<div className="relative z-10 mx-auto max-w-4xl space-y-6">
|
|
|
|
<header className="flex flex-wrap items-end justify-between gap-3">
|
|
|
|
<header className="flex flex-wrap items-end justify-between gap-3">
|
|
|
@@ -4305,7 +4126,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
</h1>
|
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="flex flex-wrap justify-end gap-2">
|
|
|
|
<div className="flex flex-wrap justify-end gap-2">
|
|
|
|
{performanceMode === 'normal' && (
|
|
|
|
|
|
|
|
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
|
|
|
|
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
|
|
|
|
text-xs shadow-sm backdrop-blur dark:border-red-800
|
|
|
|
text-xs shadow-sm backdrop-blur dark:border-red-800
|
|
|
|
dark:bg-red-950/75">
|
|
|
|
dark:bg-red-950/75">
|
|
|
@@ -4330,7 +4150,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
|
|
|
|
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
|
|
|
|
端末設定により控えめ表示
|
|
|
|
端末設定により控えめ表示
|
|
|
|
</span>)}
|
|
|
|
</span>)}
|
|
|
|
</div>)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</header>
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
@@ -4608,7 +4428,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
|
|
|
|
|
|
|
|
|
|
|
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
|
|
|
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
|
|
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
|
|
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
|
|
|
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'}
|
|
|
|
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
|
|
|
</p>)}
|
|
|
|
</p>)}
|
|
|
|
|
|
|
|
|
|
|
|
{saveMutation.isError && (
|
|
|
|
{saveMutation.isError && (
|
|
|
|